From 6a66b15f56f6042371bb7695dbd116cb0ab22352 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 28 Jan 2018 03:12:35 -0600 Subject: [PATCH] =?UTF-8?q?Importar=20n=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/util.py | 143 +++++++++++++-- source/app/models/main.py | 314 +++++++++++++++++++++++++++++++-- source/app/settings.py | 3 + 3 files changed, 431 insertions(+), 29 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index de8c97d..eb871c5 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1054,26 +1054,126 @@ class LIBO(object): msg = 'Empleados importados correctamente' return rows, msg + def _get_nomina(self, doc): + rows, msg = self._get_data(doc, 'Nomina') + if len(rows) == 2: + msg = 'Sin datos para importar' + return {}, msg + + fields = ( + 'rfc', + 'tipo_nomina', + 'fecha_pago', + 'fecha_inicial_pago', + 'fecha_final_pago', + ) + data = tuple([dict(zip(fields, r[1:])) for r in rows[2:]]) + return data, '' + + def _get_percepciones(self, doc, count): + rows, msg = self._get_data(doc, 'Percepciones') + if len(rows) == 2: + msg = 'Sin Percepciones' + return {}, msg + + if len(rows[0][2:]) % 2: + msg = 'Las Percepciones deben ir en pares: Gravado y Exento' + return {}, msg + + data = tuple([r[2:] for r in rows[:count+2]]) + return data, '' + + def _get_deducciones(self, doc, count): + rows, msg = self._get_data(doc, 'Deducciones') + if len(rows) == 2: + msg = 'Sin Deducciones' + return {}, msg + + data = tuple([r[2:] for r in rows[:count+2]]) + return data, '' + + def _get_otros_pagos(self, doc, count): + rows, msg = self._get_data(doc, 'OtrosPagos') + if len(rows) == 2: + msg = 'Sin Otros Pagos' + return {}, msg + + data = tuple([r[2:] for r in rows[:count+2]]) + return data, '' + + def _get_horas_extras(self, doc, count): + rows, msg = self._get_data(doc, 'HorasExtras') + if len(rows) == 2: + msg = 'Sin Horas Extras' + return {}, msg + + if len(rows[1][1:]) % 4: + msg = 'Las Horas Extras deben ir grupos de 4 columnas' + return {}, msg + + data = tuple([r[1:] for r in rows[:count+2]]) + return data, '' + + def _get_incapacidades(self, doc, count): + rows, msg = self._get_data(doc, 'Incapacidades') + if len(rows) == 2: + msg = 'Sin Incapacidades' + return {}, msg + + if len(rows[1][1:]) % 3: + msg = 'Las Incapacidades deben ir grupos de 3 columnas' + return {}, msg + + data = tuple([r[1:] for r in rows[:count+2]]) + return data, '' + def nomina(self, path): options = {'AsTemplate': True, 'Hidden': True} doc = self._doc_open(path, options) if doc is None: - return () + msg = 'No se pudo abrir la plantilla' + return {}, msg + + data = {} + nomina, msg = self._get_nomina(doc) + if msg: + doc.close(True) + return {}, msg + + percepciones, msg = self._get_percepciones(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + deducciones, msg = self._get_deducciones(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + otros_pagos, msg = self._get_otros_pagos(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + horas_extras, msg = self._get_horas_extras(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + incapacidades, msg = self._get_incapacidades(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg - data, msg = self._get_data(doc, 'Nomina') doc.close(True) + data['nomina'] = nomina + data['percepciones'] = percepciones + data['deducciones'] = deducciones + data['otros_pagos'] = otros_pagos + data['horas_extras'] = horas_extras + data['incapacidades'] = incapacidades - if len(data) == 1: - msg = 'Sin datos para importar' - return (), msg - - fields = ( - 'num_empleado', - 'rfc', - ) - rows = tuple([dict(zip(fields, r)) for r in data[1:]]) - msg = 'Nomina importada correctamente' - return rows, msg + return data, '' def invoice(self, path): options = {'AsTemplate': True, 'Hidden': True} @@ -2846,3 +2946,20 @@ def import_invoice(rfc): def calc_to_date(value): return datetime.date.fromordinal(int(value) + 693594) + + +def get_days(start, end): + return (end - start).days + 1 + + +def log_file(name, msg='', kill=False): + path = _join(PATH_MEDIA, 'tmp', '{}.log'.format(name)) + + if kill: + _kill(path) + return + + with open(path, 'a') as fh: + line = '{} : {}'.format(str(now()), msg) + fh.write(line) + return diff --git a/source/app/models/main.py b/source/app/models/main.py index 3d2a68a..5ee418d 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5421,6 +5421,8 @@ class Empleados(BaseModel): en = 0 ea = 0 for row in rows: + # ~ if row['rfc'] == 'BASM740115RW0': + # ~ continue data = self._validate_import(self, row) w = (Empleados.rfc==row['rfc']) with database_proxy.transaction(): @@ -5471,7 +5473,7 @@ class Empleados(BaseModel): class CfdiNomina(BaseModel): empleado = ForeignKeyField(Empleados) version = TextField(default=CURRENT_CFDI) - serie = TextField(default='') + serie = TextField(default='N') folio = IntegerField(default=0) fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) fecha_timbrado = DateTimeField(null=True) @@ -5488,7 +5490,7 @@ class CfdiNomina(BaseModel): auto_round=True) total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) - tipo_comprobante = TextField(default='I') + tipo_comprobante = TextField(default='N') metodo_pago = TextField(default='PUE') lugar_expedicion = TextField(default='') confirmacion = TextField(default='') @@ -5511,7 +5513,7 @@ class CfdiNomina(BaseModel): acuse = TextField(default='') tipo_relacion = TextField(default='') error = TextField(default='') - version = TextField(default=CURRENT_CFDI_NOMINA) + version_nomina = TextField(default=CURRENT_CFDI_NOMINA) registro_patronal = TextField(default='') rfc_patron_origen = TextField(default='') tipo_nomina = ForeignKeyField(SATTipoNomina) @@ -5527,27 +5529,304 @@ class CfdiNomina(BaseModel): class Meta: order_by = ('fecha',) - def _validate_import(self, row): + def _get_serie(self): + serie = Configuracion.get_('chk_config_serie_nomina') + if not serie: + serie = DEFAULT_SAT_NOMINA['SERIE'] + return serie + + def _get_folio(self, serie): + folio = int(Configuracion.get_('chk_config_folio_nomina') or '0') + inicio = (CfdiNomina + .select(fn.Max(CfdiNomina.folio).alias('mf')) + .where(CfdiNomina.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None: + new = 1 + else: + new += 1 + + if folio > new: + new = folio + + return new + + def _validate_nomina(self, row): sn = {'si': True, 'no': False} data = row.copy() - return data + rfc = data.pop('rfc') + try: + data['empleado'] = Empleados.get(Empleados.rfc==rfc) + except Empleados.DoesNotExist: + msg = 'No existe el Empleado con RFC: {}'.format(rfc) + return {}, msg + + tipo_nomina = SATTipoNomina.get_by_key(row['tipo_nomina']) + if tipo_nomina is None: + msg = 'RFC: {}, Tipo de Nómina no existe: {}'.format(row['tipo_nomina']) + return {}, msg + + data['serie'] = self._get_serie(self) + data['folio'] = self._get_folio(self, data['serie']) + data['forma_pago'] = DEFAULT_SAT_NOMINA['FORMA_PAGO'] + data['uso_cfdi'] = DEFAULT_SAT_NOMINA['USO_CFDI'] + data['tipo_nomina'] = tipo_nomina + data['fecha_pago'] = util.calc_to_date(row['fecha_pago']) + data['fecha_inicial_pago'] = util.calc_to_date(row['fecha_inicial_pago']) + data['fecha_final_pago'] = util.calc_to_date(row['fecha_final_pago']) + data['dias_pagados'] = util.get_days(data['fecha_inicial_pago'], data['fecha_final_pago']) + + return data, '' + + def _validate_percepciones(self, headers, row): + total_gravado = 0.0 + total_exento = 0.0 + total_jubilacion = 0.0 + total_separacion = 0.0 + + data = [] + for i, key in enumerate(headers[::2]): + gravado = round(row[i * 2], DECIMALES) + exento = round(row[i * 2 + 1], DECIMALES) + if not gravado and not exento: + continue + tp = SATTipoPercepcion.get_by_key(key) + if tp is None: + continue + + total_gravado += gravado + total_exento += exento + if key in ('039', '044'): + total_jubilacion += gravado + exento + elif key in ('022', '023', '025'): + total_separacion += gravado + exento + new = { + 'tipo_percepcion': tp, + 'importe_gravado': gravado, + 'importe_exento': exento, + } + data.append(new) + + total_sueldos = round(total_gravado + total_exento, DECIMALES) + totals = { + 'total_gravado': total_gravado, + 'total_exento': total_exento, + 'total_jubilacion': total_jubilacion, + 'total_separacion': total_separacion, + 'total_sueldos': total_sueldos, + 'total_percepciones': round( + total_sueldos + total_jubilacion + total_separacion, DECIMALES) + } + + return data, totals, '' + + def _validate_deducciones(self, headers, row): + total_retenciones = 0.0 + total_otras_deducciones = 0.0 + + data = [] + for i, value in enumerate(row): + key = headers[0][i] + importe = round(value, DECIMALES) + if not importe: + continue + + td = SATTipoDeduccion.get_by_key(key) + if td is None: + continue + + if key == '002': + total_retenciones += importe + else: + total_otras_deducciones += importe + + new = { + 'tipo_deduccion': td, + 'importe': importe, + } + data.append(new) + + totals = { + 'total_retenciones': total_retenciones, + 'total_otras_deducciones': total_otras_deducciones, + 'total_deducciones': round( + total_retenciones + total_otras_deducciones, DECIMALES) + } + + return data, totals, '' + + def _validate_otros_pagos(self, headers, row): + total_otros_pagos = 0.0 + + data = [] + subsidio_causado = round(row[0], DECIMALES) + for i, value in enumerate(row): + if not i: + continue + + key = headers[0][i] + importe = round(value, DECIMALES) + if not importe: + continue + + td = SATTipoOtroPago.get_by_key(key) + if td is None: + continue + + total_otros_pagos += importe + + new = { + 'tipo_otro_pago': td, + 'importe': importe, + } + if key == '002': + new['subsidio_causado'] = subsidio_causado + data.append(new) + + totals = {'total_otros_pagos': total_otros_pagos} + + return data, totals, '' + + def _validate_horas_extras(self, row): + data = [] + for i, key in enumerate(row[::4]): + days = int(row[i * 4]) + key = row[i * 4 + 1] + the = SATTipoHoras.get_by_key(key) + if the is None: + continue + + hours = int(row[i * 4 + 2]) + importe = round(row[i * 4 + 3], DECIMALES) + if not hours or not importe: + continue + + new = { + 'dias': days, + 'tipos_horas': the, + 'horas_extra': hours, + 'importe_pagado': importe, + } + data.append(new) + + return data, '' + + def _validate_incapacidades(self, row): + data = [] + for i, key in enumerate(row[::3]): + key = row[i * 3] + ti = SATTipoIncapacidad.get_by_key(key) + if ti is None: + continue + + days = int(row[i * 4 + 1]) + importe = round(row[i * 4 + 2], DECIMALES) + if not days or not importe: + continue + + new = { + 'dias': ti, + 'tipo': days, + 'importe': importe, + } + data.append(new) + + return data, '' def _import(self): + util.log_file('nomina', kill=True) emisor = Emisor.select()[0] - rows, msg = util.import_nomina(emisor.rfc) - if not rows: + data, msg = util.import_nomina(emisor.rfc) + if not data: return {'ok': False, 'msg': msg} - for row in rows: - data = self._validate_import(self, row) - # ~ w = (Nomina.empleado.rfc==row['rfc']) + hp = data['percepciones'][0] + percepciones = data['percepciones'][2:] + hd = data['deducciones'][:1] + deducciones = data['deducciones'][2:] + ho = data['otros_pagos'][:1] + otros_pagos = data['otros_pagos'][2:] + horas_extras = data['horas_extras'][2:] + incapacidades = data['incapacidades'][2:] + + for i, row in enumerate(data['nomina']): + row['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + row['regimen_fiscal'] = emisor.regimenes[0].key + row['registro_patronal'] = emisor.registro_patronal + new_nomina, msg = self._validate_nomina(self, row) + if msg: + util.log_file('nomina', msg) + continue + + new_percepciones, total_percepciones, msg = \ + self._validate_percepciones(self, hp, percepciones[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_deducciones, total_deducciones, msg = \ + self._validate_deducciones(self, hd, deducciones[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_otros_pagos, total_otros_pagos, msg = \ + self._validate_otros_pagos(self, ho, otros_pagos[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_horas_extras, msg = self._validate_horas_extras(self, horas_extras[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_incapacidades, msg = self._validate_incapacidades(self, incapacidades[i]) + if msg: + util.log_file('nomina', msg) + continue + + totals = total_percepciones.copy() + totals.update(total_deducciones) + totals.update(total_otros_pagos) + totals['subtotal'] = round(totals['total_percepciones'] + + totals['total_otros_pagos'], DECIMALES) + totals['descuento'] = totals['total_deducciones'] + totals['total'] = round(totals['subtotal'] - + totals['descuento'], DECIMALES) + with database_proxy.transaction(): - pass - # ~ if Empleados.select().where(w).exists(): - # ~ q = Empleados.update(**data).where(w) - # ~ q.execute() - # ~ else: - # ~ obj = Empleados.create(**data) + obj = CfdiNomina.create(**new_nomina) + for row in new_percepciones: + row['cfdi'] = obj + CfdiNominaPercepciones.create(**row) + for row in new_deducciones: + row['cfdi'] = obj + CfdiNominaDeducciones.create(**row) + for row in new_otros_pagos: + row['cfdi'] = obj + CfdiNominaOtroPago.create(**row) + for row in new_horas_extras: + row['cfdi'] = obj + CfdiNominaHorasExtra.create(**row) + for row in new_incapacidades: + row['cfdi'] = obj + CfdiNominaIncapacidad.create(**row) + + concepto = { + 'cfdi': obj, + 'valor_unitario': totals['subtotal'], + 'importe': totals['subtotal'], + 'descuento': totals['total_deducciones'], + } + CfdiNominaDetalle.create(**concepto) + + totals['cfdi'] = obj + CfdiNominaTotales.create(**totals) + + msg = 'Nómina importada correctamente' return {'ok': True, 'msg': msg} def _get(self): @@ -5653,6 +5932,7 @@ class CfdiNominaSeparacion(BaseModel): class CfdiNominaPercepciones(BaseModel): cfdi = ForeignKeyField(CfdiNomina) tipo_percepcion = ForeignKeyField(SATTipoPercepcion) + concepto = TextField(default='') importe_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) importe_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6, @@ -5666,6 +5946,7 @@ class CfdiNominaPercepciones(BaseModel): class CfdiNominaDeducciones(BaseModel): cfdi = ForeignKeyField(CfdiNomina) tipo_deduccion = ForeignKeyField(SATTipoDeduccion) + concepto = TextField(default='') importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) @@ -5673,6 +5954,7 @@ class CfdiNominaDeducciones(BaseModel): class CfdiNominaOtroPago(BaseModel): cfdi = ForeignKeyField(CfdiNomina) tipo_otro_pago = ForeignKeyField(SATTipoOtroPago) + concepto = TextField(default='') importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) subsidio_causado = DecimalField(default=0.0, max_digits=20, decimal_places=6, diff --git a/source/app/settings.py b/source/app/settings.py index 4f2dcb4..487caae 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -137,6 +137,9 @@ USAR_TOKEN = False CANCEL_SIGNATURE = False PUBLIC = 'Público en general' DEFAULT_SAT_NOMINA = { + 'SERIE': 'N', + 'FORMA_PAGO': '99', + 'USO_CFDI': 'P01', 'CLAVE': '84111505', 'UNIDAD': 'ACT', 'DESCRIPCION': 'Pago de nómina',