diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 8b933d9..062b8eb 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -33,9 +33,9 @@ SAT = { 'xmlns': 'http://www.sat.gob.mx/nomina', 'schema': 'http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd', }, - 'nomina12': { + 'nomina': { 'version': '1.2', - 'prefix': 'nomina', + 'prefix': 'nomina12', 'xmlns': 'http://www.sat.gob.mx/nomina12', 'schema': 'http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd', }, @@ -72,6 +72,7 @@ class CFDI(object): self._impuestos_locales = False self._donativo = False self._ine = False + self._is_nomina = False self.error = '' def _now(self): @@ -93,8 +94,6 @@ class CFDI(object): if 'nomina' in datos: self._nomina(datos['nomina']) - #~ if 'complementos' in datos: - #~ self._complementos(datos['complementos']) return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) @@ -108,35 +107,25 @@ class CFDI(object): return xml def _validate(self, datos): - if datos['impuestos']['total_locales_trasladados'] or \ - datos['impuestos']['total_locales_retenciones']: - self._impuestos_locales = True + if datos['impuestos']: + if datos['impuestos']['total_locales_trasladados'] or \ + datos['impuestos']['total_locales_retenciones']: + self._impuestos_locales = True if datos['donativo']: self._donativo = True - if 'ine' in datos['complementos']: - self._ine = True + if datos['complementos']: + if 'ine' in datos['complementos']: + self._ine = True if 'nomina' in datos: + self._is_nomina = True return self._validate_nomina(datos) + return True def _validate_nomina(self, datos): - comprobante = datos['comprobante'] - - validators = ( - ('MetodoDePago', 'NA'), - ('TipoCambio', '1'), - ('Moneda', 'MXN'), - ('TipoDeComprobante', 'egreso'), - ) - for f, v in validators: - if f in comprobante: - if v != comprobante[f]: - msg = 'El atributo: {}, debe ser: {}'.format(f, v) - self.error = msg - return False return True def _comprobante(self, datos): @@ -160,10 +149,16 @@ class CFDI(object): if self._ine: name = 'xmlns:{}'.format(SAT['ine']['prefix']) attributes[name] = SAT['ine']['xmlns'] - schema_donativo = SAT['ine']['schema'] + schema_ine = SAT['ine']['schema'] + + schema_nomina = '' + if self._nomina: + name = 'xmlns:{}'.format(SAT['nomina']['prefix']) + attributes[name] = SAT['nomina']['xmlns'] + schema_nomina = SAT['nomina']['schema'] attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \ - schema_locales + schema_donativo +schema_ine + schema_locales + schema_donativo + schema_ine + schema_nomina attributes.update(datos) if not 'Version' in attributes: @@ -263,6 +258,9 @@ class CFDI(object): return def _impuestos(self, datos): + if self._is_nomina: + return + if not datos: node_name = '{}:Impuestos'.format(self._pre) ET.SubElement(self._cfdi, node_name) @@ -288,38 +286,62 @@ class CFDI(object): return def _nomina(self, datos): - sat_nomina = SAT[NOMINA_ACTUAL] - pre = sat_nomina['prefix'] - complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre)) + pre = SAT['nomina']['prefix'] - emisor = datos.pop('Emisor', None) - receptor = datos.pop('Receptor', None) - percepciones = datos.pop('Percepciones', None) - deducciones = datos.pop('Deducciones', None) + if self._complemento is None: + self._complemento = ET.SubElement( + self._cfdi, '{}:Complemento'.format(self._pre)) - attributes = {} - attributes['xmlns:{}'.format(pre)] = sat_nomina['xmlns'] - attributes['xsi:schemaLocation'] = sat_nomina['schema'] - attributes.update(datos) + emisor = datos.pop('emisor', None) + receptor = datos.pop('receptor', None) + percepciones = datos.pop('percepciones', None) + deducciones = datos.pop('deducciones', None) + otros_pagos = datos.pop('otros_pagos', ()) + incapacidades = datos.pop('incapacidades', ()) - if not 'Version' in attributes: - attributes['Version'] = sat_nomina['version'] + nomina = ET.SubElement( + self._complemento, '{}:Nomina'.format(pre), datos['nomina']) - nomina = ET.SubElement(complemento, '{}:Nomina'.format(pre), attributes) if emisor: ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor) + if receptor: - ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor) + node = ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor) + if percepciones: - detalle = percepciones.pop('detalle', None) - percepciones = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones) - for row in detalle: - ET.SubElement(percepciones, '{}:Percepcion'.format(pre), row) + details = percepciones.pop('details', None) + hours_extra = percepciones.pop('hours_extra', None) + separacion = percepciones.pop('separacion', None) + if details: + node = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones) + for row in details: + nodep = ET.SubElement(node, '{}:Percepcion'.format(pre), row) + if row['TipoPercepcion'] == '019' and hours_extra: + for he in hours_extra: + ET.SubElement(nodep, '{}:HorasExtra'.format(pre), he) + hours_extra = None + if separacion: + ET.SubElement(node, '{}:SeparacionIndemnizacion'.format(pre), separacion) + if deducciones: - detalle = deducciones.pop('detalle', None) - deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones) - for row in detalle: - ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row) + details = deducciones.pop('details', None) + if details: + deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones) + for row in details: + ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row) + + if otros_pagos: + node = ET.SubElement(nomina, '{}:OtrosPagos'.format(pre)) + for row in otros_pagos: + subsidio = row.pop('subsidio', None) + subnode = ET.SubElement(node, '{}:OtroPago'.format(pre), row) + if subsidio: + ET.SubElement(subnode, '{}:SubsidioAlEmpleo'.format(pre), subsidio) + + if incapacidades: + node = ET.SubElement(nomina, '{}:Incapacidades'.format(pre)) + for row in incapacidades: + ET.SubElement(node, '{}:Incapacidad'.format(pre), row) return def _locales(self, datos): diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 946c137..8ebe986 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -441,7 +441,7 @@ class AppNomina(object): def on_delete(self, req, resp): values = req.params - if self._db.delete('nomina', values): + if self._db.delete('nomina', values['id']): resp.status = falcon.HTTP_200 else: resp.status = falcon.HTTP_204 diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py index 31acc20..fd6321d 100644 --- a/source/app/controllers/pac.py +++ b/source/app/controllers/pac.py @@ -174,8 +174,8 @@ class Finkok(object): return def _check_result(self, method, result): - #~ print ('CODE', result.CodEstatus) - #~ print ('INCIDENCIAS', result.Incidencias) + # ~ print ('CODE', result.CodEstatus) + # ~ print ('INCIDENCIAS', result.Incidencias) self.message = '' MSG = { 'OK': 'Comprobante timbrado satisfactoriamente', @@ -184,8 +184,8 @@ class Finkok(object): status = result.CodEstatus if status is None and result.Incidencias: for i in result.Incidencias['Incidencia']: - self.error += 'Error: {}\n{}'.format( - i['CodigoError'], i['MensajeIncidencia']) + self.error += 'Error: {}\n{}\n{}'.format( + i['CodigoError'], i['MensajeIncidencia'], i['ExtraInfo']) return '' if method == 'timbra' and status in (MSG['OK'], MSG['307']): diff --git a/source/app/models/db.py b/source/app/models/db.py index 4d9e420..e0ea97c 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -17,6 +17,11 @@ class StorageEngine(object): def get_nomina(self, values): return main.CfdiNomina.get_by(values) + def nomina(self, values): + opt = values.pop('opt') + if opt == 'cancel': + return main.CfdiNomina.cancel(int(values['id'])) + def empresa_agregar(self, values): return main.empresa_agregar(values['alta_rfc'], False) @@ -104,6 +109,9 @@ class StorageEngine(object): def _get_filteryearsticket(self, values): return main.Tickets.filter_years() + def _get_filteryearsnomina(self, values): + return main.CfdiNomina.filter_years() + def _get_cuentayears(self, values): return main.CuentasBanco.get_years() @@ -252,6 +260,8 @@ class StorageEngine(object): return main.Configuracion.remove(id) if table == 'employee': return main.Empleados.remove(id) + if table == 'nomina': + return main.CfdiNomina.remove(id) return False def _get_client(self, values): diff --git a/source/app/models/main.py b/source/app/models/main.py index 5ee418d..646308f 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -22,6 +22,7 @@ from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \ FORMAT = '{0:.2f}' +FORMAT3 = '{0:.3f}' FORMAT_TAX = '{0:.4f}' @@ -184,6 +185,8 @@ def get_doc(type_doc, id, rfc): data, file_name = PreFacturas.get_pdf(id) elif type_doc == 'tpdf': data, file_name = Tickets.get_pdf(id) + elif type_doc == 'xmlnom': + data, file_name = CfdiNomina.get_xml(id) return data, file_name, content_type @@ -654,7 +657,6 @@ class SATRegimenes(BaseModel): class Emisor(BaseModel): rfc = TextField(unique=True) - curp = TextField(unique=True) nombre = TextField(default='') nombre_comercial = TextField(default='') calle = TextField(default='') @@ -3625,8 +3627,8 @@ class Facturas(BaseModel): comprobante['Serie'] = invoice.serie if invoice.condiciones_pago: comprobante['CondicionesDePago'] = invoice.condiciones_pago - if invoice.descuento: - comprobante['Descuento'] = invoice.descuento + # ~ if invoice.descuento: + # ~ comprobante['Descuento'] = invoice.descuento comprobante['Folio'] = str(invoice.folio) comprobante['Fecha'] = invoice.fecha.isoformat()[:19] @@ -5367,7 +5369,7 @@ class Empleados(BaseModel): es_activo = BooleanField(default=True) es_extranjero = BooleanField(default=False) fecha_alta = DateField(default=util.now) - fecha_ingreso = DateField(default=util.now) + fecha_ingreso = DateField(null=True) imss = TextField(default='') tipo_contrato = ForeignKeyField(SATTipoContrato) es_sindicalizado = BooleanField(default=False) @@ -5399,7 +5401,8 @@ class Empleados(BaseModel): data = row.copy() data['nombre_completo'] = '{} {} {}'.format( row['nombre'], row['paterno'], row['materno']).strip() - data['fecha_ingreso'] = util.calc_to_date(row['fecha_ingreso']) + if row['fecha_ingreso']: + data['fecha_ingreso'] = util.calc_to_date(row['fecha_ingreso']) data['tipo_contrato'] = SATTipoContrato.get_by_key(row['tipo_contrato']) data['es_sindicalizado'] = sn.get(row['es_sindicalizado'].lower(), False) data['tipo_jornada'] = SATTipoJornada.get_by_key(row['tipo_jornada']) @@ -5501,7 +5504,7 @@ class CfdiNomina(BaseModel): max_digits=20, decimal_places=6, auto_round=True, null=True) xml = TextField(default='') uuid = UUIDField(null=True) - estatus = TextField(default='Guardada') + estatus = TextField(default='Guardado') estatus_sat = TextField(default='Vigente') regimen_fiscal = TextField(default='') notas = TextField(default='') @@ -5546,7 +5549,7 @@ class CfdiNomina(BaseModel): if inicio is None: new = 1 else: - new += 1 + new = inicio + 1 if folio > new: new = folio @@ -5797,6 +5800,11 @@ class CfdiNomina(BaseModel): totals['total'] = round(totals['subtotal'] - totals['descuento'], DECIMALES) + new_nomina['subtotal'] = totals['subtotal'] + new_nomina['descuento'] = totals['descuento'] + new_nomina['total'] = totals['total'] + new_nomina['total_mn'] = totals['total'] + with database_proxy.transaction(): obj = CfdiNomina.create(**new_nomina) for row in new_percepciones: @@ -5829,30 +5837,402 @@ class CfdiNomina(BaseModel): msg = 'Nómina importada correctamente' return {'ok': True, 'msg': msg} - def _get(self): - rows = (nomina + def _get(self, where=''): + if not where: + where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) + rows = (CfdiNomina .select( - Nomina.id, - Nomina.serie, - Nomina.folio, - Nomina.fecha, - Nomina.status, - Nomina.fecha_pago, - Nomina.total, - Nomina.empleado.nombre_completo + CfdiNomina.id, + CfdiNomina.serie, + CfdiNomina.folio, + CfdiNomina.fecha, + CfdiNomina.estatus, + CfdiNomina.fecha_pago, + CfdiNomina.total, + Empleados.nombre_completo.alias('empleado') ) + .where(where) + .join(Empleados) + .switch(CfdiNomina) .dicts() ) return {'ok': True, 'rows': tuple(rows)} + def _make_xml(self, cfdi, auth): + emisor = Emisor.select()[0] + empleado = cfdi.empleado + certificado = Certificado.select()[0] + totals = CfdiNominaTotales.select().where(CfdiNominaTotales.cfdi==cfdi)[0] + + comprobante = {} + relacionados = {} + complementos = None + + comprobante['Serie'] = cfdi.serie + comprobante['Folio'] = str(cfdi.folio) + comprobante['Fecha'] = cfdi.fecha.isoformat()[:19] + comprobante['FormaPago'] = cfdi.forma_pago + comprobante['NoCertificado'] = certificado.serie + comprobante['Certificado'] = certificado.cer_txt + comprobante['SubTotal'] = FORMAT.format(cfdi.subtotal) + comprobante['Moneda'] = cfdi.moneda + comprobante['Total'] = FORMAT.format(cfdi.total) + comprobante['TipoDeComprobante'] = cfdi.tipo_comprobante + comprobante['MetodoPago'] = cfdi.metodo_pago + comprobante['LugarExpedicion'] = cfdi.lugar_expedicion + if cfdi.descuento: + comprobante['Descuento'] = FORMAT.format(cfdi.descuento) + + # ~ if invoice.tipo_relacion: + # ~ relacionados = { + # ~ 'tipo': invoice.tipo_relacion, + # ~ 'cfdis': FacturasRelacionadas.get_(invoice), + # ~ } + + cfdi_emisor = { + 'Rfc': emisor.rfc, + 'Nombre': emisor.nombre, + 'RegimenFiscal': cfdi.regimen_fiscal, + } + + receptor = { + 'Rfc': cfdi.empleado.rfc, + 'Nombre': cfdi.empleado.nombre_completo, + 'UsoCFDI': cfdi.uso_cfdi, + } + + conceptos = [] + rows = CfdiNominaDetalle.select().where(CfdiNominaDetalle.cfdi==cfdi) + for row in rows: + concepto = { + 'ClaveProdServ': row.clave_sat, + 'Cantidad': '1', + 'ClaveUnidad': row.clave_unidad, + 'Descripcion': row.descripcion, + 'ValorUnitario': FORMAT.format(row.valor_unitario), + 'Importe': FORMAT.format(row.importe), + } + if row.descuento: + concepto['Descuento'] = FORMAT.format(row.descuento) + + conceptos.append(concepto) + + nomina = { + 'Version': cfdi.version_nomina, + 'TipoNomina': cfdi.tipo_nomina.key, + 'FechaPago': str(cfdi.fecha_pago), + 'FechaInicialPago': str(cfdi.fecha_inicial_pago), + 'FechaFinalPago': str(cfdi.fecha_final_pago), + 'NumDiasPagados': FORMAT3.format(cfdi.dias_pagados), + } + if totals.total_percepciones: + nomina['TotalPercepciones'] = FORMAT.format(totals.total_percepciones) + if totals.total_deducciones: + nomina['TotalDeducciones'] = FORMAT.format(totals.total_deducciones) + if totals.total_otros_pagos: + nomina['TotalOtrosPagos'] = FORMAT.format(totals.total_otros_pagos) + + nomina_emisor = {} + if emisor.curp: + nomina_emisor['Curp'] = emisor.curp + if emisor.registro_patronal: + nomina_emisor['RegistroPatronal'] = emisor.registro_patronal + + nomina_receptor = { + 'Curp': empleado.curp, + 'TipoContrato': empleado.tipo_contrato.key, + 'Sindicalizado': {True: 'Si', False: 'No'}.get(empleado.es_sindicalizado), + 'TipoJornada': empleado.tipo_jornada.key, + 'TipoRegimen': empleado.tipo_regimen.key, + 'NumEmpleado': str(empleado.num_empleado), + 'RiesgoPuesto': empleado.riesgo_puesto.key, + 'PeriodicidadPago': empleado.periodicidad_pago.key, + 'ClaveEntFed': empleado.estado.key, + } + + if empleado.imss: + nomina_receptor['NumSeguridadSocial'] = empleado.imss.replace('-', '') + + if empleado.fecha_ingreso: + nomina_receptor['FechaInicioRelLaboral'] = str(empleado.fecha_ingreso) + days = util.get_days(empleado.fecha_ingreso, cfdi.fecha_final_pago) + weeks = days // 7 + if weeks: + ant = 'P{}W'.format(weeks) + else: + ant = 'P{}D'.format(days) + nomina_receptor['Antigüedad'] = ant + + if empleado.puesto: + if empleado.puesto.departamento: + nomina_receptor['Departamento'] = empleado.puesto.departamento.nombre + nomina_receptor['Puesto'] = empleado.puesto.nombre + + if empleado.clabe: + nomina_receptor['CuentaBancaria'] = empleado.clabe + elif empleado.cuenta_bancaria: + nomina_receptor['CuentaBancaria'] = empleado.cuenta_bancaria + nomina_receptor['Banco'] = empleado.banco.key + + if empleado.salario_base: + nomina_receptor['SalarioBaseCotApor'] = FORMAT.format(empleado.salario_base) + if empleado.salario_diario: + nomina_receptor['SalarioDiarioIntegrado'] = FORMAT.format(empleado.salario_diario) + + percepciones = { + 'TotalSueldos': FORMAT.format(totals.total_sueldos), + 'TotalGravado': FORMAT.format(totals.total_gravado), + 'TotalExento': FORMAT.format(totals.total_exento), + } + if totals.total_separacion: + percepciones['TotalSeparacionIndemnizacion'] = FORMAT.format(totals.total_separacion) + if totals.total_jubilacion: + percepciones['TotalJubilacionPensionRetiro'] = FORMAT.format(totals.total_jubilacion) + + rows = CfdiNominaPercepciones.select().where( + CfdiNominaPercepciones.cfdi==cfdi) + details = [] + for row in rows: + concepto = row.concepto or row.tipo_percepcion.nombre or row.tipo_percepcion.name + p = { + 'TipoPercepcion': row.tipo_percepcion.key, + 'Clave': row.tipo_percepcion.clave or row.tipo_percepcion.key, + 'Concepto': concepto[:100], + 'ImporteGravado': FORMAT.format(row.importe_gravado), + 'ImporteExento': FORMAT.format(row.importe_exento), + } + details.append(p) + percepciones['details'] = details + + rows = CfdiNominaHorasExtra.select().where(CfdiNominaHorasExtra.cfdi==cfdi) + details = [] + for row in rows: + n = { + 'Dias': str(row.dias), + 'TipoHoras': row.tipos_horas.key, + 'HorasExtra': str(row.horas_extra), + 'ImportePagado': FORMAT.format(row.importe_pagado), + } + details.append(n) + percepciones['hours_extra'] = details + + deducciones = { + 'TotalOtrasDeducciones': FORMAT.format(totals.total_otras_deducciones), + 'TotalImpuestosRetenidos': FORMAT.format(totals.total_retenciones), + } + rows = CfdiNominaDeducciones.select().where(CfdiNominaDeducciones.cfdi==cfdi) + details = [] + for row in rows: + concepto = row.concepto or row.tipo_deduccion.nombre or row.tipo_deduccion.name + p = { + 'TipoDeduccion': row.tipo_deduccion.key, + 'Clave': row.tipo_deduccion.clave or row.tipo_deduccion.key, + 'Concepto': concepto[:100], + 'Importe': FORMAT.format(row.importe), + } + details.append(p) + deducciones['details'] = details + + rows = CfdiNominaOtroPago.select().where(CfdiNominaOtroPago.cfdi==cfdi) + otros_pagos = [] + for row in rows: + concepto = row.concepto or row.tipo_otro_pago.nombre or row.tipo_otro_pago.name + p = { + 'TipoOtroPago': row.tipo_otro_pago.key, + 'Clave': row.tipo_otro_pago.clave or row.tipo_otro_pago.key, + 'Concepto': concepto[:100], + 'Importe': FORMAT.format(row.importe), + } + if row.tipo_otro_pago.key == '002' and row.subsidio_causado: + p['subsidio'] = { + 'SubsidioCausado': FORMAT.format(row.subsidio_causado) + } + otros_pagos.append(p) + + rows = CfdiNominaIncapacidad.select().where(CfdiNominaIncapacidad.cfdi==cfdi) + incapacidades = [] + for row in rows: + n = { + 'DiasIncapacidad': str(row.dias), + 'TipoIncapacidad': row.tipo.key, + 'ImporteMonetario': FORMAT.format(row.importe), + } + incapacidades.append(n) + + nomina = { + 'nomina': nomina, + 'emisor': nomina_emisor, + 'receptor': nomina_receptor, + 'percepciones': percepciones, + 'deducciones': deducciones, + 'otros_pagos': otros_pagos, + 'incapacidades': incapacidades, + } + + data = { + 'comprobante': comprobante, + 'relacionados': relacionados, + 'emisor': cfdi_emisor, + 'receptor': receptor, + 'conceptos': conceptos, + 'complementos': complementos, + 'nomina': nomina, + 'impuestos': {}, + 'donativo': {}, + } + return util.make_xml(data, certificado, auth) + + def _stamp_id(self, id): + auth = Emisor.get_auth() + obj = CfdiNomina.get(CfdiNomina.id==id) + obj.xml = self._make_xml(self, obj, auth) + obj.estatus = 'Generado' + obj.save() + + result = util.timbra_xml(obj.xml, auth) + # ~ print (result) + if result['ok']: + obj.xml = result['xml'] + obj.uuid = result['uuid'] + obj.fecha_timbrado = result['fecha'] + obj.estatus = 'Timbrado' + obj.error = '' + obj.save() + # ~ cls._sync(cls, id, auth) + else: + msg = result['error'] + obj.estatus = 'Error' + obj.error = msg + obj.save() + + + return result['ok'], obj.error + + def _stamp(self): + where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) + rows = CfdiNomina.select().where(where) + util.log_file('nomina', kill=True) + msg_error = '' + ok_stamp = 0 + for row in rows: + msg = 'Timbrando el recibo: {}-{}'.format(row.serie, row.folio) + util.log_file('nomina', msg) + result, msg = self._stamp_id(self, row.id) + if result: + msg = 'Recibo: {}-{}, timbrado correctamente'.format(row.serie, row.folio) + ok_stamp += 1 + else: + msg_error = msg + break + + ok = False + if ok_stamp: + msg = 'Se timbraron {} recibos'.format(ok_stamp) + ok = True + error = False + if msg_error: + error = True + + return {'ok': ok, 'msg_ok': msg, 'error': error, 'msg_error': msg_error} + @classmethod def get_by(cls, values): - if not 'opt' in values: + if not values: return cls._get(cls) + if values['opt'] == 'dates': + dates = util.loads(values['range']) + filters = CfdiNomina.fecha.between( + util.get_date(dates['start']), + util.get_date(dates['end'], True) + ) + return cls._get(cls, filters) + + if values['opt'] == 'yearmonth': + if values['year'] == '-1': + fy = (CfdiNomina.fecha.year > 0) + else: + fy = (CfdiNomina.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (CfdiNomina.fecha.month > 0) + else: + fm = (CfdiNomina.fecha.month == int(values['month'])) + filters = (fy & fm) + return cls._get(cls, filters) + if values['opt'] == 'import': return cls._import(cls) + if values['opt'] == 'stamp': + return cls._stamp(cls) + + @classmethod + def remove(cls, id): + obj = CfdiNomina.get(CfdiNomina.id==id) + if obj.uuid: + return False + + q = CfdiNominaDetalle.delete().where(CfdiNominaDetalle.cfdi==obj) + q.execute() + q = CfdiNominaTotales.delete().where(CfdiNominaTotales.cfdi==obj) + q.execute() + q = CfdiNominaPercepciones.delete().where(CfdiNominaPercepciones.cfdi==obj) + q.execute() + q = CfdiNominaDeducciones.delete().where(CfdiNominaDeducciones.cfdi==obj) + q.execute() + q = CfdiNominaOtroPago.delete().where(CfdiNominaOtroPago.cfdi==obj) + q.execute() + q = CfdiNominaHorasExtra.delete().where(CfdiNominaHorasExtra.cfdi==obj) + q.execute() + q = CfdiNominaIncapacidad.delete().where(CfdiNominaIncapacidad.cfdi==obj) + q.execute() + + return bool(obj.delete_instance()) + + @classmethod + def get_xml(cls, id): + obj = CfdiNomina.get(CfdiNomina.id==id) + name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.empleado.rfc) + # ~ cls._sync_xml(cls, obj) + return obj.xml, name + + @classmethod + def filter_years(cls): + data = [{'id': -1, 'value': 'Todos'}] + rows = (CfdiNomina + .select(CfdiNomina.fecha.year.alias('year')) + .group_by(CfdiNomina.fecha.year) + .order_by(CfdiNomina.fecha.year) + ) + if not rows is None: + data += [{'id': int(r.year), 'value': int(r.year)} for r in rows] + return tuple(data) + + @classmethod + def cancel(cls, id): + msg = 'Recibo cancelado correctamente' + auth = Emisor.get_auth() + certificado = Certificado.select()[0] + obj = CfdiNomina.get(CfdiNomina.id==id) + + if obj.uuid is None: + msg = 'Solo se pueden cancelar recibos timbrados' + return {'ok': False, 'msg': msg} + + data, result = util.cancel_xml(auth, obj.uuid, certificado) + if data['ok']: + data['msg'] = 'Recibo cancelado correctamente' + data['row']['estatus'] = 'Cancelado' + obj.estatus = data['row']['estatus'] + obj.error = '' + obj.cancelada = True + obj.fecha_cancelacion = result['Fecha'] + obj.acuse = result['Acuse'] + else: + obj.error = data['msg'] + obj.save() + return data + class CfdiNominaDetalle(BaseModel): cfdi = ForeignKeyField(CfdiNomina) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index c09668a..2416f75 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -9,13 +9,22 @@ var nomina_controllers = { $$('cmd_close_empleados').attachEvent('onItemClick', cmd_close_empleados_click) $$('cmd_delete_empleado').attachEvent('onItemClick', cmd_delete_empleado_click) $$('cmd_import_empleados').attachEvent('onItemClick', cmd_import_empleados_click) + $$('cmd_nomina_without_stamp').attachEvent('onItemClick', cmd_nomina_without_stamp_click) + $$('cmd_nomina_delete').attachEvent('onItemClick', cmd_nomina_delete_click) + $$('cmd_nomina_timbrar').attachEvent('onItemClick', cmd_nomina_timbrar_click) + $$('cmd_nomina_cancel').attachEvent('onItemClick', cmd_nomina_cancel_click) + $$('grid_nomina').attachEvent('onItemClick', grid_nomina_click) + $$('filter_year_nomina').attachEvent('onChange', filter_year_nomina_change) + $$('filter_month_nomina').attachEvent('onChange', filter_month_nomina_change) + $$('filter_dates_nomina').attachEvent('onChange', filter_dates_nomina_change) webix.extend($$('grid_nomina'), webix.ProgressBar) } } function default_config_nomina(){ - + current_dates_nomina() + get_nomina() } @@ -39,8 +48,26 @@ function current_dates_nomina(){ } -function get_nomina(){ +function get_nomina(filters){ + var grid = $$('grid_nomina') + grid.showProgress({type: 'icon'}) + + webix.ajax().get('/nomina', filters, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + grid.clearAll(); + grid.parse(values.rows, 'json'); + }else{ + msg_error(values.msg) + } + } + }) } @@ -259,4 +286,186 @@ function cmd_delete_empleado_click(){ } } }) +} + + +function cmd_nomina_without_stamp_click(){ + get_nomina() +} + + +function cmd_nomina_delete_click(){ + var row = $$('grid_nomina').getSelectedItem() + + if (row == undefined){ + msg = 'Selecciona un registro' + msg_error(msg) + return + } + + msg = '¿Estás seguro de eliminar el registro?

' + msg += row['empleado'] + ' (' + row['fecha_pago'] + ')' + msg += '

ESTA ACCIÓN NO SE PUEDE DESHACER

' + webix.confirm({ + title: 'Eliminar Nomina', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if (result){ + delete_nomina(row['id']) + } + } + }) +} + + +function delete_nomina(id){ + webix.ajax().del('/nomina', {id: id}, function(text, xml, xhr){ + var msg = 'Registro eliminado correctamente' + if (xhr.status == 200){ + $$('grid_nomina').remove(id); + msg_ok(msg) + } else { + msg = 'No se pudo eliminar.' + msg_error(msg) + } + }) +} + + +function cmd_nomina_timbrar_click(){ + get_nomina() + + msg = 'Se enviarán a timbrar todos los recibos sin timbrar

' + msg += '¿Estás seguro de continuar?

' + webix.confirm({ + title: 'Enviar a timbrar', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if (result){ + timbrar_nomina() + } + } + }) +} + + +function timbrar_nomina(){ + webix.ajax().get('/nomina', {opt: 'stamp'}, { + error: function(text, data, xhr) { + msg = 'Error al timbrar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if(values.ok){ + get_nomina() + msg_ok(values.msg_ok) + } + if(values.error){ + webix.alert({ + title: 'Error al Timbrar', + text: values.msg_error, + type: 'alert-error' + }) + } + } + }) +} + + +function grid_nomina_click(id, e, node){ + var row = this.getItem(id) + + if(id.column == 'xml'){ + location = '/doc/xmlnom/' + row.id + }else if(id.column == 'pdf'){ + //~ get_momina_pdf(row.id) + //~ }else if(id.column == 'email'){ + //~ enviar_correo(row) + } + +} + + +function filter_year_nomina_change(nv, ov){ + var fm = $$('filter_month_nomina') + filters = {'opt': 'yearmonth', 'year': nv, 'month': fm.getValue()} + get_nomina(filters) +} + + +function filter_month_nomina_change(nv, ov){ + var fy = $$('filter_year_nomina') + filters = {'opt': 'yearmonth', 'year': fy.getValue(), 'month': nv} + get_nomina(filters) +} + + +function filter_dates_nomina_change(range){ + if(range.start != null && range.end != null){ + filters = {'opt': 'dates', 'range': range} + get_nomina(filters) + } +} + + +function cmd_nomina_cancel_click(){ + var row = $$('grid_nomina').getSelectedItem() + + if(row == undefined){ + msg = 'Selecciona un registro' + msg_error(msg) + return + } + if(row['estatus'] != 'Timbrado'){ + msg = 'Solo se pueden cancelar recibos timbrados' + msg_error(msg) + return + } + + msg = '¿Estás seguro de cancelar el recibo?

' + msg += row['empleado'] + ' (' + row['serie'] + '-' + row['folio'] + ')' + msg += '

ESTA ACCIÓN NO SE PUEDE DESHACER

' + webix.confirm({ + title: 'Cancelar Nomina', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if (result){ + cancel_nomina(row['id']) + } + } + }) +} + + +function cancel_nomina(id){ + var grid = $$('grid_nomina') + var data = new Object() + data['opt'] = 'cancel' + data['id'] = id + + webix.ajax().sync().post('nomina', data, { + error:function(text, data, XmlHttpRequest){ + msg = 'Ocurrio un error, consulta a soporte técnico' + msg_error(msg) + }, + success:function(text, data, XmlHttpRequest){ + values = data.json(); + if(values.ok){ + grid.updateItem(id, values.row) + msg_ok(values.msg) + }else{ + msg_error(values.msg) + } + } + }) } \ No newline at end of file diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js index 5cbe1a2..9dca50f 100644 --- a/source/static/js/ui/nomina.js +++ b/source/static/js/ui/nomina.js @@ -31,6 +31,9 @@ var toolbar_nomina_filter = [ labelAlign: 'right', labelWidth: 50, width: 200, options: months}, {view: 'daterangepicker', id: 'filter_dates_nomina', label: 'Fechas', labelAlign: 'right', width: 300}, + {}, + {view: 'button', id: 'cmd_nomina_without_stamp', label: 'Sin Timbrar', + type: 'iconButton', autowidth: true, icon: 'filter'}, ] @@ -44,12 +47,14 @@ var grid_cols_nomina = [ {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort: "string"}, {id: "estatus", header: ["Estatus", {content: "selectFilter"}], adjust: "data", sort:"string"}, - {id: "fecha_pago", header: ["Fecha de Pago"], adjust: "data", sort: "string"}, + {id: 'fecha_pago', header: ['Fecha de Pago', {content: 'selectFilter'}], + adjust: 'data', sort: 'string'}, {id: 'total', header: ['Total', {content: 'numberFilter'}], width: 150, sort: 'int', format: webix.i18n.priceFormat, css: 'right', footer: {content: 'summActive', css: 'right'}}, {id: "empleado", header: ["Empleado", {content: "selectFilter"}], fillspace:true, sort:"string"}, + {id: 'xml', header: 'XML', adjust: 'data', template: get_icon('xml')}, {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')}, ]