diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 445ef34..724ced1 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', }, @@ -79,6 +79,7 @@ class CFDI(object): self._donativo = False self._ine = False self._edu = False + self._is_nomina = False self.error = '' def _now(self): @@ -115,37 +116,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 - - self._edu = datos['edu'] + 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): @@ -175,10 +164,16 @@ class CFDI(object): if self._edu: name = 'xmlns:{}'.format(SAT['edu']['prefix']) attributes[name] = SAT['edu']['xmlns'] - schema_edu = SAT['edu']['schema'] + schema_edu = SAT['edu']['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_edu + schema_locales + schema_donativo + schema_ine + + schema_edu + schema_nomina attributes.update(datos) if not 'Version' in attributes: @@ -269,6 +264,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) @@ -294,38 +292,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/cfdi_xml.py.orig b/source/app/controllers/cfdi_xml.py.orig new file mode 100644 index 0000000..b88cff8 --- /dev/null +++ b/source/app/controllers/cfdi_xml.py.orig @@ -0,0 +1,470 @@ +#!/usr/bin/env python + +import datetime +from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString + +from logbook import Logger + +#~ from settings import DEBUG + + +log = Logger('XML') +CFDI_ACTUAL = 'cfdi33' +NOMINA_ACTUAL = 'nomina12' + +SAT = { + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'cfdi32': { + 'version': '3.2', + 'prefix': 'cfdi', + 'xmlns': 'http://www.sat.gob.mx/cfd/3', + 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd', + }, + 'cfdi33': { + 'version': '3.3', + 'prefix': 'cfdi', + 'xmlns': 'http://www.sat.gob.mx/cfd/3', + 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd', + }, + 'nomina11': { + 'version': '1.1', + 'prefix': 'nomina', + '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', + }, + 'nomina': { + 'version': '1.2', + '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', + }, + 'locales': { + 'version': '1.0', + 'prefix': 'implocal', + 'xmlns': 'http://www.sat.gob.mx/implocal', + 'schema': ' http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd', + }, + 'donativo': { + 'version': '1.1', + 'prefix': 'donat', + 'xmlns': 'http://www.sat.gob.mx/donat', + 'schema': ' http://www.sat.gob.mx/donat http://www.sat.gob.mx/sitio_internet/cfd/donat/donat11.xsd', + 'leyenda': 'Este comprobante ampara un donativo, el cual será destinado por la donataria a los fines propios de su objeto social. En el caso de que los bienes donados hayan sido deducidos previamente para los efectos del impuesto sobre la renta, este donativo no es deducible. La reproducción no autorizada de este comprobante constituye un delito en los términos de las disposiciones fiscales.', + }, + 'ine': { + 'version': '1.1', + 'prefix': 'ine', + 'xmlns': 'http://www.sat.gob.mx/ine', + 'schema': ' http://www.sat.gob.mx/ine http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd', + }, + 'edu': { + 'version': '1.0', + 'prefix': 'iedu', + 'xmlns': 'http://www.sat.gob.mx/iedu', + 'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/ine/iedu.xsd', + }, +} + + +class CFDI(object): + + def __init__(self, version=CFDI_ACTUAL): + self._sat_cfdi = SAT[version] + self._xsi = SAT['xsi'] + self._pre = self._sat_cfdi['prefix'] + self._cfdi = None + self._complemento = None + self._impuestos_locales = False + self._donativo = False + self._ine = False +<<<<<<< HEAD + self._edu = False +======= + self._is_nomina = False +>>>>>>> nomina + self.error = '' + + def _now(self): + return datetime.datetime.now().isoformat()[:19] + + def get_xml(self, datos): + if not self._validate(datos): + return '' + + self._comprobante(datos['comprobante']) + self._relacionados(datos['relacionados']) + self._emisor(datos['emisor']) + self._receptor(datos['receptor']) + self._conceptos(datos['conceptos']) + self._impuestos(datos['impuestos']) + self._locales(datos['impuestos']) + self._donatarias(datos['donativo']) + self._complementos(datos['complementos']) + + if 'nomina' in datos: + self._nomina(datos['nomina']) + + return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) + + def add_sello(self, sello): + self._cfdi.attrib['Sello'] = sello + return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) + + def _to_pretty_xml(self, source): + tree = parseString(source) + xml = tree.toprettyxml(encoding='utf-8').decode('utf-8') + return xml + + def _validate(self, datos): + 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 datos['complementos']: + if 'ine' in datos['complementos']: + self._ine = True + + self._edu = datos['edu'] + + if 'nomina' in datos: + self._is_nomina = True + return self._validate_nomina(datos) + + return True + + def _validate_nomina(self, datos): + return True + + def _comprobante(self, datos): + attributes = {} + attributes['xmlns:{}'.format(self._pre)] = self._sat_cfdi['xmlns'] + attributes['xmlns:xsi'] = self._xsi + + schema_locales = '' + if self._impuestos_locales: + name = 'xmlns:{}'.format(SAT['locales']['prefix']) + attributes[name] = SAT['locales']['xmlns'] + schema_locales = SAT['locales']['schema'] + + schema_donativo = '' + if self._donativo: + name = 'xmlns:{}'.format(SAT['donativo']['prefix']) + attributes[name] = SAT['donativo']['xmlns'] + schema_donativo = SAT['donativo']['schema'] + + schema_ine = '' + if self._ine: + name = 'xmlns:{}'.format(SAT['ine']['prefix']) + attributes[name] = SAT['ine']['xmlns'] + schema_ine = SAT['ine']['schema'] + +<<<<<<< HEAD + schema_edu = '' + if self._edu: + name = 'xmlns:{}'.format(SAT['edu']['prefix']) + attributes[name] = SAT['edu']['xmlns'] + schema_edu = SAT['edu']['schema'] + + attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \ + schema_locales + schema_donativo + schema_ine + schema_edu +======= + 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_nomina +>>>>>>> nomina + attributes.update(datos) + + if not 'Version' in attributes: + attributes['Version'] = self._sat_cfdi['version'] + if not 'Fecha' in attributes: + attributes['Fecha'] = self._now() + + self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes) + return + + def _relacionados(self, datos): + if not datos or not datos['tipo'] or not datos['cfdis']: + return + + node_name = '{}:CfdiRelacionados'.format(self._pre) + value = {'TipoRelacion': datos['tipo']} + node = ET.SubElement(self._cfdi, node_name, value) + for uuid in datos['cfdis']: + node_name = '{}:CfdiRelacionado'.format(self._pre) + value = {'UUID': uuid} + ET.SubElement(node, node_name, value) + return + + def _emisor(self, datos): + node_name = '{}:Emisor'.format(self._pre) + emisor = ET.SubElement(self._cfdi, node_name, datos) + return + + def _receptor(self, datos): + node_name = '{}:Receptor'.format(self._pre) + emisor = ET.SubElement(self._cfdi, node_name, datos) + return + + def _conceptos(self, datos): + from xml.sax.saxutils import escape, unescape + + conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre)) + for row in reversed(datos): + # ~ print (row['Descripcion']) + # ~ xml = escape(xml.encode('ascii', 'xmlcharrefreplace').decode('utf-8'), False) + # ~ row['Descripcion'] = escape(row['Descripcion'].replace('\n', ' '), False) + # ~ row['Descripcion'] = row['Descripcion'].replace('\n', ' ') + # ~ print (row['Descripcion']) + + complemento = {} + if 'complemento' in row: + complemento = row.pop('complemento') + cuenta_predial = row.pop('CuentaPredial', '') + pedimento = row.pop('Pedimento', '') + student = row.pop('student', '') + + taxes = {} + if 'impuestos' in row: + taxes = row.pop('impuestos') + node_name = '{}:Concepto'.format(self._pre) + concepto = ET.SubElement(conceptos, node_name, row) + + if taxes: + node_name = '{}:Impuestos'.format(self._pre) + impuestos = ET.SubElement(concepto, node_name) + if 'traslados' in taxes and taxes['traslados']: + node_name = '{}:Traslados'.format(self._pre) + traslados = ET.SubElement(impuestos, node_name) + for traslado in taxes['traslados']: + ET.SubElement( + traslados, '{}:Traslado'.format(self._pre), traslado) + if 'retenciones' in taxes and taxes['retenciones']: + node_name = '{}:Retenciones'.format(self._pre) + retenciones = ET.SubElement(impuestos, node_name) + for retencion in taxes['retenciones']: + ET.SubElement( + retenciones, '{}:Retencion'.format(self._pre), retencion) + + if pedimento: + attributes = {'NumeroPedimento': pedimento} + node_name = '{}:InformacionAduanera'.format(self._pre) + ET.SubElement(concepto, node_name, attributes) + + if cuenta_predial: + attributes = {'Numero': cuenta_predial} + node_name = '{}:CuentaPredial'.format(self._pre) + ET.SubElement(concepto, node_name, attributes) + + if student: + node_name = '{}:ComplementoConcepto'.format(self._pre) + complemento = ET.SubElement(concepto, node_name) + ET.SubElement(complemento, 'iedu:instEducativas', student) + 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) + return + + attributes = {} + fields = ('TotalImpuestosTrasladados', 'TotalImpuestosRetenidos') + for field in fields: + if field in datos: + attributes[field] = datos[field] + node_name = '{}:Impuestos'.format(self._pre) + impuestos = ET.SubElement(self._cfdi, node_name, attributes) + + if 'retenciones' in datos and datos['retenciones']: + retenciones = ET.SubElement(impuestos, '{}:Retenciones'.format(self._pre)) + for row in datos['retenciones']: + ET.SubElement(retenciones, '{}:Retencion'.format(self._pre), row) + + if 'traslados' in datos and datos['traslados']: + traslados = ET.SubElement(impuestos, '{}:Traslados'.format(self._pre)) + for row in datos['traslados']: + ET.SubElement(traslados, '{}:Traslado'.format(self._pre), row) + return + + def _nomina(self, datos): + pre = SAT['nomina']['prefix'] + + if self._complemento is None: + self._complemento = ET.SubElement( + self._cfdi, '{}:Complemento'.format(self._pre)) + + 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', ()) + + nomina = ET.SubElement( + self._complemento, '{}:Nomina'.format(pre), datos['nomina']) + + if emisor: + ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor) + + if receptor: + node = ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor) + + if percepciones: + 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: + 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): + if not self._impuestos_locales: + return + + if self._complemento is None: + self._complemento = ET.SubElement( + self._cfdi, '{}:Complemento'.format(self._pre)) + + attributes = {} + attributes['version'] = SAT['locales']['version'] + if not datos['total_locales_trasladados']: + datos['total_locales_trasladados'] = '0.00' + attributes['TotaldeTraslados'] = datos['total_locales_trasladados'] + if not datos['total_locales_retenciones']: + datos['total_locales_retenciones'] = '0.00' + attributes['TotaldeRetenciones'] = datos['total_locales_retenciones'] + + node = ET.SubElement( + self._complemento, 'implocal:ImpuestosLocales', attributes) + + for retencion in datos['locales_retenciones']: + ET.SubElement(node, 'implocal:RetencionesLocales', retencion) + for traslado in datos['locales_trasladados']: + ET.SubElement(node, 'implocal:TrasladosLocales', traslado) + return + + def _donatarias(self, datos): + if not datos: + return + + if self._complemento is None: + self._complemento = ET.SubElement( + self._cfdi, '{}:Complemento'.format(self._pre)) + + attributes = {} + attributes['version'] = SAT['donativo']['version'] + attributes['leyenda'] = SAT['donativo']['leyenda'] + attributes.update(datos) + + node = ET.SubElement(self._complemento, 'donat:Donatarias', attributes) + + return + + def _complementos(self, datos): + if not datos: + return + + if self._complemento is None: + self._complemento = ET.SubElement( + self._cfdi, '{}:Complemento'.format(self._pre)) + + if 'ine' in datos: + atributos = {'Version': SAT['ine']['version']} + atributos.update(datos['ine']) + ET.SubElement(self._complemento, 'ine:INE', atributos) + + if 'ce' in datos: + pre = 'cce11' + datos = datos.pop('ce') + emisor = datos.pop('emisor') + propietario = datos.pop('propietario') + receptor = datos.pop('receptor') + destinatario = datos.pop('destinatario') + conceptos = datos.pop('conceptos') + + attributes = {} + attributes['xmlns:{}'.format(pre)] = \ + 'http://www.sat.gob.mx/ComercioExterior11' + attributes['xsi:schemaLocation'] = \ + 'http://www.sat.gob.mx/ComercioExterior11 ' \ + 'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd' + attributes.update(datos) + ce = ET.SubElement( + complemento, '{}:ComercioExterior'.format(pre), attributes) + + attributes = {} + if 'Curp' in emisor: + attributes = {'Curp': emisor.pop('Curp')} + node = ET.SubElement(ce, '{}:Emisor'.format(pre), attributes) + ET.SubElement(node, '{}:Domicilio'.format(pre), emisor) + + if propietario: + ET.SubElement(ce, '{}:Propietario'.format(pre), propietario) + + attributes = {} + if 'NumRegIdTrib' in receptor: + attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')} + node = ET.SubElement(ce, '{}:Receptor'.format(pre), attributes) + ET.SubElement(node, '{}:Domicilio'.format(pre), receptor) + + attributes = {} + if 'NumRegIdTrib' in destinatario: + attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')} + if 'Nombre' in destinatario: + attributes.update({'Nombre': destinatario.pop('Nombre')}) + node = ET.SubElement(ce, '{}:Destinatario'.format(pre), attributes) + ET.SubElement(node, '{}:Domicilio'.format(pre), destinatario) + + node = ET.SubElement(ce, '{}:Mercancias'.format(pre)) + fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie') + for row in conceptos: + detalle = {} + for f in fields: + if f in row: + detalle[f] = row.pop(f) + concepto = ET.SubElement(node, '{}:Mercancia'.format(pre), row) + if detalle: + ET.SubElement( + concepto, '{}:DescripcionesEspecificas'.format(pre), detalle) + return diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 59cb9e1..6436f47 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -426,6 +426,52 @@ class AppFolios(object): resp.status = falcon.HTTP_204 +class AppEmployees(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_employees(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.employees(values) + resp.status = falcon.HTTP_200 + + def on_delete(self, req, resp): + values = req.params + if self._db.delete('employee', values['id']): + resp.status = falcon.HTTP_200 + else: + resp.status = falcon.HTTP_204 + + +class AppNomina(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_nomina(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.nomina(values) + resp.status = falcon.HTTP_200 + + def on_delete(self, req, resp): + values = req.params + if self._db.delete('nomina', values['id']): + resp.status = falcon.HTTP_200 + else: + resp.status = falcon.HTTP_204 + + class AppDocumentos(object): def __init__(self, db): 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/controllers/util.py b/source/app/controllers/util.py index 1364728..45474d6 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -260,6 +260,14 @@ def get_file(path): return open(path, 'rb') +def get_files(path, ext='xml'): + docs = [] + for folder, _, files in os.walk(path): + pattern = re.compile('\.{}'.format(ext), re.IGNORECASE) + docs += [os.path.join(folder,f) for f in files if pattern.search(f)] + return tuple(docs) + + def read_file(path, mode='rb'): return open(path, mode).read() @@ -308,6 +316,10 @@ def loads(data): return json.loads(data) +def import_json(path): + return loads(read_file(path, 'r')) + + def clean(values): for k, v in values.items(): if isinstance(v, str): @@ -748,8 +760,29 @@ class LIBO(object): self._sheet.copyRange(nc.getCellAddress(), source.getRangeAddress()) return + def _copy_paste_rows(self, cell, count): + dispatch = self._create_instance('com.sun.star.frame.DispatchHelper') + + row = cell.getCellAddress().Row + source = self._sheet.getRows().getByIndex(row) + self._template.getCurrentController().select(source) + frame = self._template.getCurrentController().getFrame() + dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) + + target = self._sheet.getCellRangeByPosition(0, row + 1, 0, row + count) + self._template.getCurrentController().select(target) + dispatch.executeDispatch(frame, '.uno:Paste', '', 0, ()) + return + def _conceptos(self, data): first = True + col1 = [] + col2 = [] + col3 = [] + col4 = [] + col5 = [] + col6 = [] + count = len(data)-1 for concepto in data: key = concepto.get('noidentificacion', '') description = concepto['descripcion'] @@ -767,16 +800,36 @@ class LIBO(object): cell_6 = self._set_cell('{importe}', importe, value=True) if len(data) > 1: row = cell_1.getCellAddress().Row + 1 - self._sheet.getRows().insertByIndex(row, len(data)-1) + self._sheet.getRows().insertByIndex(row, count) + self._copy_paste_rows(cell_1, count) + row = cell_1.getCellAddress().Row else: - self._copy_row(cell_1) - cell_1 = self._set_cell(v=key, cell=cell_1) - cell_2 = self._set_cell(v=description, cell=cell_2) - cell_3 = self._set_cell(v=unidad, cell=cell_3) - cell_4 = self._set_cell(v=cantidad, cell=cell_4, value=True) - cell_5 = self._set_cell(v=valor_unitario, cell=cell_5, value=True) - cell_6 = self._set_cell(v=importe, cell=cell_6, value=True) + col1.append((key,)) + col2.append((description,)) + col3.append((unidad,)) + col4.append((float(cantidad),)) + col5.append((float(valor_unitario),)) + col6.append((float(importe),)) + col = cell_1.getCellAddress().Column + target1 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_2.getCellAddress().Column + target2 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_3.getCellAddress().Column + target3 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_4.getCellAddress().Column + target4 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_5.getCellAddress().Column + target5 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_6.getCellAddress().Column + target6 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + + target1.setFormulaArray(tuple(col1)) + target2.setDataArray(tuple(col2)) + target3.setFormulaArray(tuple(col3)) + target4.setDataArray(tuple(col4)) + target5.setDataArray(tuple(col5)) + target6.setDataArray(tuple(col6)) return def _add_totales(self, data): @@ -957,6 +1010,171 @@ class LIBO(object): rows = [dict(zip(fields, r)) for r in data[1:]] return rows, '' + def employees(self, path): + options = {'AsTemplate': True, 'Hidden': True} + doc = self._doc_open(path, options) + if doc is None: + return () + + data, msg = self._get_data(doc, 'Empleados') + doc.close(True) + + if len(data) == 1: + msg = 'Sin datos para importar' + return (), msg + + fields = ( + 'num_empleado', + 'rfc', + 'curp', + 'nombre', + 'paterno', + 'materno', + 'fecha_ingreso', + 'imss', + 'tipo_contrato', + 'es_sindicalizado', + 'tipo_jornada', + 'tipo_regimen', + 'departamento', + 'puesto', + 'riesgo_puesto', + 'periodicidad_pago', + 'banco', + 'cuenta_bancaria', + 'clabe', + 'salario_base', + 'salario_diario', + 'estado', + 'codigo_postal', + 'notas', + 'correo', + ) + rows = tuple([dict(zip(fields, r)) for r in data[1:]]) + 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: + 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 + + 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 + + return data, '' + def invoice(self, path): options = {'AsTemplate': True, 'Hidden': True} doc = self._doc_open(path, options) @@ -1002,6 +1220,34 @@ def to_pdf(data, emisor_rfc, ods=False): return read_file(path) +def import_employees(rfc): + name = '{}_employees.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return () + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.employees(path) + + return () + + +def import_nomina(rfc): + name = '{}_nomina.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return () + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.nomina(path) + + return () + + def parse_xml(xml): return ET.fromstring(xml) @@ -1432,6 +1678,24 @@ def upload_file(rfc, opt, file_obj): name = '{}_invoice.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'employees': + tmp = file_obj.filename.split('.') + ext = tmp[-1].lower() + if ext != 'ods': + msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS' + return {'status': 'server', 'name': msg, 'ok': False} + + name = '{}_employees.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'nomina': + tmp = file_obj.filename.split('.') + ext = tmp[-1].lower() + if ext != 'ods': + msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS' + return {'status': 'server', 'name': msg, 'ok': False} + + name = '{}_nomina.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} @@ -2686,3 +2950,32 @@ def import_invoice(rfc): return (), 'No se encontro LibreOffice' + +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 = '{} : {}\n'.format(str(now()), msg) + fh.write(line) + return + + +def get_log(name): + data = '' + name = '{}.log'.format(name) + path = _join(PATH_MEDIA, 'tmp', name) + if is_file(path): + data = open(path).read() + return data, name diff --git a/source/app/main.py b/source/app/main.py index 21c96ee..42127ee 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -16,7 +16,7 @@ from controllers.main import (AppEmpresas, AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco, - AppMovimientosBanco, AppTickets, AppStudents + AppMovimientosBanco, AppTickets, AppStudents, AppEmployees, AppNomina ) @@ -53,6 +53,8 @@ api.add_route('/tickets', AppTickets(db)) api.add_route('/cuentasbanco', AppCuentasBanco(db)) api.add_route('/movbanco', AppMovimientosBanco(db)) api.add_route('/students', AppStudents(db)) +api.add_route('/employees', AppEmployees(db)) +api.add_route('/nomina', AppNomina(db)) # ~ Activa si usas waitress y NO estas usando servidor web diff --git a/source/app/main.py.orig b/source/app/main.py.orig new file mode 100644 index 0000000..ed48fbc --- /dev/null +++ b/source/app/main.py.orig @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import falcon +from falcon_multipart.middleware import MultipartMiddleware +from beaker.middleware import SessionMiddleware + +from middleware import ( + AuthMiddleware, + JSONTranslator, + ConnectionMiddleware, + static, + handle_404 +) +from models.db import StorageEngine +from controllers.main import (AppEmpresas, + AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, + AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, + AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco, +<<<<<<< HEAD + AppMovimientosBanco, AppTickets, AppStudents +======= + AppMovimientosBanco, AppTickets, AppEmployees, AppNomina +>>>>>>> nomina +) + + +from settings import DEBUG, MV, PATH_SESSIONS + + +db = StorageEngine() + +api = falcon.API(middleware=[ + AuthMiddleware(), + JSONTranslator(), + ConnectionMiddleware(), + MultipartMiddleware(), +]) +api.req_options.auto_parse_form_urlencoded = True +api.add_sink(handle_404, '') + +api.add_route('/empresas', AppEmpresas(db)) +api.add_route('/', AppLogin(db)) +api.add_route('/logout', AppLogout(db)) +api.add_route('/admin', AppAdmin(db)) +api.add_route('/emisor', AppEmisor(db)) +api.add_route('/folios', AppFolios(db)) +api.add_route('/main', AppMain(db)) +api.add_route('/values/{table}', AppValues(db)) +api.add_route('/files/{table}', AppFiles(db)) +api.add_route('/config', AppConfig(db)) +api.add_route('/doc/{type_doc}/{id_doc}', AppDocumentos(db)) +api.add_route('/partners', AppPartners(db)) +api.add_route('/products', AppProducts(db)) +api.add_route('/invoices', AppInvoices(db)) +api.add_route('/preinvoices', AppPreInvoices(db)) +api.add_route('/tickets', AppTickets(db)) +api.add_route('/cuentasbanco', AppCuentasBanco(db)) +api.add_route('/movbanco', AppMovimientosBanco(db)) +<<<<<<< HEAD +api.add_route('/students', AppStudents(db)) +======= +api.add_route('/employees', AppEmployees(db)) +api.add_route('/nomina', AppNomina(db)) +>>>>>>> nomina + + +# ~ Activa si usas waitress y NO estas usando servidor web +# ~ api.add_sink(static, '/static') + +session_options = { + 'session.type': 'file', + 'session.cookie_expires': True, + 'session.httponly': True, + 'session.secure': True, + 'session.data_dir': PATH_SESSIONS['data'], + 'session.lock_dir': PATH_SESSIONS['lock'], +} +if DEBUG or MV: + session_options['session.secure'] = False + +app = SessionMiddleware(api, session_options) + diff --git a/source/app/models/db.py b/source/app/models/db.py index 84a9b78..37c3281 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -11,6 +11,17 @@ class StorageEngine(object): def authenticate(self, args): return main.authenticate(args) + def get_employees(self, values): + return main.Empleados.get_by(values) + + 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) @@ -107,6 +118,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() @@ -257,6 +271,10 @@ class StorageEngine(object): return main.NivelesEducativos.remove(id) if table == 'students': return main.Alumnos.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/db.py.orig b/source/app/models/db.py.orig new file mode 100644 index 0000000..e5b9f7a --- /dev/null +++ b/source/app/models/db.py.orig @@ -0,0 +1,398 @@ +#!/usr/bin/env python + +from . import main + + +class StorageEngine(object): + + def __init__(self): + pass + + def authenticate(self, args): + return main.authenticate(args) + + def get_employees(self, values): + return main.Empleados.get_by(values) + + 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) + + def empresa_borrar(self, values): + return main.empresa_borrar(values['rfc']) + + def _get_empresas(self, values): + return main.get_empresas() + + def get_values(self, table, values=None, session=None): + if table in ('allusuarios', 'usuarioupdate'): + return getattr(self, '_get_{}'.format(table))(values, session) + return getattr(self, '_get_{}'.format(table))(values) + + def _get_schoolgroups(self, values): + return main.Grupos.get_by(values) + + def _get_nivedusat(self, values): + return main.SATNivelesEducativos.get_by() + + def _get_niveduall(self, values): + return main.NivelesEducativos.get_all() + + def _get_titlelogin(self, values): + return main.get_title_app(2) + + def _get_canopenpre(self, values): + return main.PreFacturasDetalle.can_open(values['id']) + + def _get_importinvoice(self, values): + return main.import_invoice() + + def _get_main(self, values): + return main.config_main() + + def _get_configtimbrar(self, values): + return main.config_timbrar() + + def _get_invoicenotes(self, values): + return main.Facturas.get_notes(values['id']) + + def save_invoice_notes(self, values): + return main.Facturas.save_notes(values) + + def _get_configticket(self, values): + return main.config_ticket() + + def _get_saldocuenta(self, values): + return main.CuentasBanco.get_saldo(values['id']) + + def _get_validartimbrar(self, values): + return main.validar_timbrar() + + def _get_preproductos(self, values): + return main.PreFacturasDetalle.facturar(values['id']) + + def upload_file(self, session, table, file_obj): + if not 'rfc' in session: + return {'status': 'error'} + return main.upload_file(session['rfc'], table, file_obj) + + def get_config(self, values): + return main.Configuracion.get_(values) + + def add_config(self, values): + return main.Configuracion.add(values) + + def add_cert(self, file_obj): + return main.Certificado.add(file_obj) + + def validate_cert(self, values, session): + return main.Certificado.validate(values, session) + + def validate_email(self, values): + return main.test_correo(values) + + def send_email(self, values, session): + return main.Facturas.send(values['id'], session['rfc']) + + def enviar_prefac(self, values): + return main.PreFacturas.enviar(values['id']) + + def _get_cancelinvoice(self, values): + return main.Facturas.cancel(values['id']) + + def _get_statussat(self, values): + return main.Facturas.get_status_sat(values['id']) + + def _get_filteryears(self, values): + years1 = main.Facturas.filter_years() + years2 = main.PreFacturas.filter_years() + return [years1, years2] + + 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() + + def _get_cert(self, values): + return main.Certificado.get_data() + + def _get_cp(self, values): + return main.get_cp(values['cp']) + + def _get_formapago(self, values): + return main.SATFormaPago.get_activos(values) + + def _get_tiporelacion(self, values): + return main.SATTipoRelacion.get_activos(values) + + def _get_condicionespago(self, values): + return main.CondicionesPago.get_() + + def _get_categorias(self, values): + return main.Categorias.get_all() + + def _get_newkey(self, values): + return main.Productos.next_key() + + def _get_unidades(self, values): + return main.SATUnidades.get_activos() + + def add_moneda(self, values): + return main.SATMonedas.add(values) + + def add_unidad(self, values): + return main.SATUnidades.add(values) + + def add_impuesto(self, values): + return main.SATImpuestos.add(values) + + def add_usuario(self, values): + return main.Usuarios.add(values) + + def edit_usuario(self, values): + return main.Usuarios.edit(values) + + def _get_taxes(self, values): + return main.SATImpuestos.get_activos() + + def _get_alltaxes(self, values): + return main.SATImpuestos.get_() + + def _get_allcurrencies(self, values): + return main.SATMonedas.get_() + + def _get_allbancos(self, values): + return main.SATBancos.get_() + + def _get_allunidades(self, values): + return main.SATUnidades.get_() + + def _get_allformasdepago(self, values): + return main.SATFormaPago.get_() + + def _get_allusoscfdi(self, values): + return main.SATUsoCfdi.get_all() + + def _get_allusuarios(self, values, session): + return main.Usuarios.get_(session['userobj']) + + def _get_usuarioupdate(self, values, session): + return main.Usuarios.actualizar(values, session['userobj']) + + def _get_taxupdate(self, values): + return main.SATImpuestos.actualizar(values) + + def _get_currencyupdate(self, values): + return main.SATMonedas.actualizar(values) + + def _get_bancoupdate(self, values): + return main.SATBancos.actualizar(values) + + def _get_unidadupdate(self, values): + return main.SATUnidades.actualizar(values) + + def _get_formasdepagoupdate(self, values): + return main.SATFormaPago.actualizar(values) + + def _get_usocfdiupdate(self, values): + return main.SATUsoCfdi.actualizar(values) + + def _get_emisorcuentasbanco(self, values): + return main.CuentasBanco.emisor() + + def _get_satkey(self, values): + return main.get_sat_key(values['key']) + + def _get_satmonedas(self, values): + return main.get_sat_monedas(values['key']) + + def _get_satunidades(self, values): + return main.get_sat_unidades(values['key']) + + def _get_satproductos(self, values): + return main.get_sat_productos(values['key']) + + def _get_series(self, values): + return main.Folios.get_all() + + def _get_monedas(self, values): + return main.SATMonedas.get_activos() + + def _get_monedasid(self, values): + return main.SATMonedas.get_activos_by_id() + + def _get_bancosid(self, values): + return main.SATBancos.get_activos_by_id() + + def _get_regimenes(self, values): + return main.Emisor.get_regimenes() + + def _get_usocfdi(self, values): + return main.SATUsoCfdi.get_activos() + + def _get_ebancomov(self, values): + return main.MovimientosBanco.con(values['id']) + + def delete(self, table, id): + if table == 'partner': + return main.Socios.remove(id) + if table == 'product': + return main.Productos.remove(id) + if table == 'invoice': + return main.Facturas.remove(id) + if table == 'folios': + return main.Folios.remove(id) + if table == 'preinvoice': + return main.PreFacturas.remove(id) + if table == 'satimpuesto': + return main.SATImpuestos.remove(id) + if table == 'satunit': + return main.SATUnidades.remove(id) + if table == 'cuentasbanco': + return main.CuentasBanco.remove(id) + if table == 'movbanco': + return main.MovimientosBanco.remove(id) + if table == 'usuario': + return main.Usuarios.remove(id) + if table == 'config': + return main.Configuracion.remove(id) +<<<<<<< HEAD + if table == 'nivedu': + return main.NivelesEducativos.remove(id) + if table == 'students': + return main.Alumnos.remove(id) +======= + if table == 'employee': + return main.Empleados.remove(id) + if table == 'nomina': + return main.CfdiNomina.remove(id) +>>>>>>> nomina + return False + + def _get_client(self, values): + return main.Socios.get_by_client(values) + + def _get_student(self, values): + return main.Alumnos.get_by_name(values) + + def _get_product(self, values): + return main.Productos.get_by(values) + + def _get_productokey(self, values): + return main.Productos.get_by_key(values) + + def get_partners(self, values): + return main.Socios.get_(values) + + def partner(self, values): + id = int(values.pop('id', '0')) + if id: + return main.Socios.actualizar(values, id) + return main.Socios.add(values) + + def get_products(self, values): + return main.Productos.get_(values) + + def products(self, values): + id = int(values.pop('id', '0')) + if id: + return main.Productos.actualizar(values, id) + + opt = values.get('opt', '') + if opt: + return main.Productos.opt(values) + + return main.Productos.add(values) + + def invoice(self, values, user): + id = int(values.pop('id', '0')) + if id: + return main.Facturas.actualizar(values, id) + + return main.Facturas.add(values, user) + + def preinvoice(self, values): + id = int(values.pop('id', '0')) + #~ if id: + #~ return main.PreFacturas.actualizar(values, id) + return main.PreFacturas.add(values) + + def get_students(self, values): + return main.Alumnos.get_by(values) + + def students(self, values): + opt = values.pop('opt') + if opt == 'add': + return main.Alumnos.add(values['values']) + if opt == 'edit': + return main.Alumnos.actualizar(values['values']) + + def tickets(self, values, user): + opt = values.pop('opt') + if opt == 'add': + return main.Tickets.add(values, user) + if opt == 'cancel': + return main.Tickets.cancel(values) + if opt == 'invoice': + return main.Tickets.invoice(values, user) + if opt == 'print': + return main.Tickets.printer(values) + + def get_tickets(self, values): + return main.Tickets.get_by(values) + + def get_invoices(self, values): + return main.Facturas.get_(values) + + def get_preinvoices(self, values): + return main.PreFacturas.get_(values) + + def _get_timbrar(self, values): + return main.Facturas.timbrar(int(values['id'])) + + def _get_anticipoegreso(self, values): + return main.Facturas.anticipo_egreso(int(values['id'])) + + def get_emisor(self, rfc): + return main.Emisor.get_(rfc) + + def emisor(self, values): + return main.Emisor.add(values) + + def cuentasbanco(self, values): + return main.CuentasBanco.add(values) + + def add_movbanco(self, values): + return main.MovimientosBanco.add(values) + + def get_cuentasbanco(self, values): + return main.CuentasBanco.get_(values) + + def get_folios(self): + return main.Folios.get_() + + def add_folios(self, values): + return main.Folios.add(values) + + def add_nivel_educativo(self, values): + return main.NivelesEducativos.add(values) + + def get_doc(self, type_doc, id, rfc): + return main.get_doc(type_doc, id, rfc) + + def get_movimientosbanco(self, values): + return main.MovimientosBanco.get_(values) + + def importar_bdfl(self): + return main.importar_bdfl() diff --git a/source/app/models/main.py b/source/app/models/main.py index 8b0806c..070bf98 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -17,10 +17,12 @@ if __name__ == '__main__': from controllers import util from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ - CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, DECIMALES_TAX, TITLE_APP + CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ + DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP FORMAT = '{0:.2f}' +FORMAT3 = '{0:.3f}' FORMAT_TAX = '{0:.4f}' @@ -66,7 +68,8 @@ def desconectar(): def upload_file(rfc, opt, file_obj): result = util.upload_file(rfc, opt, file_obj) if result['ok']: - if opt != 'bdfl': + names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods') + if not opt in names: Configuracion.add({opt: file_obj.filename}) return result @@ -168,6 +171,7 @@ def get_doc(type_doc, id, rfc): 'xml': 'application/xml', 'ods': 'application/octet-stream', 'zip': 'application/octet-stream', + 'nomlog': 'application/txt', } content_type = types.get(type_doc, 'application/pdf') if type_doc == 'xml': @@ -182,6 +186,10 @@ 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) + elif type_doc == 'nomlog': + data, file_name = util.get_log('nomina') return data, file_name, content_type @@ -193,10 +201,12 @@ def config_main(): obj = None punto_de_venta = util.get_bool(Configuracion.get_('chk_usar_punto_de_venta')) + nomina = util.get_bool(Configuracion.get_('chk_usar_nomina')) data = { 'empresa': get_title_app(3), 'punto_de_venta': punto_de_venta, 'escuela': False, + 'nomina': nomina, } if not obj is None: titulo = '{} - {}' @@ -323,6 +333,7 @@ class Configuracion(BaseModel): 'chk_ticket_direct_print', 'chk_ticket_edit_cant', 'chk_ticket_total_up', + 'chk_usar_nomina', ) data = (Configuracion .select() @@ -677,6 +688,7 @@ class Emisor(BaseModel): token_timbrado = TextField(default='') token_soporte = TextField(default='') logo = TextField(default='') + registro_patronal = TextField(default='') regimenes = ManyToManyField(SATRegimenes, related_name='emisores') def __str__(self): @@ -696,6 +708,7 @@ class Emisor(BaseModel): obj = obj[0] row['emisor'] = { 'emisor_rfc': obj.rfc, + 'emisor_curp': obj.curp, 'emisor_nombre': obj.nombre, 'emisor_cp': obj.codigo_postal, 'emisor_cp2': obj.cp_expedicion, @@ -719,6 +732,7 @@ class Emisor(BaseModel): 'correo_timbrado': obj.correo_timbrado, 'token_timbrado': obj.token_timbrado, 'token_soporte': obj.token_soporte, + 'emisor_registro_patronal': obj.registro_patronal, 'regimenes': [row.id for row in obj.regimenes] } else: @@ -753,6 +767,7 @@ class Emisor(BaseModel): def _clean(self, values): fields = util.clean(values) fields['rfc'] = fields.pop('emisor_rfc') + fields['curp'] = fields.pop('emisor_curp', '') fields['nombre'] = fields.pop('emisor_nombre') fields['codigo_postal'] = fields.pop('emisor_cp') fields['cp_expedicion'] = fields.pop('emisor_cp2', '') or fields['codigo_postal'] @@ -775,6 +790,7 @@ class Emisor(BaseModel): fields['fecha_dof'] = fields.pop('ong_fecha_dof', None) if len(fields['rfc']) == 12: fields['es_moral'] = True + fields['registro_patronal'] = fields.pop('emisor_registro_patronal', '') fields['regimenes'] = SATRegimenes.get_( util.loads(fields['regimenes'])) return fields @@ -1393,6 +1409,18 @@ class SATBancos(BaseModel): result = bool(q.execute()) return {'ok': result} + @classmethod + def get_by_key(cls, key): + if not key: + return + try: + obj = SATBancos.get(SATBancos.key==key) + return obj + except SATBancos.DoesNotExist: + msg = 'SATBancos no existe: {}'.format(key) + log.error(msg) + return + class SATNivelesEducativos(BaseModel): name = TextField(index=True) @@ -1888,6 +1916,338 @@ class SATUsoCfdi(BaseModel): return tuple(rows) +class SATEstados(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + pais = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name', 'pais'), True), + ) + + def __str__(self): + return 'Estado: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATEstados.get(SATEstados.key==key) + return obj + except SATEstados.DoesNotExist: + msg = 'SATEstados no existe: {}'.format(key) + log.error(msg) + return + + +class SATOrigenRecurso(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Origen Recurso: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATOrigenRecurso.get(SATOrigenRecurso.key==key) + return obj + except SATOrigenRecurso.DoesNotExist: + msg = 'SATOrigenRecurso no existe: {}'.format(key) + log.error(msg) + return + + +class SATPeriodicidadPago(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Periodicidad de Pago: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATPeriodicidadPago.get(SATPeriodicidadPago.key==key) + return obj + except SATPeriodicidadPago.DoesNotExist: + msg = 'SATPeriodicidadPago no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoContrato(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Contrato: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoContrato.get(SATTipoContrato.key==key) + return obj + except SATTipoContrato.DoesNotExist: + msg = 'SATTipoContrato no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoDeduccion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Deducción: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoDeduccion.get(SATTipoDeduccion.key==key) + return obj + except SATTipoDeduccion.DoesNotExist: + msg = 'SATTipoDeduccion no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoHoras(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Horas: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoHoras.get(SATTipoHoras.key==key) + return obj + except SATTipoHoras.DoesNotExist: + msg = 'SATTipoHoras no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoIncapacidad(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Incapacidad: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoIncapacidad.get(SATTipoIncapacidad.key==key) + return obj + except SATTipoIncapacidad.DoesNotExist: + msg = 'SATTipoIncapacidad no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoJornada(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Jornada: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoJornada.get(SATTipoJornada.key==key) + return obj + except SATTipoJornada.DoesNotExist: + msg = 'SATTipoJornada no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoNomina(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Nómina: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoNomina.get(SATTipoNomina.key==key) + return obj + except SATTipoNomina.DoesNotExist: + msg = 'SATTipoNomina no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoOtroPago(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Otro Pago: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoOtroPago.get(SATTipoOtroPago.key==key) + return obj + except SATTipoOtroPago.DoesNotExist: + msg = 'SATTipoOtroPago no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoPercepcion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Percepción: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoPercepcion.get(SATTipoPercepcion.key==key) + return obj + except SATTipoPercepcion.DoesNotExist: + msg = 'SATTipoPercepcion no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoRegimen(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Regimen de contratación: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoRegimen.get(SATTipoRegimen.key==key) + return obj + except SATTipoRegimen.DoesNotExist: + msg = 'SATTipoRegimen no existe: {}'.format(key) + log.error(msg) + return + + +class SATRiesgoPuesto(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Riesgo Puesto: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATRiesgoPuesto.get(SATRiesgoPuesto.key==key) + return obj + except SATRiesgoPuesto.DoesNotExist: + msg = 'SATRiesgoPuesto no existe: {}'.format(key) + log.error(msg) + return + + class TipoCambio(BaseModel): dia = DateField(default=util.now) moneda = ForeignKeyField(SATMonedas) @@ -3231,7 +3591,7 @@ class Facturas(BaseModel): locales_retenciones = 0 for product in products: - # ~ print ('\n', product) + # ~ print ('\n', product['descripcion']) id_product = product.pop('id') id_student = product.pop('id_student', 0) p = Productos.get(Productos.id==id_product) @@ -5125,6 +5485,1120 @@ class SeriesProductos(BaseModel): order_by = ('serie',) +class Departamentos(BaseModel): + nombre = TextField(default='', unique=True) + descripcion = TextField(default='') + + class Meta: + order_by = ('nombre',) + + +class Puestos(BaseModel): + departamento = ForeignKeyField(Departamentos, null=True) + nombre = TextField(default='', unique=True) + descripcion = TextField(default='') + + class Meta: + order_by = ('nombre',) + + @classmethod + def get_by_depto(cls, puesto, depto): + departamento = None + if depto: + with database_proxy.transaction(): + departamento, _ = Departamentos.get_or_create(nombre=depto) + data = {'departamento': departamento, 'nombre': puesto} + obj, _ = Puestos.get_or_create(**data) + return obj + + +class Empleados(BaseModel): + num_empleado = TextField(default='') + rfc = TextField(default='', unique=True) + curp = TextField(default='', unique=True) + nombre = TextField(default='') + paterno = TextField(default='') + materno = TextField(default='') + nombre_completo = TextField(default='') + es_activo = BooleanField(default=True) + es_extranjero = BooleanField(default=False) + fecha_alta = DateField(default=util.now) + fecha_ingreso = DateField(null=True) + imss = TextField(default='') + tipo_contrato = ForeignKeyField(SATTipoContrato) + es_sindicalizado = BooleanField(default=False) + tipo_jornada = ForeignKeyField(SATTipoJornada, null=True) + tipo_regimen = ForeignKeyField(SATTipoRegimen) + puesto = ForeignKeyField(Puestos, null=True) + riesgo_puesto = ForeignKeyField(SATRiesgoPuesto, null=True) + periodicidad_pago = ForeignKeyField(SATPeriodicidadPago) + banco = ForeignKeyField(SATBancos, null=True) + cuenta_bancaria = TextField(default='') + clabe = TextField(default='') + salario_base = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + salario_diario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + estado = ForeignKeyField(SATEstados) + codigo_postal = TextField(default='') + notas = TextField(default='') + correo = TextField(default='') + + class Meta: + order_by = ('nombre_completo',) + indexes = ( + (('num_empleado', 'rfc'), True), + ) + + def _validate_import(self, row): + sn = {'si': True, 'no': False} + data = row.copy() + data['nombre_completo'] = '{} {} {}'.format( + row['nombre'], row['paterno'], row['materno']).strip() + 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']) + data['tipo_regimen'] = SATTipoRegimen.get_by_key(row['tipo_regimen']) + data['puesto'] = Puestos.get_by_depto(row['puesto'], row['departamento']) + data['riesgo_puesto'] = SATRiesgoPuesto.get_by_key(row['riesgo_puesto']) + data['periodicidad_pago'] = SATPeriodicidadPago.get_by_key(row['periodicidad_pago']) + data['banco'] = SATBancos.get_by_key(row['banco']) + data['estado'] = SATEstados.get_by_key(row['estado']) + del data['departamento'] + return data + + def _import(self): + emisor = Emisor.select()[0] + rows, msg = util.import_employees(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + 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(): + if Empleados.select().where(w).exists(): + q = Empleados.update(**data).where(w) + q.execute() + ea += 1 + else: + obj = Empleados.create(**data) + en += 1 + + msg = 'Empleados encontrados: {}
'.format(len(rows)) + msg += 'Empleados nuevos: {}
'.format(en) + msg += 'Empleados actualizados: {}
'.format(ea) + msg += 'Empleados no importados: {}'.format(len(rows) - en - ea) + return {'ok': True, 'msg': msg} + + def _get(self): + rows = (Empleados + .select( + Empleados.id, + Empleados.num_empleado, + Empleados.rfc, + Empleados.curp, + Empleados.nombre_completo, + Empleados.imss, + Empleados.salario_base, + Empleados.salario_diario, + Empleados.fecha_ingreso) + .dicts() + ) + return {'ok': True, 'rows': tuple(rows)} + + @classmethod + def get_by(cls, values): + if not 'opt' in values: + return cls._get(cls) + + if values['opt'] == 'import': + return cls._import(cls) + + @classmethod + def remove(cls, id): + try: + q = Empleados.delete().where(Empleados.id==id) + return bool(q.execute()) + except IntegrityError: + return False + + +class CfdiNomina(BaseModel): + empleado = ForeignKeyField(Empleados) + version = TextField(default=CURRENT_CFDI) + 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) + forma_pago = TextField(default='') + condiciones_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + moneda = TextField(default='MXN') + tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6, + auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + tipo_comprobante = TextField(default='N') + metodo_pago = TextField(default='PUE') + lugar_expedicion = TextField(default='') + confirmacion = TextField(default='') + uso_cfdi = TextField(default='') + total_retenciones = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + xml = TextField(default='') + uuid = UUIDField(null=True) + estatus = TextField(default='Guardado') + estatus_sat = TextField(default='Vigente') + regimen_fiscal = TextField(default='') + notas = TextField(default='') + saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + pagada = BooleanField(default=False) + cancelada = BooleanField(default=False) + fecha_cancelacion = DateTimeField(null=True) + acuse = TextField(default='') + tipo_relacion = TextField(default='') + error = TextField(default='') + version_nomina = TextField(default=CURRENT_CFDI_NOMINA) + registro_patronal = TextField(default='') + rfc_patron_origen = TextField(default='') + tipo_nomina = ForeignKeyField(SATTipoNomina) + fecha_pago = DateField() + fecha_inicial_pago = DateField() + fecha_final_pago = DateField() + dias_pagados = DecimalField(default=0.0, max_digits=12, decimal_places=2, + auto_round=True) + origen_recurso = ForeignKeyField(SATOrigenRecurso, null=True) + monto_recurso_propio = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + class Meta: + order_by = ('fecha',) + + 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 = inicio + 1 + + if folio > new: + new = folio + + return new + + def _validate_nomina(self, row): + sn = {'si': True, 'no': False} + data = row.copy() + 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 = 0.0 + exento = 0.0 + if isinstance(row[i * 2], float): + gravado = round(row[i * 2], DECIMALES) + if isinstance(row[i * 2 + 1], float): + 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 = 0.0 + if isinstance(value, float): + 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 = 0.0 + if isinstance(value, float): + 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 = 0 + if isinstance(row[i * 4], float): + days = int(row[i * 4]) + key = row[i * 4 + 1] + the = SATTipoHoras.get_by_key(key) + if the is None: + continue + + hours = 0 + if isinstance(row[i * 4 + 2], float): + hours = int(row[i * 4 + 2]) + importe = 0.0 + if isinstance(row[i * 4 + 3], float): + 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 = 0 + if isinstance(row[i * 3 + 1], float): + days = int(row[i * 3 + 1]) + importe = 0.0 + if isinstance(row[i * 3 + 2], float): + importe = round(row[i * 3 + 2], DECIMALES) + if not days or not importe: + continue + + new = { + 'dias': ti, + 'tipo': days, + 'importe': importe, + } + data.append(new) + + return data, '' + + def _validate_exists(self, values): + result = (CfdiNomina + .select() + .where( + (CfdiNomina.empleado==values['empleado']) & + (CfdiNomina.fecha_pago==values['fecha_pago']) & + (CfdiNomina.fecha_inicial_pago==values['fecha_inicial_pago']) & + (CfdiNomina.fecha_final_pago==values['fecha_final_pago']) & + (CfdiNomina.total==values['total'])) + .exists()) + return result + + def _import(self): + util.log_file('nomina', kill=True) + emisor = Emisor.select()[0] + data, msg = util.import_nomina(emisor.rfc) + if not data: + return {'ok': False, 'msg': msg} + + 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) + + new_nomina['subtotal'] = totals['subtotal'] + new_nomina['descuento'] = totals['descuento'] + new_nomina['total'] = totals['total'] + new_nomina['total_mn'] = totals['total'] + + if self._validate_exists(self, new_nomina): + info = '{}'.format(new_nomina['empleado'].nombre_completo) + msg = 'Nomina existente: {}'.format(info) + util.log_file('nomina', msg) + continue + + try: + with database_proxy.transaction(): + 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) + except Exception as e: + msg = 'ERROR: {}-{}'.format(new_nomina['serie'], new_nomina['folio']) + util.log_file('nomina', msg) + util.log_file('nomina', str(e)) + continue + + msg = 'Nómina importada correctamente' + return {'ok': True, 'msg': msg} + + def _get(self, where=''): + if not where: + where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) + rows = (CfdiNomina + .select( + 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) + .order_by(CfdiNomina.id) + .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.replace('-', '') + elif empleado.cuenta_bancaria: + nomina_receptor['CuentaBancaria'] = empleado.cuenta_bancaria.replace('-', '') + 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), + } + if totals.total_retenciones: + 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): + msg = '' + where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) + rows = CfdiNomina.select().where(where).order_by(CfdiNomina.id) + 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 + util.log_file('nomina', msg) + else: + msg = 'Error la timbrar: {}-{}, {}'.format(row.serie, row.folio, msg) + util.log_file('nomina', msg) + 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 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) + clave_sat = TextField(default=DEFAULT_SAT_NOMINA['CLAVE']) + cantidad = DecimalField(default=1.0, max_digits=18, decimal_places=6, + auto_round=True) + clave_unidad = TextField(default=DEFAULT_SAT_NOMINA['UNIDAD']) + descripcion = TextField(default=DEFAULT_SAT_NOMINA['DESCRIPCION']) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('cfdi',) + + +class CfdiNominaTotales(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_percepciones = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_deducciones = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_otros_pagos = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_sueldos = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_separacion = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_jubilacion = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_retenciones = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_otras_deducciones = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaJubilacion(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + total_una_exhibicion = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total_parcialidad = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + monto_diario = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaSeparacion(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + total_pagado = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + years_servicio = IntegerField(default=0) + ultimo_sueldo = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +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, + auto_round=True) + valor_mercado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + precio_al_ortorgarse = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +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) + + +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, + auto_round=True) + saldo_a_favor = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + year = IntegerField(default=0) + remanente_saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaIncapacidad(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + dias = IntegerField(default=0) + tipo = ForeignKeyField(SATTipoIncapacidad) + importe = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaHorasExtra(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + dias = IntegerField(default=0) + tipos_horas = ForeignKeyField(SATTipoHoras) + horas_extra = IntegerField(default=0) + importe_pagado = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaSubcontratos(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + rfc = TextField(default='') + porcentaje = DecimalField(default=0.0, max_digits=12, decimal_places=2, + auto_round=True) + + class Meta: + order_by = ('cfdi',) + + +class CfdiNominaOtros(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + node = TextField(default='') + key = TextField(default='') + value = TextField(default='') + + +class CfdiNominaRelacionados(BaseModel): + cfdi = ForeignKeyField(CfdiNomina, related_name='original') + cfdi_origen = ForeignKeyField(CfdiNomina, related_name='relacion') + + class Meta: + order_by = ('cfdi',) + + def authenticate(args): respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''} values = util.get_con(args['rfc']) @@ -5264,14 +6738,22 @@ def _crear_tablas(rfc): PreFacturasRelacionadas, Tickets, TicketsDetalle, TicketsImpuestos, SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos, - SATNivelesEducativos, - Socios, Contactos, ContactoCorreos, ContactoDirecciones, - ContactoTelefonos, + SATNivelesEducativos, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago, + SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras, + SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago, + SATTipoPercepcion, SATTipoRegimen, + Socios, Contactos, ContactoCorreos, ContactoDirecciones, Empleados, + ContactoTelefonos, Departamentos, Puestos, Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco, TipoCorreo, TipoDireccion, TipoPariente, TipoResponsable, TipoTelefono, TipoTitulo, TipoMovimientoAlumno, TipoMovimientoAlmacen, CfdiPagos, NivelesEducativos, Alumnos, AlumnosParientes, Grupos, ParienteDirecciones, ParienteTelefonos, ParienteCorreos, + CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle, + CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion, + CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones, + CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, + CfdiNominaTotales, Emisor.regimenes.get_through_model(), Socios.tags.get_through_model(), Productos.impuestos.get_through_model(), @@ -5316,8 +6798,17 @@ def _migrate_tables(): conectar(args) - tablas = [Sucursales] - log.info('Creando nuevas tablas...') + tablas = [Sucursales, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago, + SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras, + SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago, + SATTipoPercepcion, SATTipoRegimen, Departamentos, Puestos, Empleados, + CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle, + CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion, + CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones, + CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, + CfdiNominaTotales, + ] + log.info('Creando tablas nuevas...') database_proxy.create_tables(tablas, True) log.info('Tablas creadas correctamente...') @@ -5325,12 +6816,23 @@ def _migrate_tables(): migrations = [] migrator = PostgresqlMigrator(database_proxy) - sucursal = ForeignKeyField(Sucursales, null=True, to_field=Sucursales.id) columns = [c.name for c in database_proxy.get_columns('usuarios')] if not 'sucursal_id' in columns: + sucursal = ForeignKeyField(Sucursales, null=True, to_field=Sucursales.id) migrations.append( migrator.add_column('usuarios', 'sucursal_id', sucursal)) + columns = [c.name for c in database_proxy.get_columns('emisor')] + if not 'registro_patronal' in columns: + registro_patronal = TextField(default='') + migrations.append( + migrator.add_column( + 'emisor', 'registro_patronal', registro_patronal)) + if not 'curp' in columns: + curp = TextField(default='') + migrations.append( + migrator.add_column('emisor', 'curp', curp)) + if migrations: with database_proxy.atomic() as txn: migrate(*migrations) @@ -5882,7 +7384,7 @@ def _importar_factura_libre_gambas(conexion): _importar_socios(data['Socios']) _importar_facturas(data['Facturas']) _importar_categorias(data['Categorias']) - # ~ _importar_productos_gambas(data['Productos']) + _importar_productos_gambas(data['Productos']) _import_tickets(data['Tickets']) log.info('Importación terminada...') @@ -6057,6 +7559,41 @@ def _importar_productos(archivo): return +def _import_from_folder(path): + files = util.get_files(path, 'json') + if not files: + msg = 'No se encontraron archivos para importar' + log.error(msg) + return + + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + log.info('Importando valores...') + for p in files: + msg = '\tImportando tabla: {}' + data = util.import_json(p) + log.info(msg.format(data['tabla'])) + table = globals()[data['tabla']] + for r in data['datos']: + try: + with database_proxy.atomic() as txn: + table.create(**r) + except IntegrityError: + pass + + log.info('Valores importados...') + return + + def _test(): rfc = input('Introduce el RFC: ').strip().upper() if not rfc: @@ -6107,10 +7644,12 @@ help_lr = 'Listar RFCs' @click.option('-a', '--alta', is_flag=True, default=False) @click.option('-r', '--rfc') @click.option('-d', '--detalle', is_flag=True, default=False) +@click.option('-id', '--importar-directorio') def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, agregar_rfc, borrar_rfc, listar_rfc, importar_valores, archivo, conexion, factura_libre, factura_libre_gambas, test, generar_archivo_productos, - importar_productos, backup_dbs, no_bd, alta, rfc, detalle): + importar_productos, backup_dbs, no_bd, alta, rfc, detalle, + importar_directorio): opt = locals() @@ -6216,6 +7755,9 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, _importar_productos(opt['archivo']) sys.exit(0) + if opt['importar_directorio']: + _import_from_folder(opt['importar_directorio']) + if opt['backup_dbs']: util.backup_dbs() diff --git a/source/app/models/main.py.orig b/source/app/models/main.py.orig new file mode 100644 index 0000000..9f00ce9 --- /dev/null +++ b/source/app/models/main.py.orig @@ -0,0 +1,7772 @@ +#!/usr/bin/env python + +from decimal import Decimal +import sqlite3 +import click +from peewee import * +from playhouse.fields import PasswordField, ManyToManyField +from playhouse.shortcuts import case, SQL, cast + + +if __name__ == '__main__': + import os, sys + parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, parent_dir) + + +from controllers import util +from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \ + INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ + CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ + DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP + + +FORMAT = '{0:.2f}' +FORMAT3 = '{0:.3f}' +FORMAT_TAX = '{0:.4f}' + + +database_proxy = Proxy() +class BaseModel(Model): + class Meta: + database = database_proxy + + +def conectar(opt): + db = { + 'sqlite': SqliteDatabase, + 'postgres': PostgresqlDatabase, + 'mysql': MySQLDatabase, + } + db_type = opt.pop('type') + db_name = opt.pop('name') + if not db_type in db: + log.error('Tipo de base de datos no soportado') + return False + #~ print ('DB NAME', db_name) + database = db[db_type](db_name, **opt) + try: + database_proxy.initialize(database) + database_proxy.connect() + log.info('Conectado a la BD...') + return True + except OperationalError as e: + log.error('Error al intentar conectar a la base de datos') + log.error(e) + return False + + +def desconectar(): + if database_proxy.obj is None: + return + if not database_proxy.is_closed(): + database_proxy.close() + log.info('Desconectado a la BD...') + return + + +def upload_file(rfc, opt, file_obj): + result = util.upload_file(rfc, opt, file_obj) + if result['ok']: + names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods') + if not opt in names: + Configuracion.add({opt: file_obj.filename}) + return result + + +def validar_timbrar(): + try: + obj = Emisor.select()[0] + except IndexError: + msg = 'Es necesario agregar los datos del emisor' + return {'ok': False, 'msg': msg} + + try: + obj = Folios.select()[0] + except IndexError: + msg = 'Es necesaria al menos una serie de folios' + return {'ok': False, 'msg': msg} + + msg = 'Es necesario configurar un certificado de sellos' + try: + obj = Certificado.select()[0] + except IndexError: + return {'ok': False, 'msg': msg} + + if not obj.serie: + return {'ok': False, 'msg': msg} + + dias = obj.hasta - util.now() + if dias.days < 0: + msg = 'El certificado ha vencido, es necesario cargar uno nuevo' + return {'ok': False, 'msg': msg} + + msg = '' + if dias.days < 15: + msg = 'El certificado vence en: {} días.'.format(dias.days) + + return {'ok': True, 'msg': msg} + + + +def _get_taxes_product(id): + model_pt = Productos.impuestos.get_through_model() + impuestos = tuple(model_pt + .select( + model_pt.productos_id.alias('product'), + model_pt.satimpuestos_id.alias('tax')) + .where(model_pt.productos_id==id).dicts()) + return impuestos + + +def import_invoice(): + log.info('Importando factura...') + emisor = Emisor.select()[0] + rows, msg = util.import_invoice(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + # ~ clave, descripcion, precio, cantidad + products = {} + for row in rows: + try: + obj = Productos.get(Productos.clave==row[0]) + if obj.id in products: + vu = round(row[2], 2) + descuento = round(row[3], 2) + cant = round(row[4], 2) + pf = products[obj.id]['valor_unitario'] - descuento + products[obj.id]['cantidad'] += cant + products[obj.id]['importe'] = round( + pf * products[obj.id]['cantidad'], DECIMALES) + if vu != products[obj.id]['valor_unitario']: + msg = 'Precio diferente en producto: {}'.format(row[0]) + return {'ok': False, 'msg': msg} + else: + vu = round(row[2], 2) + descuento = round(row[3], 2) + cant = round(row[4], 2) + pf = vu - descuento + p = { + 'id': obj.id, + 'delete': '-', + 'clave': obj.clave, + 'descripcion': obj.descripcion, + 'unidad': obj.unidad.name, + 'cantidad': cant, + 'valor_unitario': vu, + 'descuento': obj.descuento, + 'importe': round(pf * cant, DECIMALES), + 'taxes': _get_taxes_product(obj.id), + } + products[obj.id] = p + except Productos.DoesNotExist: + pass + log.info('Factura importada...') + return {'ok': True, 'rows': tuple(products.values())} + + +def get_doc(type_doc, id, rfc): + types = { + 'xml': 'application/xml', + 'ods': 'application/octet-stream', + 'zip': 'application/octet-stream', + 'nomlog': 'application/txt', + } + content_type = types.get(type_doc, 'application/pdf') + if type_doc == 'xml': + data, file_name = Facturas.get_xml(id) + elif type_doc == 'pdf': + data, file_name = Facturas.get_pdf(id, rfc) + elif type_doc == 'ods': + data, file_name = Facturas.get_ods(id, rfc) + elif type_doc == 'zip': + data, file_name = Facturas.get_zip(id, rfc) + elif type_doc == 'pre': + 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) + elif type_doc == 'nomlog': + data, file_name = util.get_log('nomina') + + return data, file_name, content_type + + +def config_main(): + try: + obj = Emisor.select()[0] + except IndexError: + obj = None + + punto_de_venta = util.get_bool(Configuracion.get_('chk_usar_punto_de_venta')) + nomina = util.get_bool(Configuracion.get_('chk_usar_nomina')) + data = { + 'empresa': get_title_app(3), + 'punto_de_venta': punto_de_venta, +<<<<<<< HEAD + 'escuela': False, +======= + 'nomina': nomina, +>>>>>>> nomina + } + if not obj is None: + titulo = '{} - {}' + data['empresa'] = titulo.format(data['empresa'], obj.nombre) + data['escuela'] = obj.es_escuela + + return data + + +def config_timbrar(): + try: + obj = Emisor.select()[0] + except IndexError: + return {'cfdi_donativo': False} + + conf = { + 'cfdi_donativo': obj.es_ong, + 'cfdi_anticipo': Configuracion.get_('chk_config_anticipo'), + 'cfdi_ine': Configuracion.get_bool('chk_config_ine'), + 'cfdi_edu': Configuracion.get_bool('chk_config_edu'), + 'cfdi_metodo_pago': Configuracion.get_bool('chk_config_ocultar_metodo_pago'), + 'cfdi_condicion_pago': Configuracion.get_bool('chk_config_ocultar_condiciones_pago'), + 'cfdi_open_pdf': Configuracion.get_bool('chk_config_open_pdf'), + 'cfdi_show_pedimento': Configuracion.get_bool('chk_config_show_pedimento'), + 'cfdi_tax_locales': Configuracion.get_bool('chk_config_tax_locales'), + 'cfdi_tax_decimals': Configuracion.get_bool('chk_config_tax_decimals'), + 'cfdi_with_taxes': Configuracion.get_bool('chk_config_price_with_taxes_in_invoice'), + } + + return conf + + +def config_ticket(): + conf = { + 'open_pdf': Configuracion.get_bool('chk_ticket_pdf_show'), + 'direct_print': Configuracion.get_bool('chk_ticket_direct_print'), + 'edit_cant': Configuracion.get_bool('chk_ticket_edit_cant'), + 'total_up': Configuracion.get_bool('chk_ticket_total_up'), + } + return conf + + +class Configuracion(BaseModel): + clave = TextField(unique=True) + valor = TextField(default='') + + def __str__(self): + return '{} = {}'.format(self.clave, self.valor) + + @classmethod + def get_bool(cls, key): + data = (Configuracion + .select(Configuracion.valor) + .where(Configuracion.clave == key) + ) + if data: + return util.get_bool(data[0].valor) + return False + + @classmethod + def get_(cls, keys): + if isinstance(keys, str): + data = (Configuracion + .select(Configuracion.valor) + .where(Configuracion.clave == keys) + ) + if data: + return data[0].valor + return '' + + if keys['fields'] == 'productos': + fields = ( + 'chk_config_cuenta_predial', + 'chk_config_codigo_barras', + 'chk_config_precio_con_impuestos', + 'chk_llevar_inventario', + ) + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + values = {r.clave: r.valor for r in data} + values['default_tax'] = SATImpuestos.select()[0].id + values['default_unidad'] = SATUnidades.get_default() + return values + + if keys['fields'] == 'configtemplates': + try: + emisor = Emisor.select()[0] + is_ong = emisor.es_ong + except IndexError: + is_ong = False + + values = {'txt_plantilla_donataria': is_ong} + fields = ( + ('chk_usar_punto_de_venta', 'txt_plantilla_ticket'), + ) + + for s, key in fields: + value = util.get_bool(Configuracion.get_(s)) + values[key] = value + + return values + + if keys['fields'] == 'configotros': + fields = ( + 'chk_config_ocultar_metodo_pago', + 'chk_config_ocultar_condiciones_pago', + 'chk_config_send_zip', + 'chk_config_open_pdf', + 'chk_config_show_pedimento', + 'chk_config_tax_locales', + 'chk_config_tax_decimals', + 'chk_config_price_with_taxes_in_invoice', + 'chk_config_anticipo', + 'chk_config_cuenta_predial', + 'chk_config_codigo_barras', + 'chk_config_precio_con_impuestos', + 'chk_llevar_inventario', + 'chk_config_ine', + 'chk_config_edu', + 'chk_usar_punto_de_venta', + 'chk_ticket_pdf_show', + 'chk_ticket_direct_print', + 'chk_ticket_edit_cant', + 'chk_ticket_total_up', + 'chk_usar_nomina', + ) + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + values = {r.clave: util.get_bool(r.valor) for r in data} + tp = 'txt_ticket_printer' + values[tp] = Configuracion.get_(tp) + return values + + if keys['fields'] == 'correo': + fields = ('correo_servidor', 'correo_puerto', 'correo_ssl', + 'correo_usuario', 'correo_contra', 'correo_copia', + 'correo_asunto', 'correo_mensaje', 'correo_directo', + 'correo_confirmacion') + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + elif keys['fields'] == 'path_cer': + fields = ('path_key', 'path_cer') + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + elif keys['fields'] == 'templates': + fields = ( + 'txt_plantilla_factura_32', + 'txt_plantilla_factura_33', + 'txt_plantilla_factura_33j', + 'txt_plantilla_ticket', + 'txt_plantilla_donataria', + ) + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + elif keys['fields'] == 'timbrar': + fields = ( + 'chk_config_ocultar_metodo_pago', + 'chk_config_ocultar_condiciones_pago', + 'chk_config_anticipo', + 'chk_config_ine', + 'chk_config_open_pdf', + ) + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + + values = {r.clave: r.valor for r in data} + return values + + @classmethod + def add(cls, values): + # ~ print (values) + try: + for k, v in values.items(): + #~ print (k, v) + obj, created = Configuracion.get_or_create(clave=k) + obj.valor = v + obj.save() + return {'ok': True} + except Exception as e: + log.error(str(e)) + return {'ok': False, 'msg': str(e)} + + @classmethod + def remove(cls, key): + q = Configuracion.delete().where(Configuracion.clave==key) + return bool(q.execute()) + + class Meta: + order_by = ('clave',) + indexes = ( + (('clave', 'valor'), True), + ) + + +class Tags(BaseModel): + tag = TextField(index=True, unique=True) + + class Meta: + order_by = ('tag',) + + +class TipoDireccion(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoTitulo(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoTelefono(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoCorreo(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoPariente(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoResponsable(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoMovimientoAlumno(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoMovimientoAlmacen(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class Sucursales(BaseModel): + nombre = TextField(default='') + direccion = TextField(default='') + serie_facturas = TextField(default='') + serie_tickets = TextField(default='') + + class Meta: + order_by = ('nombre',) + + +class Usuarios(BaseModel): + usuario = TextField(unique=True) + nombre = TextField(default='') + apellidos = TextField(default='') + correo = TextField(default='') + contraseña = PasswordField() + es_superusuario = BooleanField(default=False) + es_admin = BooleanField(default=False) + es_activo = BooleanField(default=True) + fecha_ingreso = DateTimeField(default=util.now) + ultimo_ingreso = DateTimeField(null=True) + sucursal = ForeignKeyField(Sucursales, null=True) + + def __str__(self): + t = '{} {} ({})' + return t.format(self.nombre, self.apellidos, self.usuario) + + class Meta: + order_by = ('nombre', 'apellidos') + + @classmethod + def add(cls, values): + values['contraseña'] = values.pop('contra') + try: + Usuarios.create(**values) + return {'ok': True} + except Exception as e: + log.error(e) + msg = 'Ocurrio un error, consulta a soporte técnico' + return {'ok': False, 'msg': msg} + + @classmethod + def remove(cls, id): + q = Usuarios.delete().where(Usuarios.id==int(id)) + return bool(q.execute()) + + @classmethod + def edit(self, values): + # ~ print (values) + id = int(values.pop('id')) + try: + if 'contra' in values: + values['contraseña'] = values.pop('contra') + q = Usuarios.update(**values).where(Usuarios.id==id) + result = {'ok': bool(q.execute())} + except IntegrityError: + msg = 'El usuario ya existe' + result = {'ok': False, 'msg': msg} + + return result + + @classmethod + def actualizar(self, values, user): + id = int(values['id']) + v = {'0': False, '1': True} + + if values['field'] == 'es_superusuario' and not user.es_superusuario: + msg = 'Solo un super usuario puede hacer este cambio' + return {'ok': False, 'msg': msg} + + if values['field'] == 'es_activo': + q = (Usuarios + .update(**{'es_activo': v[values['value']]}) + .where(Usuarios.id==id)) + result = bool(q.execute()) + elif values['field'] == 'es_admin': + q = (Usuarios + .update(**{'es_admin': v[values['value']]}) + .where(Usuarios.id==id)) + result = bool(q.execute()) + elif values['field'] == 'es_superusuario': + q = (Usuarios + .update(**{'es_superusuario': v[values['value']]}) + .where(Usuarios.id==id)) + result = bool(q.execute()) + + return {'ok': result} + + @classmethod + def get_(cls, user): + if user.es_superusuario: + rows = Usuarios.select().dicts() + else: + filters = (Usuarios.es_superusuario == False) + rows = Usuarios.select().where(filters).dicts() + for row in rows: + del row['contraseña'] + return tuple(rows) + + +class Registro(BaseModel): + usuario = TextField() + accion = TextField(default='') + tabla = TextField(default='') + fecha = DateTimeField(default=util.now) + + def __str__(self): + t = '{} {}-{} ({})' + return t.format(self.usuario, self.accion, self.tabla, self.fecha) + + class Meta: + order_by = ('usuario', 'fecha') + + @classmethod + def add(cls, values): + try: + Registro.create(**values) + return + except: + return + + +class SATRegimenes(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + fisica = BooleanField(default=False) + moral = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return '{} ({})'.format(self.name, self.key) + + @classmethod + def get_(cls, ids): + if isinstance(ids, int): + ids = [ids] + return SATRegimenes.select().where(SATRegimenes.id.in_(ids)) + + @classmethod + def get_activos(cls, rfc): + where = ((SATRegimenes.activo==True) & (SATRegimenes.fisica==True)) + if (len(rfc) == 12): + where = ((SATRegimenes.activo==True) & (SATRegimenes.moral==True)) + + rows = (SATRegimenes + .select( + SATRegimenes.id, + SATRegimenes.name.alias('value')) + .where(where) + .dicts() + ) + return tuple(rows) + + +class Emisor(BaseModel): + rfc = TextField(unique=True) + nombre = TextField(default='') + nombre_comercial = TextField(default='') + calle = TextField(default='') + no_exterior = TextField(default='') + no_interior = TextField(default='') + colonia = TextField(default='') + municipio = TextField(default='') + estado = TextField(default='') + pais = TextField(default='México') + codigo_postal = TextField(default='') + cp_expedicion = TextField(default='') + es_moral = BooleanField(default=False) + es_ong = BooleanField(default=False) + es_escuela = BooleanField(default=False) + autorizacion = TextField(default='') + fecha_autorizacion = DateField(null=True) + fecha_dof = DateField(null=True) + telefono = TextField(default='') + correo = TextField(default='') + web = TextField(default='') + curp = TextField(default='') + correo_timbrado = TextField(default='') + token_timbrado = TextField(default='') + token_soporte = TextField(default='') + logo = TextField(default='') + registro_patronal = TextField(default='') + regimenes = ManyToManyField(SATRegimenes, related_name='emisores') + + def __str__(self): + t = '{} ({})' + return t.format(self.nombre, self.rfc) + + class Meta: + order_by = ('nombre',) + + @classmethod + def get_(cls, rfc): + regimenes = SATRegimenes.get_activos(rfc) + row = {'regimenes': regimenes} + + obj = Emisor.select().where(Emisor.rfc==rfc) + if bool(obj): + obj = obj[0] + row['emisor'] = { + 'emisor_rfc': obj.rfc, + 'emisor_curp': obj.curp, + 'emisor_nombre': obj.nombre, + 'emisor_cp': obj.codigo_postal, + 'emisor_cp2': obj.cp_expedicion, + 'emisor_calle': obj.calle, + 'emisor_no_exterior': obj.no_exterior, + 'emisor_no_interior': obj.no_interior, + 'emisor_colonia': obj.colonia, + 'emisor_municipio': obj.municipio, + 'emisor_estado': obj.estado, + 'emisor_pais': obj.pais, + 'emisor_logo': obj.logo, + 'emisor_nombre_comercial': obj.nombre_comercial, + 'emisor_telefono': obj.telefono, + 'emisor_correo': obj.correo, + 'emisor_web': obj.web, + 'es_escuela': obj.es_escuela, + 'es_ong': obj.es_ong, + 'ong_autorizacion': obj.autorizacion, + 'ong_fecha': obj.fecha_autorizacion, + 'ong_fecha_dof': obj.fecha_dof, + 'correo_timbrado': obj.correo_timbrado, + 'token_timbrado': obj.token_timbrado, + 'token_soporte': obj.token_soporte, + 'emisor_registro_patronal': obj.registro_patronal, + 'regimenes': [row.id for row in obj.regimenes] + } + else: + row['emisor'] = {'emisor_rfc': rfc} + + return {'ok': True, 'row': row} + + @classmethod + def get_auth(cls): + try: + obj = Emisor.select()[0] + data = { + 'RFC': obj.rfc, + 'USER': obj.correo_timbrado, + 'PASS': obj.token_timbrado, + 'REPO': obj.token_soporte, + } + return data + except: + return {} + + @classmethod + def get_regimenes(cls): + try: + obj = Emisor.select()[0] + except IndexError: + return () + + rows = [{'id': row.key, 'value': row.name} for row in obj.regimenes] + return tuple(rows) + + def _clean(self, values): + fields = util.clean(values) + fields['rfc'] = fields.pop('emisor_rfc') + fields['curp'] = fields.pop('emisor_curp', '') + fields['nombre'] = fields.pop('emisor_nombre') + fields['codigo_postal'] = fields.pop('emisor_cp') + fields['cp_expedicion'] = fields.pop('emisor_cp2', '') or fields['codigo_postal'] + fields['calle'] = fields.pop('emisor_calle', '') + fields['no_exterior'] = fields.pop('emisor_no_exterior', '') + fields['no_interior'] = fields.pop('emisor_no_interior', '') + fields['colonia'] = fields.pop('emisor_colonia', '') + fields['municipio'] = fields.pop('emisor_municipio', '') + fields['estado'] = fields.pop('emisor_estado', '') + fields['pais'] = fields.pop('emisor_pais', 'México') + fields['logo'] = fields.pop('emisor_logo', '') + fields['nombre_comercial'] = fields.pop('emisor_nombre_comercial', '') + fields['telefono'] = fields.pop('emisor_telefono', '') + fields['correo'] = fields.pop('emisor_correo', '') + fields['web'] = fields.pop('emisor_web', '') + fields['es_escuela'] = bool(fields['es_escuela'].replace('0', '')) + fields['es_ong'] = bool(fields['es_ong'].replace('0', '')) + fields['autorizacion'] = fields.pop('ong_autorizacion', '') + fields['fecha_autorizacion'] = fields.pop('ong_fecha', None) + fields['fecha_dof'] = fields.pop('ong_fecha_dof', None) + if len(fields['rfc']) == 12: + fields['es_moral'] = True + fields['registro_patronal'] = fields.pop('emisor_registro_patronal', '') + fields['regimenes'] = SATRegimenes.get_( + util.loads(fields['regimenes'])) + return fields + + @classmethod + def add(cls, values): + fields = cls._clean(cls, values) + obj, created = Emisor.get_or_create(rfc=fields['rfc']) + obj.regimenes = fields.pop('regimenes') + q = Emisor.update(**fields).where(Emisor.id==obj.id) + return {'ok': bool(q.execute())} + + +class Certificado(BaseModel): + key = BlobField(null=True) + key_enc = TextField(default='') + cer = BlobField(null=True) + cer_pem = TextField(default='') + cer_txt = TextField(default='') + p12 = BlobField(null=True) + serie = TextField(default='') + rfc = TextField(default='') + desde = DateTimeField(null=True) + hasta = DateTimeField(null=True) + + def __str__(self): + return self.serie + + @classmethod + def get_data(cls): + obj = cls.get_(cls) + row = { + 'cert_rfc': obj.rfc, + 'cert_serie': obj.serie, + 'cert_desde': obj.desde, + 'cert_hasta': obj.hasta, + } + return row + + def get_(cls): + return Certificado.select()[0] + + @classmethod + def add(cls, file_obj): + if file_obj.filename.endswith('key'): + path_key = util.save_temp(file_obj.file.read()) + Configuracion.add({'path_key': path_key}) + elif file_obj.filename.endswith('cer'): + path_cer = util.save_temp(file_obj.file.read()) + Configuracion.add({'path_cer': path_cer}) + return {'status': 'server'} + + @classmethod + def validate(cls, values, session): + row = {} + result = False + + obj = cls.get_(cls) + paths = Configuracion.get_({'fields': 'path_cer'}) + cert = util.Certificado(paths) + auth = Emisor.get_auth() + data = cert.validate(values['contra'], session['rfc'], auth) + if data: + msg = 'Certificado guardado correctamente' + q = Certificado.update(**data).where(Certificado.id==obj.id) + if q.execute(): + result = True + row = { + 'cert_rfc': data['rfc'], + 'cert_serie': data['serie'], + 'cert_desde': data['desde'], + 'cert_hasta': data['hasta'], + } + else: + msg = cert.error + + Configuracion.add({'path_key': ''}) + Configuracion.add({'path_cer': ''}) + + return {'ok': result, 'msg': msg, 'data': row} + + + +class Folios(BaseModel): + serie = TextField(unique=True) + inicio = IntegerField(default=1) + default = BooleanField(default=False) + usarcon = TextField(default='') + + class Meta: + order_by = ('-default', 'serie', 'inicio') + indexes = ( + (('serie', 'inicio'), True), + ) + + @classmethod + def get_default(cls): + folio = Folios.select()[0] + return folio.serie + + @classmethod + def get_all(cls): + rows = (Folios + .select( + Folios.id, + Folios.serie.alias('value'), + Folios.usarcon, + ) + .dicts() + ) + return tuple(rows) + + @classmethod + def get_(cls): + rows = (Folios + .select( + Folios.id, + SQL(" '-' AS delete"), + Folios.serie, + Folios.inicio, + case(Folios.usarcon, ( + ('I', 'Ingreso'), + ('E', 'Egreso'), + ('T', 'Traslado'), + ), 'Todos').alias('usarcon'), + case(Folios.default, ( + (True, 'Si'), + (False, 'No'), + )).alias('pre') + ) + .dicts() + ) + return tuple(rows) + + @classmethod + def add(cls, values): + uc = { + '': 'Todos', + 'I': 'Ingreso', + 'E': 'Egreso', + 'T': 'Traslado', + } + pre = { + True: 'Si', + False: 'No', + } + + if 'default' in values: + values['default'] = True + try: + obj = Folios.create(**values) + except IntegrityError: + msg = 'Serie ya existe' + return {'ok': False, 'msg': msg} + row = { + 'id': obj.id, + 'delete' : '-', + 'serie' : obj.serie, + 'inicio' : obj.inicio, + 'usarcon' : uc[obj.usarcon], + 'pre' : pre[obj.default], + } + return {'ok': True, 'row': row} + + @classmethod + def remove(cls, id): + q = Folios.delete().where(Folios.id==id) + return bool(q.execute()) + + +class Categorias(BaseModel): + categoria = TextField() + padre = ForeignKeyField('self', null=True, related_name='hijos') + + class Meta: + order_by = ('categoria',) + indexes = ( + (('categoria', 'padre'), True), + ) + + @classmethod + def exists(cls, filters): + return Categorias.select().where(filters).exists() + + @classmethod + def get_by_id(cls, id): + try: + return Categorias.get(Categorias.id==id) + except: + return None + + @classmethod + def get_all(cls): + rows = (Categorias.select( + Categorias.id, + Categorias.categoria.alias('value'), + Categorias.padre.alias('parent_id')) + ).dicts() + for row in rows: + if row['parent_id'] is None: + row['parent_id'] = 0 + return tuple(rows) + + +class CondicionesPago(BaseModel): + condicion = TextField(unique=True) + + class Meta: + order_by = ('condicion',) + + def __str__(self): + return self.condicion + + @classmethod + def get_(cls): + q = CondicionesPago.select(CondicionesPago.condicion).tuples() + data = [r[0] for r in q] + return data + + @classmethod + def get_or(cls, value): + if value is None: + return value + obj, _ = CondicionesPago.get_or_create(condicion=value) + return obj + + +class SATUnidades(BaseModel): + key = TextField(unique=True, index=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name') + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return '{} ({})'.format(self.name, self.key) + + @classmethod + def get_by_name(self, name): + try: + return SATUnidades.get(SATUnidades.name==name) + except: + return None + + @classmethod + def get_(self): + rows = SATUnidades.select().dicts() + return tuple(rows) + + @classmethod + def add(self, values): + try: + SATUnidades.create(**values) + return {'ok': True} + except: + return {'ok': False} + + @classmethod + def actualizar(self, values): + id = int(values['id']) + if values['field'] == 'activo': + v = {'0': False, '1': True} + q = (SATUnidades + .update(**{'activo': v[values['value']]}) + .where(SATUnidades.id==id)) + result = bool(q.execute()) + elif values['field'] == 'default': + q = SATUnidades.update(**{'default': False}) + q.execute() + + v = {'false': False, 'true': True} + q = (SATUnidades + .update(**{'default': v[values['value']]}) + .where(SATUnidades.id==id)) + result = bool(q.execute()) + + return {'ok': result} + + @classmethod + def get_default(cls): + obj = SATUnidades.select()[0] + if obj.default: + return obj.id + return 0 + + @classmethod + def get_activos(cls): + rows = (SATUnidades + .select( + SATUnidades.id, + SATUnidades.name.alias('value')) + .where(SATUnidades.activo==True) + .dicts() + ) + return tuple(rows) + + @classmethod + def remove(cls, id): + with database_proxy.transaction(): + try: + q = SATUnidades.delete().where(SATUnidades.id==id) + return bool(q.execute()) + except IntegrityError: + return False + + +class SATFormaPago(BaseModel): + key = TextField(unique=True, index=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Forma de pago: ({}) {}'.format(self.key, self.name) + + @classmethod + def get_(self): + rows = SATFormaPago.select().dicts() + return tuple(rows) + + @classmethod + def get_by_key(cls, key): + return SATFormaPago.get(SATFormaPago.key==key) + + @classmethod + def get_activos(cls, values): + field = SATFormaPago.id + if values: + field = SATFormaPago.key.alias('id') + rows = (SATFormaPago + .select(field, SATFormaPago.name.alias('value')) + .where(SATFormaPago.activo==True) + .dicts() + ) + return tuple(rows) + + @classmethod + def actualizar(self, values): + id = int(values['id']) + if values['field'] == 'activo': + v = {'0': False, '1': True} + q = (SATFormaPago + .update(**{'activo': v[values['value']]}) + .where(SATFormaPago.id==id)) + result = bool(q.execute()) + elif values['field'] == 'default': + q = SATFormaPago.update(**{'default': False}) + q.execute() + + v = {'false': False, 'true': True} + q = (SATFormaPago + .update(**{'default': v[values['value']]}) + .where(SATFormaPago.id==id)) + result = bool(q.execute()) + + return {'ok': result} + + +class SATAduanas(BaseModel): + key = TextField(unique=True, index=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'name'), True), + ) + + +class SATMonedas(BaseModel): + key = TextField(unique=True, index=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Moneda: ({}) {}'.format(self.key, self.name) + + @classmethod + def add(self, values): + try: + SATMonedas.create(**values) + return {'ok': True} + except: + return {'ok': False} + + @classmethod + def get_(self): + rows = SATMonedas.select().dicts() + return tuple(rows) + + @classmethod + def get_activos(cls): + rows = (SATMonedas + .select( + SATMonedas.key.alias('id'), + SATMonedas.name.alias('value')) + .where(SATMonedas.activo==True) + .dicts() + ) + return tuple(rows) + + @classmethod + def get_activos_by_id(cls): + rows = (SATMonedas + .select( + SATMonedas.id, + SATMonedas.name.alias('value')) + .where(SATMonedas.activo==True) + .dicts() + ) + return tuple(rows) + + @classmethod + def actualizar(self, values): + id = int(values['id']) + if values['field'] == 'activo': + v = {'0': False, '1': True} + q = (SATMonedas + .update(**{'activo': v[values['value']]}) + .where(SATMonedas.id==id)) + result = bool(q.execute()) + elif values['field'] == 'default': + q = SATMonedas.update(**{'default': False}) + q.execute() + + v = {'false': False, 'true': True} + q = (SATMonedas + .update(**{'default': v[values['value']]}) + .where(SATMonedas.id==id)) + result = bool(q.execute()) + + return {'ok': result} + + +class SATImpuestos(BaseModel): + key = TextField(index=True) + name = TextField(default='', index=True) + factor = TextField(default='T') + tipo = TextField(default='T') + tasa = DecimalField(default=0.0, decimal_places=6, auto_round=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'factor', 'tipo', 'tasa'), True), + ) + + @classmethod + def get_o_crea(self, values): + obj, _ = SATImpuestos.get_or_create(**values) + return obj + + @classmethod + def add(self, values): + tasa = float(values['tasa']) + tipo = 'T' + if tasa < 0: + tipo = 'R' + + row = { + 'key': IMPUESTOS.get(values['impuesto']), + 'name': values['impuesto'], + 'tipo': tipo, + 'tasa': abs(tasa), + } + + try: + obj = SATImpuestos.create(**row) + row['id'] = obj.id + row['delete'] = '-' + return {'ok': True, 'row': row} + except IntegrityError: + return {'ok': False, 'msg': 'El impuesto ya existe'} + + @classmethod + def remove(cls, id): + with database_proxy.transaction(): + try: + q = SATImpuestos.delete().where(SATImpuestos.id==id) + return bool(q.execute()) + except IntegrityError: + return False + + @classmethod + def get_(self): + rows = (SATImpuestos.select( + SATImpuestos.id, + SQL(" '-' AS delete"), + SATImpuestos.name, + SATImpuestos.tipo, + SATImpuestos.tasa, + SATImpuestos.activo, + SATImpuestos.default) + .dicts() + ) + return tuple(rows) + + @classmethod + def get_activos(self): + rows = SATImpuestos.select().where(SATImpuestos.activo==True).dicts() + return tuple(rows) + + @classmethod + def actualizar(self, values): + id = int(values['id']) + if values['field'] == 'activo': + v = {'0': False, '1': True} + q = (SATImpuestos + .update(**{'activo': v[values['value']]}) + .where(SATImpuestos.id==id)) + result = bool(q.execute()) + elif values['field'] == 'default': + q = SATImpuestos.update(**{'default': False}) + q.execute() + + v = {'false': False, 'true': True} + q = (SATImpuestos + .update(**{'default': v[values['value']]}) + .where(SATImpuestos.id==id)) + result = bool(q.execute()) + + return {'ok': result} + + +class SATTipoRelacion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de relación: ({}) {}'.format(self.key, self.name) + + @classmethod + def get_activos(cls, values): + field = SATTipoRelacion.id + if values: + field = SATTipoRelacion.key.alias('id') + rows = (SATTipoRelacion + .select(field, SATTipoRelacion.name.alias('value')) + .where(SATTipoRelacion.activo==True) + .dicts() + ) + return ({'id': '-', 'value': ''},) + tuple(rows) + + + +class SATBancos(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(index=True) + razon_social = TextField(default='') + rfc = TextField(default='') + activo = BooleanField(default=False) + + class Meta: + order_by = ('-activo', 'name') + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Banco: {} ({})'.format(self.name, self.key) + + @classmethod + def get_(cls): + rows = SATBancos.select().dicts() + return tuple(rows) + + @classmethod + def get_activos_by_id(cls): + rows = (SATBancos + .select( + SATBancos.id, + SATBancos.name.alias('value')) + .where(SATBancos.activo==True) + .dicts() + ) + return tuple(rows) + + @classmethod + def actualizar(cls, values): + id = int(values['id']) + if values['field'] == 'activo': + v = {'0': False, '1': True} + q = (SATBancos + .update(**{'activo': v[values['value']]}) + .where(SATBancos.id==id)) + result = bool(q.execute()) + return {'ok': result} + + @classmethod + def get_by_key(cls, key): + if not key: + return + try: + obj = SATBancos.get(SATBancos.key==key) + return obj + except SATBancos.DoesNotExist: + msg = 'SATBancos no existe: {}'.format(key) + log.error(msg) + return + + +class SATNivelesEducativos(BaseModel): + name = TextField(index=True) + + class Meta: + order_by = ('name',) + + def __str__(self): + return self.name + + @classmethod + def get_by(cls): + rows = SATNivelesEducativos.select( + SATNivelesEducativos.name).tuples() + return tuple([r[0] for r in rows]) + + +class NivelesEducativos(BaseModel): + nombre = TextField() + autorizacion = TextField(default='') + + class Meta: + order_by = ('nombre',) + indexes = ( + (('nombre', 'autorizacion'), True), + ) + + def __str__(self): + return '{} ({})'.format(self.nombre, self.autorizacion) + + @classmethod + def get_all(cls): + rows = NivelesEducativos.select().dicts() + return tuple(rows) + + def _add_group(self, obj): + Grupos.get_or_create(**{'nivel': obj}) + return + + @classmethod + def add(cls, values): + try: + obj = NivelesEducativos.create(**values) + # Revisar + cls._add_group(cls, obj) + result = {'ok': True} + except IntegrityError: + msg = 'Nivel Educativo existente' + result = {'ok': False, 'msg': msg} + return result + + @classmethod + def remove(cls, id): + q = NivelesEducativos.delete().where(NivelesEducativos.id==int(id)) + return bool(q.execute()) + + +class Grupos(BaseModel): + nivel = ForeignKeyField(NivelesEducativos) + grado = TextField(default='') + nombre = TextField(default='') + + class Meta: + order_by = ('nivel', 'grado', 'nombre') + indexes = ( + (('nivel', 'grado', 'nombre'), True), + ) + + def __str__(self): + return '{} {} {}'.format(self.nivel.nombre, self.grado, self.nombre) + + @classmethod + def get_by(cls, values): + rows = (Grupos.select( + Grupos.id.alias('id'), + NivelesEducativos.nombre.alias('value')) + .join(NivelesEducativos) + .switch(Grupos) + .dicts() + ) + return tuple(rows) + + +class CuentasBanco(BaseModel): + de_emisor = BooleanField(default=False) + activa = BooleanField(default=True) + nombre = TextField() + banco = ForeignKeyField(SATBancos) + fecha_apertura = DateField(null=True) + cuenta = TextField(default='') + clabe = TextField(default='') + moneda = ForeignKeyField(SATMonedas) + saldo_inicial = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('nombre',) + indexes = ( + (('banco', 'cuenta'), True), + ) + + def __str__(self): + return '{} ({})'.format(self.banco.name, self.cuenta[-4:]) + + @classmethod + def actualizar_saldo(cls, id, saldo): + fields = {'saldo': saldo} + q = CuentasBanco.update(**fields).where(CuentasBanco.id==id) + return bool(q.execute()) + + @classmethod + def get_saldo(cls, id): + try: + obj = CuentasBanco.get(CuentasBanco.id==id) + return obj.saldo + except CuentasBanco.DoesNotExist: + return 0 + + @classmethod + def remove(cls, id): + try: + with database_proxy.atomic() as txn: + q = MovimientosBanco.delete().where(MovimientosBanco.cuenta==id) + q.execute() + q = CuentasBanco.delete().where(CuentasBanco.id==id) + q.execute() + return True + except: + return False + + @classmethod + def get_years(cls): + data = [{'id': -1, 'value': 'Todos'}] + year1 = (CuentasBanco + .select(fn.Min(CuentasBanco.fecha_apertura.year)) + .where(CuentasBanco.de_emisor==True, CuentasBanco.activa==True) + .group_by(CuentasBanco.fecha_apertura.year) + .order_by(CuentasBanco.fecha_apertura.year) + .scalar() + ) + + if year1: + year2 = util.now().year + 1 + data += [{'id': y, 'value': y} for y in range(int(year1), year2)] + + return data + + @classmethod + def get_(cls, values): + if values['tipo'] == '1': + rows = (CuentasBanco + .select() + .where(CuentasBanco.de_emisor==True, CuentasBanco.activa==True) + ) + if not (len(rows)): + return {'ok': False} + + first = rows[0] + rows = [{'id': r.id, 'value': '{} ({})'.format( + r.banco.name, r.cuenta[-4:])} for r in rows] + data = { + 'ok': True, + 'rows': tuple(rows), + 'moneda': first.moneda.name, + 'saldo': first.saldo, + } + return data + + return + + @classmethod + def emisor(cls): + rows = (CuentasBanco + .select( + CuentasBanco.id, + CuentasBanco.activa, + CuentasBanco.nombre, + SATBancos.name.alias('banco'), + CuentasBanco.fecha_apertura, + CuentasBanco.cuenta, + CuentasBanco.clabe, + SATMonedas.name.alias('moneda'), + CuentasBanco.saldo + ) + .join(SATBancos).switch(CuentasBanco) + .join(SATMonedas).switch(CuentasBanco) + .where(CuentasBanco.de_emisor==True).dicts() + ) + return tuple(rows) + + @classmethod + def add(cls, values): + w = '37137137137137137' + dv = str( + (10 - + sum([(int(v) * int(values['clabe'][i])) % 10 for i, v in enumerate(w)]) + % 10) % 10) + if dv != values['clabe'][-1]: + msg = 'Digito de control de la CLABE es incorrecto' + return {'ok': False, 'msg': msg} + + fecha_deposito = values.pop('fecha_deposito', None) + + with database_proxy.transaction(): + try: + obj = CuentasBanco.create(**values) + except IntegrityError: + msg = 'Esta cuenta ya existe' + return {'ok': False, 'msg': msg} + + nuevo_mov= { + 'cuenta': obj.id, + 'fecha': fecha_deposito, + 'descripcion': 'Saldo inicial', + 'forma_pago': SATFormaPago.get_by_key('99'), + 'deposito': values['saldo'], + 'saldo': values['saldo'], + } + MovimientosBanco.add(nuevo_mov) + + rows = (CuentasBanco + .select( + CuentasBanco.id, + CuentasBanco.activa, + CuentasBanco.nombre, + SATBancos.name.alias('banco'), + CuentasBanco.fecha_apertura, + CuentasBanco.cuenta, + CuentasBanco.clabe, + SATMonedas.name.alias('moneda'), + CuentasBanco.saldo + ) + .join(SATBancos).switch(CuentasBanco) + .join(SATMonedas).switch(CuentasBanco) + .where(CuentasBanco.id==obj.id).dicts() + ) + data = {'ok': True, 'row': rows[0]} + return data + + +class MovimientosBanco(BaseModel): + cuenta = ForeignKeyField(CuentasBanco) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + descripcion = TextField(default='') + forma_pago = ForeignKeyField(SATFormaPago) + retiro = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + deposito = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + cancelado = BooleanField(default=False) + conciliado = BooleanField(default=False) + moneda = TextField(default='MXN') # Complemento de pagos + tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6, + auto_round=True) + numero_operacion = TextField(default='') + origen_rfc = TextField(default='') + origen_nombre = TextField(default='') + origen_cuenta = TextField(default='') + destino_rfc = TextField(default='') + destino_cuenta = TextField(default='') + tipo_cadena_pago = TextField(default='') + certificado_pago = TextField(default='') + cadena_pago = TextField(default='') + sello_pago = TextField(default='') + + class Meta: + order_by = ('fecha',) + + def _ultimo_saldo(self, cuenta, fecha): + query = (MovimientosBanco + .select() + .where( + (MovimientosBanco.cuenta==cuenta) & + (MovimientosBanco.fecharow.fecha) & + (MovimientosBanco.cancelado==False)) + ) + + saldo = round(Decimal(row.saldo), DECIMALES) + for mov in query: + mov.saldo = saldo + mov.deposito - mov.retiro + mov.save() + saldo = mov.saldo + CuentasBanco.actualizar_saldo(row.cuenta, saldo) + return saldo + + @classmethod + def add(cls, values): + ids = values.pop('ids', '') + actualizar = False + if 'saldo' in values: + saldo = values['saldo'] + else: + actualizar = True + hora = values.pop('hora') + values['fecha'] = '{}T{}'.format(values['fecha'][:10], hora) + values['cuenta'] = int(values['cuenta']) + values['retiro'] = util.get_float(values['retiro']) + values['deposito'] = util.get_float(values['deposito']) + values['forma_pago'] = int(values['forma_pago']) + + ultimo_saldo = cls._ultimo_saldo( + cls, values['cuenta'], values['fecha']) + values['saldo'] = \ + ultimo_saldo - values['retiro'] + values['deposito'] + + with database_proxy.transaction(): + try: + obj = MovimientosBanco.create(**values) + except IntegrityError: + msg = 'Este movimiento ya existe' + return {'ok': False, 'msg': msg} + + if actualizar: + saldo = cls._actualizar_saldos(cls, obj) + if ids: + FacturasPagos.add(obj, util.loads(ids)) + + return {'ok': True, 'saldo': saldo} + + @classmethod + def remove(cls, id): + try: + obj = MovimientosBanco.get(MovimientosBanco.id==id) + except MovimientosBanco.DoesNotExist: + return False + + if obj.conciliado or obj.cancelado: + return False + + with database_proxy.transaction(): + obj.cancelado = True + obj.save() + FacturasPagos.cancelar(obj) + + obj = cls._movimiento_anterior(cls, obj.cuenta, obj.fecha) + cls._actualizar_saldos(cls, obj) + + return True + + @classmethod + def con(cls, id): + cant = (MovimientosBanco + .select(MovimientosBanco.id) + .where(MovimientosBanco.cuenta==id) + .count() + ) + if cant > 2: + return {'ok': True} + + return {'ok': False} + + + @classmethod + def get_(cls, values): + cuenta = int(values['cuenta']) + if 'fechas' in values: + rango = values['fechas'] + fd = (MovimientosBanco.fecha.between( + util.get_date(rango['start']), + util.get_date(rango['end'], True))) + filtros = (fd & + (MovimientosBanco.cuenta==cuenta) & + (MovimientosBanco.cancelado==False) + ) + else: + year = int(values['year']) + mes = int(values['mes']) + if year == -1: + fy = (MovimientosBanco.fecha.year > 0) + else: + fy = (MovimientosBanco.fecha.year == year) + if mes == -1: + fm = (MovimientosBanco.fecha.month > 0) + else: + fm = (MovimientosBanco.fecha.month == mes) + filtros = (fy & fm & + (MovimientosBanco.cuenta==cuenta) & + (MovimientosBanco.cancelado==False) + ) + + rows = tuple(MovimientosBanco + .select( + MovimientosBanco.id, + MovimientosBanco.fecha, + MovimientosBanco.numero_operacion, + MovimientosBanco.descripcion, + MovimientosBanco.retiro, + MovimientosBanco.deposito, + MovimientosBanco.saldo) + .where(filtros) + .dicts() + ) + + return {'ok': True, 'rows': rows} + + +class CfdiPagos(BaseModel): + movimiento = ForeignKeyField(MovimientosBanco) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + fecha_timbrado = DateTimeField(null=True) + xml = TextField(default='') + uuid = UUIDField(null=True) + estatus = TextField(default='Guardado') + estatus_sat = TextField(default='') + notas = TextField(default='') + cancelado = BooleanField(default=False) + + class Meta: + order_by = ('movimiento',) + + +class SATUsoCfdi(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + fisica = BooleanField(default=True) + moral = BooleanField(default=False) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Uso del CFDI: {} ({})'.format(self.name, self.key) + + @classmethod + def actualizar(self, values): + id = int(values['id']) + if values['field'] == 'activo': + v = {'0': False, '1': True} + q = (SATUsoCfdi + .update(**{'activo': v[values['value']]}) + .where(SATUsoCfdi.id==id)) + result = bool(q.execute()) + return {'ok': result} + + @classmethod + def get_all(self): + rows = SATUsoCfdi.select().dicts() + return tuple(rows) + + @classmethod + def get_id(self, key): + if key is None: + return + return SATUsoCfdi.get(SATUsoCfdi.key==key).id + + @classmethod + def get_key(self, id): + if id is None: + return + return SATUsoCfdi.get(SATUsoCfdi.id==id).key + + @classmethod + def get_activos(cls): + rows = (SATUsoCfdi + .select( + SATUsoCfdi.key.alias('id'), + SATUsoCfdi.name.alias('value'), + SATUsoCfdi.fisica, + SATUsoCfdi.moral, + ) + .where(SATUsoCfdi.activo==True) + .dicts() + ) + return tuple(rows) + + +class SATEstados(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + pais = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name', 'pais'), True), + ) + + def __str__(self): + return 'Estado: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATEstados.get(SATEstados.key==key) + return obj + except SATEstados.DoesNotExist: + msg = 'SATEstados no existe: {}'.format(key) + log.error(msg) + return + + +class SATOrigenRecurso(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Origen Recurso: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATOrigenRecurso.get(SATOrigenRecurso.key==key) + return obj + except SATOrigenRecurso.DoesNotExist: + msg = 'SATOrigenRecurso no existe: {}'.format(key) + log.error(msg) + return + + +class SATPeriodicidadPago(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Periodicidad de Pago: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATPeriodicidadPago.get(SATPeriodicidadPago.key==key) + return obj + except SATPeriodicidadPago.DoesNotExist: + msg = 'SATPeriodicidadPago no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoContrato(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Contrato: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoContrato.get(SATTipoContrato.key==key) + return obj + except SATTipoContrato.DoesNotExist: + msg = 'SATTipoContrato no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoDeduccion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Deducción: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoDeduccion.get(SATTipoDeduccion.key==key) + return obj + except SATTipoDeduccion.DoesNotExist: + msg = 'SATTipoDeduccion no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoHoras(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Horas: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoHoras.get(SATTipoHoras.key==key) + return obj + except SATTipoHoras.DoesNotExist: + msg = 'SATTipoHoras no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoIncapacidad(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Incapacidad: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoIncapacidad.get(SATTipoIncapacidad.key==key) + return obj + except SATTipoIncapacidad.DoesNotExist: + msg = 'SATTipoIncapacidad no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoJornada(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Jornada: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoJornada.get(SATTipoJornada.key==key) + return obj + except SATTipoJornada.DoesNotExist: + msg = 'SATTipoJornada no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoNomina(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Nómina: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoNomina.get(SATTipoNomina.key==key) + return obj + except SATTipoNomina.DoesNotExist: + msg = 'SATTipoNomina no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoOtroPago(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Otro Pago: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoOtroPago.get(SATTipoOtroPago.key==key) + return obj + except SATTipoOtroPago.DoesNotExist: + msg = 'SATTipoOtroPago no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoPercepcion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Percepción: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoPercepcion.get(SATTipoPercepcion.key==key) + return obj + except SATTipoPercepcion.DoesNotExist: + msg = 'SATTipoPercepcion no existe: {}'.format(key) + log.error(msg) + return + + +class SATTipoRegimen(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Regimen de contratación: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoRegimen.get(SATTipoRegimen.key==key) + return obj + except SATTipoRegimen.DoesNotExist: + msg = 'SATTipoRegimen no existe: {}'.format(key) + log.error(msg) + return + + +class SATRiesgoPuesto(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Riesgo Puesto: {} ({})'.format(self.name, self.key) + + @classmethod + def get_by_key(cls, key): + try: + obj = SATRiesgoPuesto.get(SATRiesgoPuesto.key==key) + return obj + except SATRiesgoPuesto.DoesNotExist: + msg = 'SATRiesgoPuesto no existe: {}'.format(key) + log.error(msg) + return + + +class TipoCambio(BaseModel): + dia = DateField(default=util.now) + moneda = ForeignKeyField(SATMonedas) + tipo_cambio = DecimalField(max_digits=15, decimal_places=6, auto_round=True) + + class Meta: + order_by = ('-dia',) + + +class Addendas(BaseModel): + nombre = TextField(unique=True) + addenda = TextField() + + class Meta: + order_by = ('nombre',) + + +class Socios(BaseModel): + tipo_persona = IntegerField(default=1) + rfc = TextField(index=True) + nombre = TextField(index=True) + slug = TextField(default='') + nombre_comercial = TextField(index=True, default='') + calle = TextField(default='') + no_exterior = TextField(default='') + no_interior = TextField(default='') + colonia = TextField(default='') + municipio = TextField(default='') + estado = TextField(default='') + pais = TextField(default='') + codigo_postal = TextField(default='') + notas = TextField(default='') + telefonos = TextField(default='') + es_activo = BooleanField(default=True) + es_ong = BooleanField(default=False) + fecha_alta = DateField(default=util.now) + dias_pago = IntegerField(default=0) + dias_habiles = BooleanField(default=False) + es_cliente = BooleanField(default=False) + es_proveedor = BooleanField(default=False) + cuenta_cliente = TextField(default='') + cuenta_proveedor = TextField(default='') + saldo_cliente = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + saldo_proveedor = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + web = TextField(default='') + correo_facturas = TextField(default='') + forma_pago = ForeignKeyField(SATFormaPago, null=True) + condicion_pago = ForeignKeyField(CondicionesPago, null=True) + addenda = ForeignKeyField(Addendas, null=True) + uso_cfdi = ForeignKeyField(SATUsoCfdi, null=True) + tags = ManyToManyField(Tags, related_name='socios_tags') + + def __str__(self): + t = '{} ({})' + return t.format(self.nombre, self.rfc) + + class Meta: + order_by = ('nombre',) + indexes = ( + (('rfc', 'slug'), True), + ) + + def _clean(self, values): + fields = util.clean(values) + fields['rfc'] = fields['rfc'].upper() + fields['nombre'] = util.spaces(fields['nombre']) + fields['slug'] = util.to_slug(fields['nombre']) + uso_cfdi = fields.pop('uso_cfdi_socio', None) + fields['uso_cfdi'] = SATUsoCfdi.get_id(uso_cfdi) + fields['condicion_pago'] = \ + CondicionesPago.get_or(fields.get('condicion_pago', None)) + + fb = ('dias_habiles', 'es_activo', 'es_cliente', + 'es_proveedor', 'es_ong') + for name in fb: + fields[name] = bool(fields[name].replace('0', '')) + return fields + + @classmethod + def get_(cls, values): + #~ print ('values', values) + id = values.get('id', 0) + if id: + id = int(values['id']) + row = Socios.select().where(Socios.id==id).dicts()[0] + row['uso_cfdi_socio'] = SATUsoCfdi.get_key(row.pop('uso_cfdi')) + if not row['condicion_pago'] is None: + row['condicion_pago'] = \ + str(CondicionesPago.get(id=row['condicion_pago'])) + return row + + #~ return {'data': data['rows'][:100], 'pos':0, 'total_count': 1300} + #~ start = 0 + #~ count = 0 + #~ end = 100 + #~ if values: + #~ {'start': '100', 'count': '100', 'continue': 'true'} + #~ start = int(values['start']) + #~ cont = int(values['count']) + #~ end = start + count + + total = Socios.select().count() + + rows = (Socios + .select( + Socios.id, + Socios.rfc, + Socios.nombre, + Socios.saldo_cliente) + .dicts() + ) + return {'pos': 0, 'total_count': total, 'data': tuple(rows)} + + @classmethod + def get_by_client(cls, values): + id = int(values.get('id', 0)) + if id: + row = (Socios + .select( + Socios.id, Socios.nombre, Socios.rfc, + SATFormaPago.key.alias('forma_pago'), + SATUsoCfdi.key.alias('uso_cfdi')) + .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) + .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) + .where((Socios.id==id) & (Socios.es_cliente==True)) + .dicts() + ) + if len(row): + return {'ok': True, 'row': row[0]} + return {'ok': False} + + name = values.get('name', '') + if name: + rows = (Socios + .select(Socios.id, Socios.nombre, Socios.rfc, + SATFormaPago.key.alias('forma_pago'), + SATUsoCfdi.key.alias('uso_cfdi')) + .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) + .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) + .where((Socios.es_cliente==True) & + (Socios.rfc.contains(name) | + Socios.nombre.contains(name))) + .dicts()) + return tuple(rows) + return {'ok': False} + + @classmethod + def add(cls, values): + fields = cls._clean(cls, values) + try: + obj = Socios.create(**fields) + except IntegrityError as e: + msg = 'Ya existe el RFC y Razón Social' + data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} + return data + + #~ ToDo Agregar tags + + row = { + 'id': obj.id, + 'rfc': obj.rfc, + 'nombre': obj.nombre, + 'saldo_cliente': obj.saldo_cliente, + } + data = {'ok': True, 'row': row, 'new': True} + return data + + @classmethod + def actualizar(cls, values, id): + fields = cls._clean(cls, values) + try: + q = Socios.update(**fields).where(Socios.id==id) + q.execute() + except IntegrityError: + msg = 'Ya existe el RFC y Razón Social' + data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} + return data + + row = { + 'id': id, + 'rfc': fields['rfc'], + 'nombre': fields['nombre'], + } + data = {'ok': True, 'row': row, 'new': False} + return data + + @classmethod + def remove(cls, id): + count = (Facturas + .select(fn.COUNT(Facturas.id)).join(Socios) + .where(Socios.id==id) + .count()) + if count: + return False + + q = Socios.delete().where(Socios.id==id) + return bool(q.execute()) + + +class Contactos(BaseModel): + socio = ForeignKeyField(Socios) + titulo = ForeignKeyField(TipoTitulo) + foto = TextField(default='') + nombre = TextField(index=True) + paterno = TextField(index=True) + materno = TextField(default='') + fecha_nacimiento = DateField(null=True) + notas = TextField(default='') + + class Meta: + order_by = ('socio', 'nombre') + indexes = ( + (('socio', 'nombre', 'paterno', 'materno'), True), + ) + + +class ContactoDirecciones(BaseModel): + contacto = ForeignKeyField(Contactos) + tipo = ForeignKeyField(TipoDireccion) + direccion = TextField() + + class Meta: + order_by = ('contacto',) + indexes = ( + (('contacto', 'tipo', 'direccion'), True), + ) + + +class ContactoTelefonos(BaseModel): + contacto = ForeignKeyField(Contactos) + tipo = ForeignKeyField(TipoTelefono) + telefono = TextField() + + class Meta: + order_by = ('contacto',) + indexes = ( + (('contacto', 'tipo', 'telefono'), True), + ) + + +class ContactoCorreos(BaseModel): + contacto = ForeignKeyField(Contactos) + tipo = ForeignKeyField(TipoCorreo) + correo = TextField() + + class Meta: + order_by = ('contacto',) + indexes = ( + (('contacto', 'tipo', 'correo'), True), + ) + + +class Alumnos(BaseModel): + rfc = TextField(null=True) + curp = TextField(index=True, unique=True) + foto = TextField(default='') + nombre = TextField(index=True) + paterno = TextField(index=True) + materno = TextField(default='') + calle = TextField(default='') + no_exterior = TextField(default='') + no_interior = TextField(default='') + colonia = TextField(default='') + municipio = TextField(default='') + estado = TextField(default='') + pais = TextField(default='') + codigo_postal = TextField(default='') + notas = TextField(default='') + telefonos = TextField(default='') + correos = TextField(default='') + es_activo = BooleanField(default=True) + fecha_alta = DateField(default=util.now) + fecha_nacimiento = DateField(null=True) + factura = ForeignKeyField(Socios, null=True) + grupo = ForeignKeyField(Grupos, null=True) + + def __str__(self): + t = '{} {} {}' + return t.format(self.nombre, self.paterno, self.materno) + + class Meta: + order_by = ('nombre', 'paterno') + + def _clean(self, values): + fields = util.clean(util.loads(values)) + fields['rfc'] = fields['rfc'].upper().strip() + fields['curp'] = fields['curp'].upper() + fields['nombre'] = util.spaces(fields['nombre']) + fields['paterno'] = util.spaces(fields['paterno']) + fields['materno'] = util.spaces(fields['materno']) + return fields + + def _get(self, where): + rows = (Alumnos + .select() + .where(where) + .dicts() + ) + return tuple(rows) + + @classmethod + def get_by_name(cls, values): + rows = () + name = values.get('name', '') + if name: + rows = (Alumnos + .select( + Alumnos.id, + Alumnos.nombre, + Alumnos.paterno, + Alumnos.materno, + Alumnos.rfc) + .where((Alumnos.es_activo==True) & + (Alumnos.nombre.contains(name) | + Alumnos.paterno.contains(name) | + Alumnos.materno.contains(name) | + Alumnos.rfc.contains(name))) + .dicts()) + rows = tuple(rows) + + return rows + + @classmethod + def get_by(cls, values): + if 'id' in values: + id = int(values['id']) + w = (Alumnos.id==id) + rows = cls._get(cls, w) + return rows[0] + + if not values: + w = None + + return cls._get(cls, w) + + @classmethod + def add(cls, values): + fields = cls._clean(cls, values) + try: + obj = Alumnos.create(**fields) + except IntegrityError as e: + msg = 'Ya existe un alumno con este CURP' + data = {'ok': False, 'msg': msg} + return data + + data = {'ok': True} + return data + + @classmethod + def actualizar(cls, values): + fields = cls._clean(cls, values) + id = int(fields.pop('id')) + try: + q = Alumnos.update(**fields).where(Alumnos.id==id) + q.execute() + except IntegrityError: + msg = 'Ya existe un Alumno con este CURP' + data = {'ok': False, 'msg': msg} + return data + + data = {'ok': True} + return data + + @classmethod + def remove(cls, id): + q = Alumnos.delete().where(Alumnos.id==id) + return bool(q.execute()) + + +class AlumnosParientes(BaseModel): + alumno = ForeignKeyField(Alumnos) + tipo_pariente = ForeignKeyField(TipoPariente) + foto = TextField(default='') + nombre = TextField(index=True) + paterno = TextField(index=True) + materno = TextField(default='') + fecha_nacimiento = DateField(null=True) + puede_recoger = BooleanField(default=False) + + class Meta: + order_by = ('alumno',) + + +class ParienteDirecciones(BaseModel): + pariente = ForeignKeyField(AlumnosParientes) + tipo = ForeignKeyField(TipoDireccion) + direccion = TextField() + + class Meta: + order_by = ('pariente',) + + +class ParienteTelefonos(BaseModel): + pariente = ForeignKeyField(AlumnosParientes) + tipo = ForeignKeyField(TipoTelefono) + telefono = TextField() + + class Meta: + order_by = ('pariente',) + + +class ParienteCorreos(BaseModel): + pariente = ForeignKeyField(AlumnosParientes) + tipo = ForeignKeyField(TipoCorreo) + correo = TextField() + + class Meta: + order_by = ('pariente',) + + +class Almacenes(BaseModel): + nombre = TextField(default='') + ubicacion = TextField(default='') + + class Meta: + order_by = ('nombre',) + + +class Productos(BaseModel): + almacen = ForeignKeyField(Almacenes, null=True) + categoria = ForeignKeyField(Categorias, null=True) + clave = TextField(unique=True, index=True) + clave_sat = TextField(default='') + descripcion = TextField(index=True) + unidad = ForeignKeyField(SATUnidades) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + ultimo_costo = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + inventario = BooleanField(default=False) + existencia = DecimalField(default=0.0, max_digits=18, decimal_places=2, + auto_round=True) + minimo = DecimalField(default=0.0, max_digits=18, decimal_places=2, + auto_round=True) + codigo_barras = TextField(default='') + cuenta_predial = TextField(default='') + es_activo = BooleanField(default=True) + impuestos = ManyToManyField(SATImpuestos, related_name='productos') + tags = ManyToManyField(Tags, related_name='productos_tags') + + class Meta: + order_by = ('descripcion',) + + @classmethod + def next_key(cls): + try: + with database_proxy.transaction(): + value = (Productos + .select(fn.Max(cast(Productos.clave, 'int')).alias('fm')) + .order_by(SQL('fm')) + .scalar()) + except Exception as e: + values = (Productos + .select(Productos.clave) + .order_by(Productos.clave) + .tuples() + ) + values = [int(v[0]) for v in values if v[0].isdigit()] + value = 0 + if values: + value = max(values) + + value = value or 0 + value += 1 + return {'value': value} + + @classmethod + def get_by_key(cls, values): + clave = values.get('key', '') + row = (Productos + .select( + Productos.id, + Productos.clave, + Productos.clave_sat, + Productos.descripcion, + SATUnidades.name.alias('unidad'), + Productos.valor_unitario, + Productos.descuento) + .join(SATUnidades).switch(Productos) + .where((Productos.es_activo==True) & + ((Productos.clave==clave) | (Productos.codigo_barras==clave))) + .dicts() + ) + if len(row): + id = row[0]['id'] + model_pt = Productos.impuestos.get_through_model() + taxes = tuple(model_pt + .select( + model_pt.productos_id.alias('product'), + model_pt.satimpuestos_id.alias('tax')) + .where(model_pt.productos_id==id).dicts()) + return {'ok': True, 'row': row[0], 'taxes': taxes} + + return {'ok': False} + + def _validate_import(self, product): + product['categoria'] = Categorias.get_by_id(int(product['categoria'])) + product['unidad'] = SATUnidades.get_by_name(product['unidad']) + product['inventario'] = bool(product['inventario']) + if not product['inventario']: + product['existencia'] = 0.0 + + impuestos = product['impuestos'].split('|') + if not impuestos: + taxes = [SATImpuestos.select().where(SATImpuestos.id==6)] + else: + taxes = [] + for i in range(0, len(impuestos), 3): + w = ( + (SATImpuestos.key == impuestos[i]) & + (SATImpuestos.name == impuestos[i+1]) & + (SATImpuestos.tasa == float(impuestos[i+2])) + ) + try: + taxes.append(SATImpuestos.get(w)) + except: + pass + + product['impuestos'] = taxes + w = (Productos.clave==product['clave']) + return product, w + + def _import(self): + emisor = Emisor.select()[0] + rows, msg = util.import_products(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + cs = 0 + np = 0 + ap = 0 + for p in rows: + data, w = self._validate_import(self, p) + if data['unidad'] is None: + msg = 'Producto: {} - No se encontró la unidad'.format( + data['clave']) + log.error(msg) + continue + + result = util.get_sat_key('productos', data['clave_sat']) + if not result['ok']: + msg = 'Producto: {} - Clave SAT incorrecta: {}'.format( + data['clave'], data['clave_sat']) + log.error(msg) + cs += 1 + continue + + # ~ print (data) + taxes = data.pop('impuestos') + try: + with database_proxy.transaction(): + if Productos.select().where(w).exists(): + q = Productos.update(**data).where(w) + q.execute() + obj = Productos.get(w) + obj.impuestos = taxes + log.info('\tProducto actualizado: {}'.format(data['clave'])) + ap += 1 + else: + obj = Productos.create(**data) + obj.impuestos = taxes + log.info('\tProducto agregado: {}'.format(data['clave'])) + np += 1 + except Exception as e: + msg = 'Error al importar producto: {}'.format(data['clave']) + log.error(msg) + log.error(e) + + msg = 'Productos encontrados: {}
'.format(len(rows)) + msg += 'Productos agregados: {}
'.format(np) + msg += 'Productos actualizados: {}
'.format(ap) + msg += 'Productos con problemas: {}
'.format(len(rows) - np - ap) + msg += 'Productos con clave SAT erronea: {}'.format(cs) + return {'ok': True, 'msg': msg} + + @classmethod + def opt(cls, values): + if values['opt'] == 'import': + return cls._import(cls) + + return {'ok': False, 'msg': 'Sin opción'} + + @classmethod + def get_by(cls, values): + clave = values.get('id', '') + if clave: + row = (Productos + .select( + Productos.id, + Productos.clave, + Productos.clave_sat, + Productos.descripcion, + SATUnidades.name.alias('unidad'), + Productos.valor_unitario, + Productos.descuento) + .join(SATUnidades).switch(Productos) + .where((Productos.es_activo==True) & + ((Productos.id==clave) | (Productos.clave==clave))) + .dicts()) + if len(row): + id = row[0]['id'] + model_pt = Productos.impuestos.get_through_model() + taxes = tuple(model_pt + .select( + model_pt.productos_id.alias('product'), + model_pt.satimpuestos_id.alias('tax')) + .where(model_pt.productos_id==id).dicts()) + return {'ok': True, 'row': row[0], 'taxes': taxes} + + return {'ok': False} + + name = values.get('name', '') + if name: + rows = (Productos + .select( + Productos.id, + Productos.clave, + Productos.clave_sat, + Productos.descripcion, + SATUnidades.name.alias('unidad'), + Productos.valor_unitario) + .join(SATUnidades) + .switch(Productos) + .where((Productos.es_activo==True) & + ((Productos.descripcion.contains(name)) | + (Productos.clave.contains(name)))) + .dicts() + ) + return tuple(rows) + return {'ok': False} + + @classmethod + def get_(cls, values): + if values: + id = int(values['id']) + row = (Productos + .select( + Productos.id, + Productos.es_activo.alias('es_activo_producto'), + Productos.categoria, + Productos.clave, + Productos.clave_sat, + Productos.descripcion, + Productos.unidad, + Productos.valor_unitario, + Productos.cuenta_predial, + Productos.inventario, + Productos.existencia, + Productos.minimo, + ) + .where(Productos.id==id).dicts()[0] + ) + obj = Productos.get(Productos.id==id) + taxes = [row.id for row in obj.impuestos] + return {'row': row, 'taxes': taxes} + + rows = (Productos + .select( + Productos.id, + Productos.es_activo, + Productos.clave_sat, + Productos.clave, + Productos.descripcion, + SATUnidades.name.alias('unidad'), + Productos.valor_unitario) + .join(SATUnidades) + .dicts() + ) + return {'ok': True, 'rows': tuple(rows)} + + def _clean(self, values): + taxes = util.loads(values.pop('taxes')) + descripcion = util.spaces(values.pop('descripcion')) + fields = util.clean(values) + + fields.pop('precio_con_impuestos', '') + fields['es_activo'] = fields.pop('es_activo_producto') + fields['descripcion'] = descripcion + fields['unidad'] = int(fields['unidad']) + fields['valor_unitario'] = fields['valor_unitario'].replace( + '$', '').replace(',', '') + + fb = ('es_activo', 'inventario') + for name in fb: + fields[name] = bool(fields[name].replace('0', '')) + + return fields, taxes + + @classmethod + def add(cls, values): + fields, taxes = cls._clean(cls, values) + + if Productos.select().where(Productos.clave==fields['clave']).exists(): + msg = 'Clave ya existe' + return {'ok': False, 'msg': msg} + + obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes)) + + with database_proxy.transaction(): + obj = Productos.create(**fields) + obj.impuestos = obj_taxes + row = { + 'id': obj.id, + 'es_activo': obj.es_activo, + 'clave': obj.clave, + 'clave_sat': obj.clave_sat, + 'descripcion': obj.descripcion, + 'unidad': obj.unidad.name, + 'valor_unitario': obj.valor_unitario, + } + data = {'ok': True, 'row': row, 'new': True} + return data + + @classmethod + def actualizar(cls, values, id): + if not 'cuenta_predial' in values: + values['cuenta_predial'] = '' + fields, taxes = cls._clean(cls, values) + obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes)) + with database_proxy.transaction(): + q = Productos.update(**fields).where(Productos.id==id) + try: + q.execute() + except IntegrityError: + msg = 'Ya existe un producto con esta clave' + data = {'ok': False, 'row': {}, 'new': False, 'msg': msg} + return data + + obj = Productos.get(Productos.id==id) + obj.impuestos = obj_taxes + row = { + 'id': obj.id, + 'es_activo': obj.es_activo, + 'clave': obj.clave, + 'clave_sat': obj.clave_sat, + 'descripcion': obj.descripcion, + 'unidad': obj.unidad.name, + 'valor_unitario': obj.valor_unitario, + } + data = {'ok': True, 'row': row, 'new': False} + return data + + @classmethod + def remove(cls, id): + count = (FacturasDetalle + .select(fn.COUNT(FacturasDetalle.id)).join(Productos) + .where(Productos.id==id) + .count() + ) + if count: + return False + + with database_proxy.transaction(): + obj = Productos.get(Productos.id==id) + obj.impuestos.clear() + obj.tags.clear() + q = Productos.delete().where(Productos.id==id) + return bool(q.execute()) + + +class RangosPrecios(BaseModel): + producto = ForeignKeyField(Productos) + descripcion = TextField(default='') + desde = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + hasta = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = IntegerField(default=0) + + class Meta: + order_by = ('producto',) + + +class Facturas(BaseModel): + cliente = ForeignKeyField(Socios) + version = TextField(default=CURRENT_CFDI) + serie = TextField(default='') + folio = IntegerField(default=0) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + fecha_timbrado = DateTimeField(null=True) + forma_pago = TextField(default='') + condiciones_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + moneda = TextField(default='MXN') + tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6, + auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + tipo_comprobante = TextField(default='I') + metodo_pago = TextField(default='PUE') + lugar_expedicion = TextField(default='') + confirmacion = TextField(default='') + uso_cfdi = TextField(default='') + total_retenciones = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + xml = TextField(default='') + uuid = UUIDField(null=True) + estatus = TextField(default='Guardada') + estatus_sat = TextField(default='Vigente') + regimen_fiscal = TextField(default='') + notas = TextField(default='') + saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + pagada = BooleanField(default=False) + cancelada = BooleanField(default=False) + fecha_cancelacion = DateTimeField(null=True) + acuse = TextField(default='') + donativo = BooleanField(default=False) + anticipo = BooleanField(default=False) + egreso_anticipo = BooleanField(default=False) + tipo_relacion = TextField(default='') + error = TextField(default='') + + class Meta: + order_by = ('fecha',) + + @classmethod + def cancel(cls, id): + if CANCEL_SIGNATURE: + return cls._cancel_signature(cls, id) + return cls._cancel_xml(cls, id) + + def _cancel_xml(self, id): + msg = 'Factura cancelada correctamente' + auth = Emisor.get_auth() + certificado = Certificado.select()[0] + obj = Facturas.get(Facturas.id==id) + + if obj.version == '3.2': + msg = 'No es posible cancelar CFDI 3.2' + return {'ok': False, 'msg': msg} + + data, result = util.cancel_xml(auth, obj.uuid, certificado) + if data['ok']: + obj.estatus = 'Cancelada' + obj.error = '' + obj.cancelada = True + obj.fecha_cancelacion = result['Fecha'] + obj.acuse = result['Acuse'] + self._actualizar_saldo_cliente(self, obj, True) + else: + obj.error = data['msg'] + obj.save() + return data + + def _cancel_signature(self, id): + msg = 'Factura cancelada correctamente' + auth = Emisor.get_auth() + certificado = Certificado.select()[0] + obj = Facturas.get(Facturas.id==id) + data, result = util.cancel_signature( + obj.uuid, certificado.p12, certificado.rfc, auth) + if data['ok']: + obj.estatus = 'Cancelada' + obj.error = '' + obj.cancelada = True + obj.fecha_cancelacion = result['Fecha'] + obj.acuse = result['Acuse'] + self._actualizar_saldo_cliente(self, obj, True) + else: + obj.error = data['msg'] + obj.save() + return data + + @classmethod + def filter_years(cls): + data = [{'id': -1, 'value': 'Todos'}] + rows = (Facturas + .select(Facturas.fecha.year.alias('year')) + .group_by(Facturas.fecha.year) + .order_by(Facturas.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 get_xml(cls, id): + obj = Facturas.get(Facturas.id==id) + name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) + cls._sync_xml(cls, obj) + return obj.xml, name + + @classmethod + def get_notes(cls, id): + obj = Facturas.get(Facturas.id==int(id)) + return {'notes': obj.notas} + + @classmethod + def save_notes(cls, values): + obj = Facturas.get(Facturas.id==int(values['id'])) + obj.notas = values.get('notes', '') + obj.save() + return {'ok': True, 'msg': 'Notas guardadas correctamente'} + + #~ Revisar + def _get_data_cfdi_to_pdf(self, xml, cancel, version): + pre_nomina = PRE['NOMINA'][version] + + data['nomina'] = {} + node = doc.find('{}Complemento/{}Nomina'.format(pre, pre_nomina)) + if not node is None: + data['nomina']['nomina'] = node.attrib.copy() + subnode = node.find('{}Emisor'.format(pre_nomina)) + if not subnode is None: + data['emisor'].update(subnode.attrib.copy()) + subnode = node.find('{}Receptor'.format(pre_nomina)) + data['receptor'].update(subnode.attrib.copy()) + + subnode = node.find('{}Percepciones'.format(pre_nomina)) + data['nomina']['percepciones'] = subnode.attrib.copy() + detalle = [] + for n in subnode.getchildren(): + if 'SeparacionIndemnizacion' in n.tag: + continue + detalle.append(n.attrib.copy()) + data['nomina']['percepciones']['detalle'] = detalle + + data['nomina']['deducciones'] = None + subnode = node.find('{}Deducciones'.format(pre_nomina)) + if not subnode is None: + data['nomina']['deducciones'] = subnode.attrib.copy() + detalle = [] + for n in subnode.getchildren(): + detalle.append(n.attrib.copy()) + data['nomina']['deducciones']['detalle'] = detalle + + data['nomina']['incapacidades'] = None + subnode = node.find('{}Incapacidades'.format(pre_nomina)) + if not subnode is None: + detalle = [] + for n in subnode.getchildren(): + detalle.append(n.attrib.copy()) + data['nomina']['incapacidades'] = detalle + + data['nomina']['otrospagos'] = None + subnode = node.find('{}OtrosPagos'.format(pre_nomina)) + if not subnode is None: + data['nomina']['otrospagos'] = subnode.attrib.copy() + detalle = [] + for n in subnode.getchildren(): + detalle.append(n.attrib.copy()) + ns = n.find('{}SubsidioAlEmpleo'.format(pre_nomina)) + if not ns is None: + data['nomina']['otrospagos']['SubsidioCausado'] = ns.attrib['SubsidioCausado'] + data['nomina']['otrospagos']['detalle'] = detalle + + return data + + def _get_not_in_xml(self, invoice, emisor): + values = {} + + values['notas'] = invoice.notas + values['fechadof'] = str(emisor.fecha_dof) + if invoice.version == '3.2': + return values + + obj = SATRegimenes.get(SATRegimenes.key==invoice.regimen_fiscal) + values['regimenfiscal'] = str(obj) + + obj = SATUsoCfdi.get(SATUsoCfdi.key==invoice.uso_cfdi) + values['usocfdi'] = str(obj) + + mp = { + 'PUE': 'Pago en una sola exhibición', + 'PPD': 'Pago en parcialidades o diferido', + } + values['metododepago'] = 'Método de Pago: ({}) {}'.format( + invoice.metodo_pago, mp[invoice.metodo_pago]) + + obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago) + values['formadepago'] = str(obj) + + obj = SATMonedas.get(SATMonedas.key==invoice.moneda) + values['moneda'] = str(obj) + + if invoice.tipo_relacion: + obj = SATTipoRelacion.get(SATTipoRelacion.key==invoice.tipo_relacion) + values['tiporelacion'] = str(obj) + + return values + + @classmethod + def get_pdf(cls, id, rfc, sync=True): + try: + emisor = Emisor.select()[0] + except IndexError: + return b'', 'sin_datos_de_emisor.pdf' + + obj = Facturas.get(Facturas.id==id) + name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc) + if obj.uuid is None: + return b'', name + + values = cls._get_not_in_xml(cls, obj, emisor) + data = util.get_data_from_xml(obj, values) + doc = util.to_pdf(data, emisor.rfc) + + if sync: + target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') + cls._sync_pdf(cls, doc, name, target) + + return doc, name + + @classmethod + def get_ods(cls, id, rfc): + try: + emisor = Emisor.select()[0] + except IndexError: + return b'', 'sin_datos_de_emisor.pdf' + + obj = Facturas.get(Facturas.id==id) + name = '{}{}_{}.ods'.format(obj.serie, obj.folio, obj.cliente.rfc) + if obj.uuid is None: + return b'', name + + values = cls._get_not_in_xml(cls, obj, emisor) + data = util.get_data_from_xml(obj, values) + doc = util.to_pdf(data, emisor.rfc, True) + return doc, name + + @classmethod + def get_zip(cls, id, rfc): + obj = Facturas.get(Facturas.id==id) + name_zip = '{}{}_{}.zip'.format(obj.serie, obj.folio, obj.cliente.rfc) + if obj.uuid is None: + return b'', name_zip + + file_xml = cls.get_xml(id) + if not file_xml[0]: + return b'', name_zip + + file_pdf = cls.get_pdf(id, rfc) + if not file_pdf[0]: + return b'', name_zip + + file_zip = util.to_zip(file_xml, file_pdf) + + return file_zip, name_zip + + @util.run_in_thread + def _send(self, id, rfc): + return Facturas.send(id, rfc) + + @util.run_in_thread + def _sync(self, id, auth): + return Facturas.sync(id, auth) + + @util.run_in_thread + def _sync_pdf(self, pdf, name_pdf, target): + auth = Emisor.get_auth() + files = ( + (pdf, name_pdf, target), + ) + util.sync_cfdi(auth, files) + return + + @util.run_in_thread + def _sync_xml(self, obj): + emisor = Emisor.select()[0] + auth = Emisor.get_auth() + name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) + target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') + files = ( + (obj.xml, name_xml, target), + ) + util.sync_cfdi(auth, files) + return + + @util.run_in_thread + def _actualizar_saldo_cliente(self, invoice, cancel=False): + if invoice.tipo_comprobante == 'T': + return + + if invoice.donativo and invoice.forma_pago == '12': + return + + importe = invoice.total_mn + if invoice.tipo_comprobante == 'E': + importe *= -1 + + if cancel: + importe *= -1 + + q = (Socios + .update(saldo_cliente=Socios.saldo_cliente + importe) + .where(Socios.id==invoice.cliente.id) + ) + return bool(q.execute()) + + @classmethod + def send(cls, id, rfc): + values = Configuracion.get_({'fields': 'correo'}) + in_zip = Configuracion.get_bool('chk_config_send_zip') + + if not values: + msg = 'No esta configurado el servidor de correo de salida' + return {'ok': False, 'msg': msg} + + obj = Facturas.get(Facturas.id==id) + if obj.uuid is None: + msg = 'La factura no esta timbrada' + return {'ok': False, 'msg': msg} + + if not obj.cliente.correo_facturas: + msg = 'El cliente no tiene configurado el correo para facturas' + return {'ok': False, 'msg': msg} + + if in_zip: + files = (cls.get_zip(id, rfc),) + else: + files = (cls.get_pdf(id, rfc), cls.get_xml(id)) + + fields = util.make_fields(obj.xml) + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(int(values['correo_ssl'])), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': obj.cliente.correo_facturas, + 'copia': values['correo_copia'], + 'confirmar': util.get_bool(values.get('correo_confirmacion', '0')), + 'asunto': util.make_info_mail(values['correo_asunto'], fields), + 'mensaje': util.make_info_mail(values['correo_mensaje'], fields), + 'files': files, + } + data= { + 'server': server, + 'options': options, + } + result = util.send_mail(data) + if not result['ok'] or result['msg']: + return {'ok': False, 'msg': result['msg']} + + msg = 'Factura enviada correctamente' + return {'ok': True, 'msg': msg} + + @classmethod + def sync(cls, id, auth): + obj = Facturas.get(Facturas.id==id) + if obj.uuid is None: + msg = 'La factura no esta timbrada' + return + + emisor = Emisor.select()[0] + pdf, name_pdf = cls.get_pdf(id, auth['RFC'], False) + name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) + target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') + files = ( + (obj.xml, name_xml, target), + (pdf, name_pdf, target), + ) + util.sync_cfdi(auth, files) + return + + def _get_filter_folios(self, values): + if not 'folio' in values: + return '' + + folios = values['folio'].split('-') + if len(folios) == 1: + try: + folio1 = int(folios[0]) + except ValueError: + return '' + + folio2 = folio1 + else: + try: + folio1 = int(folios[0]) + folio2 = int(folios[1]) + except ValueError: + return '' + + return (Facturas.folio.between(folio1, folio2)) + + def _get_por_pagar(self, ids): + filtros = ( + (Facturas.cancelada==False) & + (Facturas.uuid.is_null(False)) & + (Facturas.tipo_comprobante=='I') & + (Facturas.saldo>0) + ) + if ids: + filtros &= (Facturas.id.not_in(ids)) + + rows = tuple(Facturas + .select( + Facturas.id, + Facturas.serie, + Facturas.folio, + Facturas.uuid, + Facturas.fecha, + Facturas.tipo_comprobante, + Facturas.estatus, + Socios.nombre.alias('cliente'), + Facturas.total, + Facturas.saldo, + ) + .where(filtros) + .join(Socios) + .switch(Facturas) + .dicts() + ) + return {'ok': True, 'rows': rows} + + return + + def _get_opt(self, values): + if values['opt'] == 'porpagar': + return self._get_por_pagar(self, util.loads(values['ids'])) + + cfdis = util.loads(values['cfdis']) + + if values['year'] == '-1': + fy = (Facturas.fecha.year > 0) + else: + fy = (Facturas.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (Facturas.fecha.month > 0) + else: + fm = (Facturas.fecha.month == int(values['month'])) + + if values['opt'] == 'relacionados': + folios = self._get_filter_folios(self, values) + uuid = values.get('uuid', '') + if uuid: + f_uuid = (cast(Facturas.uuid, 'text').contains(uuid)) + cliente = (Facturas.cliente == int(values['id_cliente'])) + if cfdis: + f_ids = (Facturas.id.not_in(cfdis)) + else: + f_ids = (Facturas.id > 0) + + filters = (fy & fm & cliente & f_ids) + if folios: + filters = filters & folios + elif uuid: + filters = filters & f_uuid + + if values['anticipo'] == '1': + filters = filters & (Facturas.anticipo == True) + + rows = tuple(Facturas + .select(Facturas.id, Facturas.serie, Facturas.folio, + Facturas.uuid, Facturas.fecha, Facturas.tipo_comprobante, + Facturas.estatus, Facturas.total_mn) + .where(filters).dicts() + ) + + return {'ok': True, 'rows': rows} + + @classmethod + def get_(cls, values): + opt = values.get('opt', '') + if opt: + return cls._get_opt(cls, values) + + if 'start' in values: + filters = Facturas.fecha.between( + util.get_date(values['start']), + util.get_date(values['end'], True) + ) + else: + if values['year'] == '-1': + fy = (Facturas.fecha.year > 0) + else: + fy = (Facturas.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (Facturas.fecha.month > 0) + else: + fm = (Facturas.fecha.month == int(values['month'])) + filters = (fy & fm) + + rows = tuple(Facturas + .select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid, + Facturas.fecha, Facturas.tipo_comprobante, Facturas.estatus, + Facturas.total_mn, Socios.nombre.alias('cliente')) + .where(filters) + .join(Socios) + .switch(Facturas).dicts() + ) + return {'ok': True, 'rows': rows} + + @classmethod + def remove(cls, id): + obj = Facturas.get(Facturas.id==id) + if obj.uuid: + return False + + q = FacturasDetalle.delete().where(FacturasDetalle.factura==obj) + q.execute() + q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj) + q.execute() + q = FacturasRelacionadas.delete().where(FacturasRelacionadas.factura==obj) + q.execute() + return bool(obj.delete_instance()) + + def _get_folio(self, serie): + inicio_serie = Folios.select( + Folios.inicio).where(Folios.serie==serie).scalar() + + inicio = (Facturas + .select(fn.Max(Facturas.folio).alias('mf')) + .where(Facturas.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None or inicio_serie > inicio: + inicio = inicio_serie + else: + inicio += 1 + + return inicio + + def _calculate_totals(self, invoice, products): + tax_locales = Configuracion.get_bool('chk_config_tax_locales') + tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') + subtotal = 0 + descuento_cfdi = 0 + totals_tax = {} + total_trasladados = None + total_retenciones = None + locales_traslados = 0 + locales_retenciones = 0 + + for product in products: + # ~ print ('\n', product) + id_product = product.pop('id') + id_student = product.pop('id_student', 0) + p = Productos.get(Productos.id==id_product) + + product['unidad'] = p.unidad.key + product['clave'] = p.clave + product['clave_sat'] = p.clave_sat + product['cuenta_predial'] = p.cuenta_predial + + product['factura'] = invoice.id + product['producto'] = id_product + + cantidad = float(product['cantidad']) + valor_unitario = float(product['valor_unitario']) + descuento = float(product['descuento']) + precio_final = valor_unitario - descuento + importe = round(cantidad * precio_final, DECIMALES) + + product['cantidad'] = cantidad + product['valor_unitario'] = valor_unitario + product['descuento'] = round(descuento * cantidad, DECIMALES) + product['precio_final'] = precio_final + product['importe'] = round(cantidad * valor_unitario, DECIMALES) + + descuento_cfdi += product['descuento'] + subtotal += product['importe'] + + if id_student: + student = Alumnos.get(Alumnos.id==id_student) + product['alumno'] = str(student) + product['curp'] = student.curp + product['nivel'] = student.grupo.nivel.nombre + product['autorizacion'] = student.grupo.nivel.autorizacion.strip() + + FacturasDetalle.create(**product) + + base = product['importe'] - product['descuento'] + for tax in p.impuestos: + if tax_locales and tax.tipo == 'R' and tax.key == '000': + base = product['importe'] + if tax_decimals: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES_TAX) + else: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES) + if tax.tipo == 'T' and tax.key != '000': + total_trasladados = (total_trasladados or 0) + impuesto_producto + elif tax.tipo == 'R' and tax.key != '000': + total_retenciones = (total_retenciones or 0) + impuesto_producto + elif tax.tipo == 'T' and tax.key == '000': + locales_traslados += impuesto_producto + elif tax.tipo == 'R' and tax.key == '000': + locales_retenciones += impuesto_producto + + if tax.id in totals_tax: + totals_tax[tax.id].base += base + totals_tax[tax.id].suma_impuestos += impuesto_producto + else: + tax.base = base + tax.suma_impuestos = impuesto_producto + totals_tax[tax.id] = tax + + for tax in totals_tax.values(): + if tax.tipo == 'E': + continue + + invoice_tax = { + 'factura': invoice.id, + 'impuesto': tax.id, + 'base': tax.base, + 'importe': tax.suma_impuestos, + } + FacturasImpuestos.create(**invoice_tax) + + total = subtotal - descuento_cfdi + \ + (total_trasladados or 0) - (total_retenciones or 0) \ + + locales_traslados - locales_retenciones + total_mn = round(total * invoice.tipo_cambio, DECIMALES) + data = { + 'subtotal': subtotal, + 'descuento': descuento_cfdi, + 'total': total, + 'total_mn': total_mn, + 'total_trasladados': total_trasladados, + 'total_retenciones': total_retenciones, + } + return data + + def _guardar_relacionados(self, invoice, relacionados): + for cfdi in relacionados: + data = { + 'factura': invoice, + 'factura_origen': cfdi, + } + FacturasRelacionadas.create(**data) + return + + def _guardar_ine(self, invoice, valores): + if not valores: + return + + data = { + 'factura': invoice, + 'nombre': 'ine', + 'valores': valores, + } + FacturasComplementos.create(**data) + return + + + def _get_serie(self, user, default_serie): + if user.sucursal is None: + return default_serie + + return user.sucursal.serie_facturas or default_serie + + @classmethod + def add(cls, values, user): + productos = util.loads(values.pop('productos')) + relacionados = util.loads(values.pop('relacionados')) + ine = values.pop('ine', {}) + + emisor = Emisor.select()[0] + values['serie'] = cls._get_serie(cls, user, values['serie']) + values['folio'] = cls._get_folio(cls, values['serie']) + values['tipo_cambio'] = float(values['tipo_cambio']) + values['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + values['anticipo'] = util.get_bool(values['anticipo']) + values['donativo'] = util.get_bool(values['donativo']) + + with database_proxy.atomic() as txn: + obj = Facturas.create(**values) + totals = cls._calculate_totals(cls, obj, productos) + cls._guardar_relacionados(cls, obj, relacionados) + cls._guardar_ine(cls, obj, ine) + obj.subtotal = totals['subtotal'] + obj.descuento = totals['descuento'] + obj.total_trasladados = totals['total_trasladados'] + obj.total_retenciones = totals['total_retenciones'] + obj.total = totals['total'] + obj.saldo = totals['total'] + obj.total_mn = totals['total_mn'] + obj.save() + + msg = 'Factura guardada correctamente. Enviando a timbrar' + row = { + 'id': obj.id, + 'serie': obj.serie, + 'folio': obj.folio, + 'uuid': obj.uuid, + 'fecha': obj.fecha, + 'tipo_comprobante': obj.tipo_comprobante, + 'estatus': obj.estatus, + 'total_mn': obj.total_mn, + 'cliente': obj.cliente.nombre, + } + data = {'ok': True, 'row': row, 'new': True, 'error': False, 'msg': msg} + return data + + def _make_xml(self, invoice, auth): + tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') + tmp = 0 + emisor = Emisor.select()[0] + certificado = Certificado.select()[0] + + is_edu = False + comprobante = {} + relacionados = {} + donativo = {} + complementos = FacturasComplementos.get_(invoice) + + if invoice.donativo: + donativo['noAutorizacion'] = emisor.autorizacion + donativo['fechaAutorizacion'] = str(emisor.fecha_autorizacion) + + if invoice.serie: + comprobante['Serie'] = invoice.serie + if invoice.condiciones_pago: + comprobante['CondicionesDePago'] = invoice.condiciones_pago + # ~ if invoice.descuento: + # ~ comprobante['Descuento'] = invoice.descuento + + comprobante['Folio'] = str(invoice.folio) + comprobante['Fecha'] = invoice.fecha.isoformat()[:19] + comprobante['FormaPago'] = invoice.forma_pago + comprobante['NoCertificado'] = certificado.serie + comprobante['Certificado'] = certificado.cer_txt + comprobante['SubTotal'] = FORMAT.format(invoice.subtotal) + comprobante['Moneda'] = invoice.moneda + comprobante['TipoCambio'] = '1' + if comprobante['Moneda'] != 'MXN': + comprobante['TipoCambio'] = FORMAT.format(invoice.tipo_cambio) + comprobante['Total'] = FORMAT.format(invoice.total) + comprobante['TipoDeComprobante'] = invoice.tipo_comprobante + comprobante['MetodoPago'] = invoice.metodo_pago + comprobante['LugarExpedicion'] = invoice.lugar_expedicion + if invoice.descuento: + comprobante['Descuento'] = FORMAT.format(invoice.descuento) + + if invoice.tipo_relacion: + relacionados = { + 'tipo': invoice.tipo_relacion, + 'cfdis': FacturasRelacionadas.get_(invoice), + } + + emisor = { + 'Rfc': emisor.rfc, + 'Nombre': emisor.nombre, + 'RegimenFiscal': invoice.regimen_fiscal, + } + + receptor = { + 'Rfc': invoice.cliente.rfc, + 'Nombre': invoice.cliente.nombre, + 'UsoCFDI': invoice.uso_cfdi, + } + + conceptos = [] + rows = FacturasDetalle.select().where(FacturasDetalle.factura==invoice) + for row in rows: + concepto = { + 'ClaveProdServ': row.producto.clave_sat, + 'NoIdentificacion': row.producto.clave, + 'Cantidad': FORMAT.format(row.cantidad), + 'ClaveUnidad': row.producto.unidad.key, + 'Unidad': row.producto.unidad.name[:20], + 'Descripcion': row.descripcion, + 'ValorUnitario': FORMAT.format(row.valor_unitario), + 'Importe': FORMAT.format(row.importe), + } + if row.descuento: + concepto['Descuento'] = FORMAT.format(row.descuento) + + if row.cuenta_predial: + concepto['CuentaPredial'] = row.cuenta_predial + + if row.pedimento: + concepto['Pedimento'] = row.pedimento + + if row.autorizacion: + is_edu = True + concepto['student'] = { + 'version': '1.0', + 'nombreAlumno': row.alumno, + 'CURP': row.curp, + 'nivelEducativo': row.nivel, + 'autRVOE': row.autorizacion, + } + + taxes = {} + traslados = [] + retenciones = [] + + for impuesto in row.producto.impuestos: + if impuesto.tipo == 'E': + continue + + if impuesto.key == '000': + continue + + base = row.importe - row.descuento + if tax_decimals: + import_tax = round(impuesto.tasa * base, DECIMALES_TAX) + tmp += import_tax + xml_importe = FORMAT_TAX.format(import_tax) + else: + import_tax = round(impuesto.tasa * base, DECIMALES) + xml_importe = FORMAT.format(import_tax) + tipo_factor = 'Tasa' + if impuesto.factor != 'T': + tipo_factor = 'Cuota' + tax = { + "Base": FORMAT.format(base), + "Impuesto": impuesto.key, + "TipoFactor": tipo_factor, + "TasaOCuota": str(impuesto.tasa), + "Importe": xml_importe, + } + if impuesto.tipo == 'T': + traslados.append(tax) + else: + retenciones.append(tax) + + if traslados: + taxes['traslados'] = traslados + if retenciones: + taxes['retenciones'] = retenciones + concepto['impuestos'] = taxes + conceptos.append(concepto) + + impuestos = {} + traslados = [] + retenciones = [] + total_locales_trasladados = 0 + total_locales_retenciones = 0 + locales_trasladados = [] + locales_retenciones = [] + + if not invoice.total_trasladados is None: + impuestos['TotalImpuestosTrasladados'] = \ + FORMAT.format(invoice.total_trasladados) + if not invoice.total_retenciones is None: + impuestos['TotalImpuestosRetenidos'] = \ + FORMAT.format(invoice.total_retenciones) + + taxes = (FacturasImpuestos + .select() + .where(FacturasImpuestos.factura==invoice)) + for tax in taxes: + if tax.impuesto.key == '000': + tasa = str(round(tax.impuesto.tasa * 100, 2)) + simporte = FORMAT.format(tax.importe) + if tax.impuesto.tipo == 'T': + traslado = { + 'ImpLocTrasladado': tax.impuesto.name, + 'TasadeTraslado': tasa, + 'Importe': simporte, + } + locales_trasladados.append(traslado) + total_locales_trasladados += tax.importe + else: + retencion = { + 'ImpLocRetenido': tax.impuesto.name, + 'TasadeRetencion': tasa, + 'Importe': simporte, + } + locales_retenciones.append(retencion) + total_locales_retenciones += tax.importe + continue + + tipo_factor = 'Tasa' + if tax.impuesto.factor != 'T': + tipo_factor = 'Cuota' + + if tax_decimals: + xml_importe = FORMAT_TAX.format(tax.importe) + else: + xml_importe = FORMAT.format(tax.importe) + + if tax.impuesto.tipo == 'T': + traslado = { + "Impuesto": tax.impuesto.key, + "TipoFactor": tipo_factor, + "TasaOCuota": str(tax.impuesto.tasa), + "Importe": xml_importe, + } + traslados.append(traslado) + else: + retencion = { + "Impuesto": tax.impuesto.key, + "Importe": FORMAT.format(tax.importe), + } + retenciones.append(retencion) + + impuestos['traslados'] = traslados + impuestos['retenciones'] = retenciones + impuestos['total_locales_trasladados'] = '' + if total_locales_trasladados: + impuestos['total_locales_trasladados'] = \ + FORMAT.format(total_locales_trasladados) + impuestos['total_locales_retenciones'] = '' + if total_locales_retenciones: + impuestos['total_locales_retenciones'] = \ + FORMAT.format(total_locales_retenciones) + impuestos['locales_trasladados'] = locales_trasladados + impuestos['locales_retenciones'] = locales_retenciones + + data = { + 'comprobante': comprobante, + 'relacionados': relacionados, + 'emisor': emisor, + 'receptor': receptor, + 'conceptos': conceptos, + 'impuestos': impuestos, + 'donativo': donativo, + 'edu': is_edu, + 'complementos': complementos, + } + return util.make_xml(data, certificado, auth) + + @classmethod + def get_status_sat(cls, id): + obj = Facturas.get(Facturas.id == id) + obj.estatus_sat = util.get_sat(obj.xml) + obj.save() + return obj.estatus_sat + + @classmethod + def anticipo_egreso(cls, id): + origen = Facturas.get(Facturas.id == id) + relacionadas = (FacturasRelacionadas + .select(FacturasRelacionadas.factura_origen) + .where(FacturasRelacionadas.factura==origen)) + conceptos = (FacturasDetalle + .select() + .where(FacturasDetalle.factura==origen)) + impuestos = (FacturasImpuestos + .select() + .where(FacturasImpuestos.factura==origen)) + + #~ egreso_anticipo = BooleanField(default=False) + + serie = Folios.get_egreso(origen.serie) + nueva = { + 'cliente': origen.cliente, + 'tipo_comprobante': 'E', + 'forma_pago': '30', + 'serie': serie, + 'folio': cls._get_folio(cls, serie), + 'tipo_relacion': '07', + 'pagada': True, + 'lugar_expedicion': origen.lugar_expedicion, + 'uso_cfdi': origen.uso_cfdi, + 'moneda': origen.moneda, + 'tipo_cambio': origen.tipo_cambio, + 'regimen_fiscal': origen.regimen_fiscal, + 'subtotal': origen.subtotal, + 'total': origen.total, + 'total_trasladados': origen.total_trasladados, + 'total_retenciones': origen.total_retenciones, + } + return + + @classmethod + def timbrar(cls, id): + auth = Emisor.get_auth() + obj = Facturas.get(Facturas.id == id) + obj.xml = cls._make_xml(cls, obj, auth) + obj.estatus = 'Generada' + obj.save() + + enviar_correo = util.get_bool(Configuracion.get_('correo_directo')) + + anticipo = False + msg = 'Factura timbrada correctamente' + 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 = 'Timbrada' + obj.error = '' + obj.save() + row = {'uuid': obj.uuid, 'estatus': 'Timbrada'} + if enviar_correo: + cls._send(cls, id, auth['RFC']) + if obj.tipo_comprobante == 'I' and obj.tipo_relacion == '07': + anticipo = True + cls._actualizar_saldo_cliente(cls, obj) + cls._sync(cls, id, auth) + else: + msg = result['error'] + obj.estatus = 'Error' + obj.error = msg + obj.save() + row = {'estatus': 'Error'} + + result = { + 'ok': result['ok'], + 'msg': msg, + 'row': row, + 'anticipo': anticipo + } + + return result + + +class PreFacturas(BaseModel): + cliente = ForeignKeyField(Socios) + serie = TextField(default='PRE') + folio = IntegerField(default=0) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + forma_pago = TextField(default='') + condiciones_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + moneda = TextField(default='MXN') + tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + tipo_comprobante = TextField(default='I') + metodo_pago = TextField(default='PUE') + lugar_expedicion = TextField(default='') + uso_cfdi = TextField(default='') + total_retenciones = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + estatus = TextField(default='Generada') + regimen_fiscal = TextField(default='') + notas = TextField(default='') + donativo = BooleanField(default=False) + tipo_relacion = TextField(default='') + + class Meta: + order_by = ('fecha',) + + @util.run_in_thread + def _send_in_thread(self, id, obj, values): + log.info('Generando PDF...') + files = (self.get_pdf(id),) + log.info('PDF Generado...') + + invoice = PreFacturas.select().where(PreFacturas.id==id).dicts()[0] + fields = { + 'receptor_nombre': obj.cliente.nombre, + 'receptor_rfc': obj.cliente.rfc, + } + fields.update(invoice) + + asunto = 'Enviamos la prefactura: PRE-{}'.format(obj.folio) + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(int(values['correo_ssl'])), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': obj.cliente.correo_facturas, + 'copia': values['correo_copia'], + 'confirmar': util.get_bool(values.get('correo_confirmacion', '0')), + 'asunto': asunto, + 'mensaje': util.make_info_mail(values['correo_mensaje'], fields), + 'files': files, + } + data= { + 'server': server, + 'options': options, + } + log.info('Enviando prefactura...') + result = util.send_mail(data) + log.info('Prefactura enviada...') + return + + @classmethod + def enviar(cls, id): + values = Configuracion.get_({'fields': 'correo'}) + if not values: + msg = 'No esta configurado el servidor de correo de salida' + return {'ok': False, 'msg': msg} + + obj = PreFacturas.get(PreFacturas.id==id) + if not obj.cliente.correo_facturas: + msg = 'El cliente no tiene configurado el correo para facturas' + return {'ok': False, 'msg': msg} + + rows = PreFacturasDetalle.count(id) + if rows > 300: + cls._send_in_thread(cls, id, obj, values) + msg = 'Enviando correo...' + return {'ok': True, 'msg': msg} + + files = (cls.get_pdf(id),) + + invoice = PreFacturas.select().where(PreFacturas.id==id).dicts()[0] + fields = { + 'receptor_nombre': obj.cliente.nombre, + 'receptor_rfc': obj.cliente.rfc, + } + fields.update(invoice) + + asunto = 'Enviamos la prefactura: PRE-{}'.format(obj.folio) + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(int(values['correo_ssl'])), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': obj.cliente.correo_facturas, + 'copia': values['correo_copia'], + 'confirmar': util.get_bool(values.get('correo_confirmacion', '0')), + 'asunto': asunto, + 'mensaje': util.make_info_mail(values['correo_mensaje'], fields), + 'files': files, + } + data= { + 'server': server, + 'options': options, + } + result = util.send_mail(data) + if not result['ok'] or result['msg']: + return {'ok': False, 'msg': result['msg']} + + msg = 'Pre Factura enviada correctamente' + return {'ok': True, 'msg': msg} + + def _get_info_to_pdf(self, id): + data = {} + obj = PreFacturas.select().where(PreFacturas.id==id).dicts()[0] + regimen = SATRegimenes.get(SATRegimenes.key==obj['regimen_fiscal']) + usocfdi = SATUsoCfdi.get(SATUsoCfdi.key==obj['uso_cfdi']) + formapago = SATFormaPago.get(SATFormaPago.key==obj['forma_pago']) + moneda = SATMonedas.get(SATMonedas.key==obj['moneda']) + + emisor = util.get_dict(Emisor.select().dicts()[0]) + emisor['nointerior'] = emisor['no_interior'] + emisor['noexterior'] = emisor['no_exterior'] + emisor['codigopostal'] = emisor['codigo_postal'] + emisor['regimenfiscal'] = str(regimen) + + receptor = Socios.select().where(Socios.id==obj['cliente']).dicts()[0] + receptor['usocfdi'] = str(usocfdi) + receptor['nointerior'] = receptor['no_interior'] + receptor['noexterior'] = receptor['no_exterior'] + receptor['codigopostal'] = receptor['codigo_postal'] + + data['es_pre'] = True + data['cancelada'] = False + data['donativo'] = obj['donativo'] + + tipos = { + 'I': 'ingreso', + 'E': 'egreso', + 'T': 'traslado', + } + mp = { + 'PUE': 'Pago en una sola exhibición', + 'PPD': 'Pago en parcialidades o diferido', + } + + data['comprobante'] = obj + data['comprobante']['version'] = CURRENT_CFDI + data['comprobante']['folio'] = str(data['comprobante']['folio']) + data['comprobante']['seriefolio'] = '{}-{}'.format( + data['comprobante']['serie'], data['comprobante']['folio']) + data['comprobante']['fecha'] = str(data['comprobante']['fecha']) + data['comprobante']['tipodecomprobante'] = tipos.get( + data['comprobante']['tipo_comprobante']) + data['comprobante']['lugarexpedicion'] = \ + 'C.P. de Expedición: {}'.format( + data['comprobante']['lugar_expedicion']) + data['comprobante']['metododepago'] = 'Método de Pago: ({}) {}'.format( + obj['metodo_pago'], mp[obj['metodo_pago']]) + data['comprobante']['formadepago'] = str(formapago) + data['comprobante']['condicionesdepago'] = \ + data['comprobante']['condiciones_pago'] + data['comprobante']['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format( + data['comprobante']['tipo_cambio']) + data['comprobante']['totalenletras'] = util.to_letters( + data['comprobante']['total'], data['comprobante']['moneda']) + data['comprobante']['moneda'] = str(moneda) + + data['emisor'] = emisor + data['receptor'] = receptor + + data['conceptos'] = PreFacturasDetalle.get_(id) + data['totales'] = {} + data['totales']['moneda'] = data['comprobante']['moneda'] + data['totales']['subtotal'] = str(data['comprobante']['subtotal']) + data['totales']['total'] = str(data['comprobante']['total']) + + if obj['descuento']: + data['totales']['descuento'] = float(obj['descuento']) + + taxes = PreFacturasImpuestos.get_(id) + data['totales']['traslados'] = taxes['traslados'] + data['totales']['retenciones'] = taxes['retenciones'] + data['totales']['taxlocales'] = taxes['taxlocales'] + data['timbre'] = {} + + data['donataria'] = {} + data['ine'] = {} + + return data + + @util.run_in_thread + def _get_pdf_in_thread(self, id): + obj = PreFacturas.get(PreFacturas.id==id) + name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc) + data = self._get_info_to_pdf(self, id) + doc = util.to_pdf(data, data['emisor']['rfc']) + + emisor = Emisor.select()[0] + target = emisor.rfc + '/Prefacturas/' + files = ( + (doc, name, target), + ) + util.sync_cfdi({'REPO': False}, files) + return + + @classmethod + def get_pdf_in_thread(cls, id): + return cls._get_pdf_in_thread(cls, id) + + @classmethod + def get_pdf(cls, id): + obj = PreFacturas.get(PreFacturas.id==id) + name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc) + data = cls._get_info_to_pdf(cls, id) + doc = util.to_pdf(data, data['emisor']['rfc']) + return doc, name + + @classmethod + def remove(cls, id): + obj = PreFacturas.get(PreFacturas.id==id) + + q = PreFacturasDetalle.delete().where( + PreFacturasDetalle.factura==obj) + q.execute() + q = PreFacturasImpuestos.delete().where( + PreFacturasImpuestos.factura==obj) + q.execute() + q = PreFacturasRelacionadas.delete().where( + PreFacturasRelacionadas.factura==obj) + q.execute() + return bool(obj.delete_instance()) + + @classmethod + def filter_years(cls): + data = [{'id': -1, 'value': 'Todos'}] + rows = (PreFacturas + .select(PreFacturas.fecha.year) + .group_by(PreFacturas.fecha.year) + .order_by(PreFacturas.fecha.year) + .scalar(as_tuple=True) + ) + if not rows is None: + data += [{'id': int(row), 'value': int(row)} for row in rows] + return tuple(data) + + @classmethod + def get_(cls, values): + if values['year'] == '-1': + fy = (PreFacturas.fecha.year > 0) + else: + fy = (PreFacturas.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (PreFacturas.fecha.month > 0) + else: + fm = (PreFacturas.fecha.month == int(values['month'])) + filters = (fy & fm) + + rows = tuple(PreFacturas + .select( + PreFacturas.id, + PreFacturas.folio, + PreFacturas.fecha, + PreFacturas.tipo_comprobante, + PreFacturas.total_mn, + Socios.nombre.alias('cliente')) + .where(filters) + .join(Socios) + .switch(PreFacturas).dicts() + ) + return {'ok': True, 'rows': rows} + + def _get_folio(self, serie): + inicio = (PreFacturas + .select(fn.Max(PreFacturas.folio).alias('mf')) + .where(PreFacturas.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None: + inicio = 1 + else: + inicio += 1 + + return inicio + + def _calculate_totals(self, invoice, products): + tax_locales = Configuracion.get_bool('chk_config_tax_locales') + tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') + tax_decimals = True + subtotal = 0 + descuento_cfdi = 0 + totals_tax = {} + total_trasladados = None + total_retenciones = None + locales_traslados = 0 + locales_retenciones = 0 + + for product in products: + id_product = product.pop('id') + p = Productos.get(Productos.id==id_product) + + product['unidad'] = p.unidad.key + product['clave'] = p.clave + product['clave_sat'] = p.clave_sat + product['cuenta_predial'] = p.cuenta_predial + + product['factura'] = invoice.id + product['producto'] = id_product + + cantidad = float(product['cantidad']) + valor_unitario = float(product['valor_unitario']) + descuento = float(product['descuento']) + precio_final = valor_unitario - descuento + importe = round(cantidad * precio_final, DECIMALES) + + product['cantidad'] = cantidad + product['valor_unitario'] = valor_unitario + product['descuento'] = descuento + product['precio_final'] = precio_final + product['importe'] = round(cantidad * valor_unitario, DECIMALES) + + descuento_cfdi += descuento + subtotal += importe + + PreFacturasDetalle.create(**product) + + base = product['importe'] - product['descuento'] + for tax in p.impuestos: + if tax_locales and tax.tipo == 'R' and tax.key == '000': + base = product['importe'] + if tax_decimals: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES_TAX) + else: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES) + if tax.tipo == 'T' and tax.key != '000': + total_trasladados = (total_trasladados or 0) + impuesto_producto + elif tax.tipo == 'R' and tax.key != '000': + total_retenciones = (total_retenciones or 0) + impuesto_producto + elif tax.tipo == 'T' and tax.key == '000': + locales_traslados += impuesto_producto + elif tax.tipo == 'R' and tax.key == '000': + locales_retenciones += impuesto_producto + + if tax.id in totals_tax: + totals_tax[tax.id].base += base + totals_tax[tax.id].suma_impuestos += impuesto_producto + else: + tax.base = base + tax.suma_impuestos = impuesto_producto + totals_tax[tax.id] = tax + + for tax in totals_tax.values(): + if tax.tipo == 'E': + continue + + invoice_tax = { + 'factura': invoice.id, + 'impuesto': tax.id, + 'base': tax.base, + 'importe': tax.suma_impuestos, + } + PreFacturasImpuestos.create(**invoice_tax) + + total = subtotal - descuento_cfdi + \ + (total_trasladados or 0) - (total_retenciones or 0) \ + + locales_traslados - locales_retenciones + total_mn = round(total * invoice.tipo_cambio, DECIMALES) + data = { + 'subtotal': subtotal + descuento, + 'descuento': descuento_cfdi, + 'total': total, + 'total_mn': total_mn, + 'total_trasladados': total_trasladados, + 'total_retenciones': total_retenciones, + } + return data + + @classmethod + def add(cls, values): + productos = util.loads(values.pop('productos')) + + emisor = Emisor.select()[0] + values['serie'] = 'PRE' + values['folio'] = cls._get_folio(cls, values['serie']) + values['tipo_cambio'] = float(values['tipo_cambio']) + values['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + + with database_proxy.atomic() as txn: + obj = PreFacturas.create(**values) + totals = cls._calculate_totals(cls, obj, productos) + obj.subtotal = totals['subtotal'] + obj.descuento = totals['descuento'] + obj.total_trasladados = totals['total_trasladados'] + obj.total_retenciones = totals['total_retenciones'] + obj.total = totals['total'] + obj.total_mn = totals['total_mn'] + obj.save() + + msg = 'Factura guardada correctamente' + row = { + 'id': obj.id, + 'folio': obj.folio, + 'fecha': obj.fecha, + 'tipo_comprobante': obj.tipo_comprobante, + 'total_mn': obj.total_mn, + 'cliente': obj.cliente.nombre, + } + data = {'ok': True, 'row': row, 'new': True, 'error': False, 'msg': msg} + return data + + +class FacturasRelacionadas(BaseModel): + factura = ForeignKeyField(Facturas, related_name='original') + factura_origen = ForeignKeyField(Facturas, related_name='relacion') + + class Meta: + order_by = ('factura',) + + @classmethod + def get_(cls, invoice): + query = (FacturasRelacionadas + .select() + .where(FacturasRelacionadas.factura==invoice) + ) + return [str(r.factura_origen.uuid) for r in query] + + +class FacturasComplementos(BaseModel): + factura = ForeignKeyField(Facturas) + nombre = TextField(default='') + valores = TextField(default='') + + class Meta: + order_by = ('factura',) + + @classmethod + def get_(cls, factura): + query = (FacturasComplementos + .select() + .where(FacturasComplementos.factura==factura) + ) + return {r.nombre: util.loads(r.valores) for r in query} + + +class PreFacturasRelacionadas(BaseModel): + factura = ForeignKeyField(PreFacturas, related_name='original') + factura_origen = ForeignKeyField(PreFacturas, related_name='relacion') + + class Meta: + order_by = ('factura',) + + +class FacturasDetalle(BaseModel): + factura = ForeignKeyField(Facturas) + producto = ForeignKeyField(Productos, null=True) + cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descripcion = TextField(default='') + unidad = TextField(default='') + clave = TextField(default='') + clave_sat = TextField(default='') + categoria = TextField(default='') + aduana = TextField(default='') + pedimento = TextField(default='') + fecha_pedimento = DateField(null=True) + alumno = TextField(default='') + curp = TextField(default='') + nivel = TextField(default='') + autorizacion = TextField(default='') + cuenta_predial = TextField(default='') + + class Meta: + order_by = ('factura',) + + +class PreFacturasDetalle(BaseModel): + factura = ForeignKeyField(PreFacturas) + producto = ForeignKeyField(Productos, null=True) + descripcion = TextField(default='') + cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + aduana = TextField(default='') + pedimento = TextField(default='') + fecha_pedimento = DateField(null=True) + alumno = TextField(default='') + curp = TextField(default='') + nivel = TextField(default='') + autorizacion = TextField(default='') + cuenta_predial = TextField(default='') + + class Meta: + order_by = ('factura',) + + def _get_impuestos(self, id): + model_pt = Productos.impuestos.get_through_model() + impuestos = tuple(model_pt + .select( + model_pt.productos_id.alias('product'), + model_pt.satimpuestos_id.alias('tax')) + .where(model_pt.productos_id==id).dicts()) + return impuestos + + @classmethod + def facturar(cls, id): + data = [] + + q = PreFacturas.select().where(PreFacturas.id==id)[0] + if q.cliente.forma_pago is None: + forma_pago = '' + else: + forma_pago = q.cliente.forma_pago.key + + if q.cliente.uso_cfdi is None: + uso_cfdi = '' + else: + uso_cfdi = q.cliente.uso_cfdi.key + + receptor = { + 'id': q.cliente.id, + 'nombre': q.cliente.nombre, + 'rfc': q.cliente.rfc, + 'forma_pago': forma_pago, + 'uso_cfdi': uso_cfdi, + 'notas': q.notas, + } + + productos = PreFacturasDetalle.select().where( + PreFacturasDetalle.factura==id) + + for p in reversed(productos): + row = {'id': p.producto.id} + row['clave'] = p.producto.clave + row['descripcion'] = p.descripcion + row['unidad'] = p.producto.unidad.name + row['cantidad'] = p.cantidad + row['valor_unitario'] = p.valor_unitario + row['descuento'] = p.descuento + pf = p.valor_unitario - p.descuento + row['importe'] = round(pf * p.cantidad, DECIMALES) + impuestos = cls._get_impuestos(cls, row['id']) + data.append({'row': row, 'taxes': impuestos}) + + return {'rows': data, 'receptor': receptor} + + @classmethod + def count(cls, id): + c = PreFacturasDetalle.select().where( + PreFacturasDetalle.factura==id).count() + return c + + @classmethod + def can_open(cls, id): + c = cls.count(id) + PreFacturas.get_pdf_in_thread(id) + return c < 300 + + @classmethod + def get_(cls, id): + data = [] + + productos = PreFacturasDetalle.select().where( + PreFacturasDetalle.factura==id) + + for p in reversed(productos): + producto = {} + producto['noidentificacion'] = '{}\n(SAT {})'.format( + p.producto.clave, p.producto.clave_sat) + + producto['descripcion'] = p.descripcion + if p.cuenta_predial: + info = '\nCuenta Predial Número: {}'.format(p.cuenta_predial) + producto['descripcion'] += info + + producto['unidad'] = '{}\n({})'.format( + p.producto.unidad.name, p.producto.unidad.key) + producto['cantidad'] = str(p.cantidad) + producto['valorunitario'] = str(p.valor_unitario) + producto['importe'] = str(p.importe) + data.append(producto) + + return data + + +class FacturasImpuestos(BaseModel): + factura = ForeignKeyField(Facturas) + impuesto = ForeignKeyField(SATImpuestos) + base = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('factura',) + indexes = ( + (('factura', 'impuesto'), True), + ) + + +class FacturasPagos(BaseModel): + movimiento = ForeignKeyField(MovimientosBanco) + factura = ForeignKeyField(Facturas) + numero = IntegerField(default=1) + saldo_anterior = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + saldo = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('factura',) + indexes = ( + (('movimiento', 'factura', 'numero'), True), + ) + + def _movimiento_anterior(self, mov, id): + query = (FacturasPagos + .select() + .where(FacturasPagos.factura==id) + ) + if len(query): + return query[-1], len(query) + 1 + else: + return None, 1 + + def _actualizar_saldo_cliente(self, cliente, importe): + q = (Socios + .update(saldo_cliente=Socios.saldo_cliente + importe) + .where(Socios.id==cliente.id) + ) + return bool(q.execute()) + + def _actualizar_saldos(self, factura, saldo_anterior): + query = (FacturasPagos + .select() + .where(FacturasPagos.factura==factura) + ) + saldo = saldo_anterior + for i, row in enumerate(query): + if not saldo_anterior: + saldo_anterior = row.saldo_anterior + row.numero = i + 1 + row.saldo_anterior = saldo_anterior + row.saldo = saldo_anterior - row.importe + row.save() + saldo_anterior = row.saldo + saldo = row.saldo + + factura.saldo = saldo + factura.pagada = False + factura.save() + return + + @classmethod + def cancelar(cls, mov): + query = (FacturasPagos + .select() + .where(FacturasPagos.movimiento==mov) + ) + for row in query: + cls._actualizar_saldo_cliente(cls, row.factura.cliente, row.importe) + factura = row.factura + saldo_anterior = 0 + if row.numero == 1: + saldo_anterior = row.saldo_anterior + row.delete_instance() + cls._actualizar_saldos(cls, factura, saldo_anterior) + return + + @classmethod + def add(cls, mov, ids): + for i, importe in ids.items(): + fac = Facturas.get(Facturas.id==int(i)) + mov_ant, numero = cls._movimiento_anterior(cls, mov, fac) + nuevo = { + 'movimiento': mov, + 'factura': fac, + 'numero': numero, + 'importe': importe, + } + if mov_ant is None: + nuevo['saldo_anterior'] = float(fac.saldo) + else: + nuevo['saldo_anterior'] = float(mov_ant.saldo) + nuevo['saldo'] = nuevo['saldo_anterior'] - importe + FacturasPagos.create(**nuevo) + fac.saldo = nuevo['saldo'] + if nuevo['saldo'] == 0: + fac.pagada = True + fac.save() + cls._actualizar_saldo_cliente(cls, fac.cliente, importe * -1) + return + + +class PreFacturasImpuestos(BaseModel): + factura = ForeignKeyField(PreFacturas) + impuesto = ForeignKeyField(SATImpuestos) + base = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('factura',) + indexes = ( + (('factura', 'impuesto'), True), + ) + + @classmethod + def get_(cls, id): + data = { + 'traslados': [], + 'retenciones': [], + 'taxlocales': [], + } + + taxes = PreFacturasImpuestos.select().where( + PreFacturasImpuestos.factura==id) + + for tax in taxes: + if tax.impuesto.tipo == 'T': + title = 'Traslado {} {}'.format( + tax.impuesto.name, str(tax.impuesto.tasa)) + data['traslados'].append((title, str(tax.importe))) + elif tax.impuesto.tipo == 'R': + title = 'Retención {} {}'.format( + tax.impuesto.name, str(tax.impuesto.tasa)) + data['retenciones'].append((title, str(tax.importe))) + + return data + + +class CamposPersonalizados(BaseModel): + nombre = TextField() + slug = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + +class FacturasPersonalizados(BaseModel): + factura = ForeignKeyField(Facturas) + campo = TextField() + valor = TextField() + + class Meta: + order_by = ('factura',) + + +class Tickets(BaseModel): + cliente = ForeignKeyField(Socios, null=True) + serie = TextField(default='') + folio = IntegerField(default=0) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + forma_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + estatus = TextField(default='Generado') + notas = TextField(default='') + factura = ForeignKeyField(Facturas, null=True) + cancelado = BooleanField(default=False) + vendedor = TextField(default='') + comision = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + cambio = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('fecha',) + + def _get_folio(self, serie): + inicio = (Tickets + .select(fn.Max(Tickets.folio).alias('mf')) + .where(Tickets.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None: + inicio = 1 + else: + inicio += 1 + + return inicio + + def _without_tax(self, importe, obj): + # ~ Por ahora se asume que solo tiene IVA trasladado 0.16 + for tax in obj.impuestos: + tasa = 1.0 + float(tax.tasa) + base = round(importe / tasa, DECIMALES) + return base + + def _calcular_totales(self, ticket, productos): + subtotal = 0 + descuento_cfdi = 0 + totals_tax = {} + total_trasladados = None + + for producto in productos: + id_producto = producto.pop('id') + p = Productos.get(Productos.id==id_producto) + producto['descripcion'] = p.descripcion + producto['ticket'] = ticket.id + producto['producto'] = id_producto + + cantidad = float(producto['cantidad']) + valor_unitario = self._without_tax(self, producto['valor_unitario'], p) + descuento = float(producto['descuento']) + precio_final = valor_unitario - descuento + importe = round(cantidad * precio_final, DECIMALES) + + producto['cantidad'] = cantidad + producto['valor_unitario'] = valor_unitario + producto['descuento'] = descuento + producto['precio_final'] = precio_final + producto['importe'] = importe + + descuento_cfdi += descuento + subtotal += importe + + TicketsDetalle.create(**producto) + + if p.inventario: + p.existencia -= Decimal(cantidad) + p.save() + + base = producto['importe'] + for tax in p.impuestos: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES) + if tax.tipo == 'T' and tax.key != '000': + total_trasladados = (total_trasladados or 0) + impuesto_producto + + if tax.id in totals_tax: + totals_tax[tax.id].base += base + totals_tax[tax.id].importe += impuesto_producto + else: + tax.base = base + tax.importe = impuesto_producto + totals_tax[tax.id] = tax + + + for tax in totals_tax.values(): + if tax.tipo == 'E': + continue + + ticket_tax = { + 'ticket': ticket.id, + 'impuesto': tax.id, + 'base': tax.base, + 'importe': tax.importe, + } + TicketsImpuestos.create(**ticket_tax) + + total = subtotal + (total_trasladados or 0) + data = { + 'subtotal': subtotal + descuento, + 'descuento': descuento_cfdi, + 'total': total, + 'total_trasladados': total_trasladados, + } + return data + + def _get_serie(self, user, invoice=False): + default_serie = DEFAULT_SERIE_TICKET + if invoice: + default_serie = Folios.get_default() + + if user.sucursal is None: + return default_serie + + if invoice: + return user.sucursal.serie_facturas or default_serie + else: + return user.sucursal.serie_tickets or default_serie + + @classmethod + def get_notes(cls, tickets): + rows = Tickets.select(Tickets.notas).where(Tickets.id.in_(tickets)) + return '\n'.join([r.notas for r in rows]) + + @classmethod + def add(cls, values, user): + productos = util.loads(values.pop('productos')) + values['serie'] = cls._get_serie(cls, user) + values['folio'] = cls._get_folio(cls, values['serie']) + + with database_proxy.atomic() as txn: + obj = Tickets.create(**values) + totals = cls._calcular_totales(cls, obj, productos) + obj.subtotal = totals['subtotal'] + obj.descuento = totals['descuento'] + obj.total_trasladados = totals['total_trasladados'] + obj.total = totals['total'] + obj.save() + + row = { + 'id': obj.id, + 'serie': obj.serie, + 'folio': obj.folio, + 'fecha': obj.fecha, + 'estatus': obj.estatus, + 'total': obj.total, + } + data = {'ok': True, 'row': row} + return data + + def _get_folio_invoice(self, serie): + inicio = (Facturas + .select(fn.Max(Facturas.folio).alias('mf')) + .where(Facturas.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None: + inicio = 1 + else: + inicio += 1 + + return inicio + + def _cancel_tickets(self, invoice, tickets): + query = (Tickets + .update(estatus='Facturado', cancelado=True, factura=invoice) + .where(Tickets.id.in_(tickets)) + ) + result = query.execute() + return result + + def _calculate_totals_invoice(self, invoice, tickets): + subtotal = 0 + descuento_cfdi = 0 + totals_tax = {} + total_trasladados = None + total_retenciones = None + notes = Tickets.get_notes(tickets) + + details = TicketsDetalle.select().where(TicketsDetalle.ticket.in_(tickets)) + + for detail in details: + product = {} + p = detail.producto + product['unidad'] = p.unidad.key + product['clave'] = p.clave + product['clave_sat'] = p.clave_sat + + product['factura'] = invoice.id + product['producto'] = p.id + product['descripcion'] = detail.descripcion + + cantidad = float(detail.cantidad) + valor_unitario = float(detail.valor_unitario) + descuento = float(detail.descuento) + precio_final = valor_unitario - descuento + importe = round(cantidad * precio_final, DECIMALES) + + product['cantidad'] = cantidad + product['valor_unitario'] = valor_unitario + product['descuento'] = round(descuento * cantidad, DECIMALES) + product['precio_final'] = precio_final + product['importe'] = round(cantidad * valor_unitario, DECIMALES) + + descuento_cfdi += product['descuento'] + subtotal += product['importe'] + + FacturasDetalle.create(**product) + + base = product['importe'] - product['descuento'] + for tax in p.impuestos: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES) + if tax.tipo == 'T' and tax.key != '000': + total_trasladados = (total_trasladados or 0) + impuesto_producto + elif tax.tipo == 'R' and tax.key != '000': + total_retenciones = (total_retenciones or 0) + impuesto_producto + elif tax.tipo == 'T' and tax.key == '000': + locales_traslados += impuesto_producto + elif tax.tipo == 'R' and tax.key == '000': + locales_retenciones += impuesto_producto + + if tax.id in totals_tax: + totals_tax[tax.id].base += base + totals_tax[tax.id].suma_impuestos += impuesto_producto + else: + tax.base = base + tax.suma_impuestos = impuesto_producto + totals_tax[tax.id] = tax + + for tax in totals_tax.values(): + if tax.tipo == 'E': + continue + + invoice_tax = { + 'factura': invoice.id, + 'impuesto': tax.id, + 'base': tax.base, + 'importe': tax.suma_impuestos, + } + FacturasImpuestos.create(**invoice_tax) + + total = subtotal - descuento_cfdi + \ + (total_trasladados or 0) - (total_retenciones or 0) + total_mn = round(total * invoice.tipo_cambio, DECIMALES) + data = { + 'subtotal': subtotal, + 'descuento': descuento_cfdi, + 'total': total, + 'total_mn': total_mn, + 'total_trasladados': total_trasladados, + 'total_retenciones': total_retenciones, + 'notas': notes, + } + return data + + @classmethod + def invoice(cls, values, user): + is_invoice_day = util.get_bool(values['is_invoice_day']) + id_client = int(values['client']) + tickets = util.loads(values['tickets']) + + if is_invoice_day: + filters = ( + Socios.rfc == 'XAXX010101000' and + Socios.slug == 'publico_en_general') + try: + client = Socios.get(filters) + except Socios.DoesNotExist: + msg = 'No existe el cliente Público en General. Agregalo primero.' + data = {'ok': False, 'msg': msg} + return data + else: + client = Socios.get(Socios.id==id_client) + + if client.forma_pago is None: + msg = 'La Forma de Pago del cliente, no esta asignada' + data = {'ok': False, 'msg': msg} + return data + + emisor = Emisor.select()[0] + data = {} + data['cliente'] = client + data['serie'] = cls._get_serie(cls, user, True) + data['folio'] = cls._get_folio_invoice(cls, data['serie']) + data['forma_pago'] = client.forma_pago.key + data['tipo_cambio'] = 1.00 + data['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + if client.uso_cfdi is None: + data['uso_cfdi'] = 'P01' + else: + data['uso_cfdi'] = client.uso_cfdi.key + data['regimen_fiscal'] = emisor.regimenes[0].key + + with database_proxy.atomic() as txn: + obj = Facturas.create(**data) + totals = cls._calculate_totals_invoice(cls, obj, tickets) + obj.subtotal = totals['subtotal'] + obj.descuento = totals['descuento'] + obj.total_trasladados = totals['total_trasladados'] + obj.total_retenciones = totals['total_retenciones'] + obj.total = totals['total'] + obj.saldo = totals['total'] + obj.total_mn = totals['total_mn'] + obj.notas = totals['notas'] + obj.save() + cls._cancel_tickets(cls, obj, tickets) + + msg = 'Factura generada correctamente.

Enviando a timbrar' + data = {'ok': True, 'msg': msg, 'id': obj.id} + return data + + def _update_inventory_if_cancel(self, id): + products = TicketsDetalle.select().where(TicketsDetalle.ticket==id) + for p in products: + if p.producto.inventario: + p.producto.existencia += p.cantidad + p.producto.save() + return + + @classmethod + def cancel(cls, values): + id = int(values['id']) + msg = 'Ticket cancelado correctamente' + u = {'cancelado': True, 'estatus': 'Cancelado'} + with database_proxy.atomic() as txn: + obj = Tickets.update(**u).where(Tickets.id==id) + result = bool(obj.execute()) + if result: + cls._update_inventory_if_cancel(cls, id) + row = {'estatus': 'Cancelado'} + return {'ok': result, 'row': row, 'msg': msg} + + def _get_filters(self, values): + opt = values.get('opt', '') + if not opt: + return + + if opt == 'active': + filters = (Tickets.cancelado==False) + return filters + + if opt == 'today': + t = util.today() + filters = ( + (Tickets.fecha.day == t.day) & + (Tickets.fecha.month == t.month) & + (Tickets.fecha.year == t.year) + ) + return filters + + if opt == 'yearmonth': + if values['year'] == '-1': + fy = (Tickets.fecha.year > 0) + else: + fy = (Tickets.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (Tickets.fecha.month > 0) + else: + fm = (Tickets.fecha.month == int(values['month'])) + filters = (fy & fm) + return filters + + if opt == 'dates': + dates = util.loads(values['range']) + filters = Tickets.fecha.between( + util.get_date(dates['start']), + util.get_date(dates['end'], True) + ) + return filters + + return + + @classmethod + def get_by(cls, values): + filters = cls._get_filters(cls, values) + + rows = tuple(Tickets + .select( + Tickets.id, + Tickets.serie, + Tickets.folio, + Tickets.fecha, + Tickets.estatus, + Tickets.total) + .where(filters) + .dicts() + ) + return {'ok': True, 'rows': rows} + + @classmethod + def filter_years(cls): + data = [{'id': -1, 'value': 'Todos'}] + rows = (Tickets + .select(Tickets.fecha.year.alias('year')) + .group_by(Tickets.fecha.year) + .order_by(Tickets.fecha.year) + ) + if not rows is None: + data += [{'id': int(r.year), 'value': int(r.year)} for r in rows] + return tuple(data) + + def _get_info_to_pdf(self, id): + data = {} + obj = Tickets.select().where(Tickets.id==id).dicts()[0] + + formapago = SATFormaPago.get(SATFormaPago.key==obj['forma_pago']) + + emisor = util.get_dict(Emisor.select().dicts()[0]) + + emisor['nointerior'] = emisor['no_interior'] + emisor['noexterior'] = emisor['no_exterior'] + emisor['codigopostal'] = emisor['codigo_postal'] + + data['is_ticket'] = True + data['cancelada'] = obj['cancelado'] + data['donativo'] = '' + + data['comprobante'] = obj + data['comprobante']['version'] = 'ticket' + data['comprobante']['folio'] = str(data['comprobante']['folio']) + data['comprobante']['seriefolio'] = '{}-{}'.format( + data['comprobante']['serie'], data['comprobante']['folio']) + data['comprobante']['fecha'] = str(data['comprobante']['fecha']) + # ~ data['comprobante']['lugarexpedicion'] = \ + # ~ 'C.P. de Expedición: {}'.format( + # ~ data['comprobante']['lugar_expedicion']) + data['comprobante']['formadepago'] = str(formapago) + data['comprobante']['totalenletras'] = util.to_letters( + data['comprobante']['total'], 'peso') + + data['emisor'] = emisor + data['receptor'] = {'nombre': 'Público en general'} + + data['conceptos'] = TicketsDetalle.get_by_ticket(id) + data['totales'] = {} + data['totales']['total'] = str(data['comprobante']['total']) + data['totales']['subtotal'] = str(data['comprobante']['subtotal']) + data['totales']['moneda'] = 'peso' + data['totales']['traslados'] = () + data['totales']['retenciones'] = () + data['totales']['taxlocales'] = () + data['timbre'] = {} + data['donataria'] = {} + data['ine'] = {} + + # ~ if obj['descuento']: + # ~ data['totales']['descuento'] = float(obj['descuento']) + + return data + + @classmethod + def get_pdf(cls, id): + obj = Tickets.get(Tickets.id==id) + name = '{}{}.pdf'.format(obj.serie, obj.folio) + data = cls._get_info_to_pdf(cls, id) + doc = util.to_pdf(data, data['emisor']['rfc']) + return doc, name + + def _format_ticket(self, id): + emisor = util.get_dict(Emisor.select().dicts()[0]) + regimen = Emisor.select()[0].regimenes[0].name + ticket = Tickets.select().where(Tickets.id==id).dicts()[0] + products = TicketsDetalle.get_by_print(id) + emisor['name'] = '{}\n'.format(emisor['nombre']) + emisor['rfc'] = 'RFC: {}\n'.format(emisor['rfc']) + emisor['regimen'] = 'Regimen: {}\n'.format(regimen) + interior = '' + if emisor['no_interior']: + interior = ', {}'.format(emisor['no_interior']) + colonia = '' + if emisor['colonia']: + colonia = ', Col. {}'.format(emisor['colonia']) + municipio = '' + if emisor['municipio']: + municipio = ', {}'.format(emisor['municipio']) + estado = '' + if emisor['estado']: + estado = ', {}'.format(emisor['estado']) + cp = '' + if emisor['codigo_postal']: + cp = ', C.P. {}'.format(emisor['codigo_postal']) + pais = '' + if emisor['pais']: + pais = ', {}'.format(emisor['pais']) + direccion = '{} {}{}{}{}{}{}{}\n\n'.format(emisor['calle'], + emisor['no_exterior'], interior, colonia, municipio, estado, cp, + pais) + emisor['address'] = direccion + + ticket['title'] = 'Ticket: {}{}\n\n'.format(ticket['serie'], + ticket['folio']) + ticket['date'] = 'Fecha y hora: {}\n'.format(ticket['fecha']) + ticket['letters'] = '{}\n\n'.format( + util.to_letters(ticket['total'], 'peso')) + ticket['total'] = 'TOTAL: $ {:>12,.2f}\n\n'.format(ticket['total']) + + data = { + 'emisor': emisor, + 'receptor': {'name': '{}\n\n'.format(PUBLIC)}, + 'ticket': ticket, + 'products': products, + } + return data + + def _get_info_printer(self): + info = {} + value = Configuracion.get_('txt_ticket_printer') + if not value: + return info + + values = value.split(':') + if len(values) == 1: + info = {'ip': values[0], 'usb': ()} + elif len(values) == 2: + info = {'ip': '', 'usb': (int(values[0], 16), int(values[1], 16))} + elif len(values) == 5: + info = {'ip': '', 'usb': ( + int(values[0], 16), + int(values[1], 16), + int(values[2]), + int(values[3], 16), + int(values[4], 16), + ) + } + + return info + + @classmethod + def printer(cls, values): + id = int(values['id']) + + info_printer = cls._get_info_printer(cls) + if not info_printer: + msg = 'Es necesario configurar una impresora.' + result = {'ok': False, 'msg': msg} + return result + + data = cls._format_ticket(cls, id) + result = util.print_ticket(data, info_printer) + msg = 'Ticket impreso correctamente' + if not result: + msg = 'Asegurate de que la impresora este conectada y funcionando.' + result = {'ok': result, 'msg': msg} + return result + + +class TicketsDetalle(BaseModel): + ticket = ForeignKeyField(Tickets) + producto = ForeignKeyField(Productos, null=True) + descripcion = TextField(default='') + cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('ticket',) + + def _with_tax(self, product): + precio_final = float(product.precio_final) + base = float(product.precio_final) + for tax in product.producto.impuestos: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES) + precio_final += impuesto_producto + + return precio_final + + @classmethod + def get_by_print(cls, id): + products = TicketsDetalle.select().where(TicketsDetalle.ticket==id) + lines = [] + for p in products: + price_with_tax = cls._with_tax(cls, p) + importe = round(price_with_tax * float(p.cantidad), DECIMALES) + l = '{:>6,.2f} {:<4} {:<14} {:>9,.2f} {:>10,.2f}\n'.format( + p.cantidad, p.producto.unidad.name[:5], p.descripcion[:14], + price_with_tax, importe + ) + lines.append(l) + + return lines + + @classmethod + def get_by_ticket(cls, id): + data = [] + + productos = TicketsDetalle.select().where(TicketsDetalle.ticket==id) + + for p in productos: + producto = {} + producto['noidentificacion'] = p.producto.clave + producto['descripcion'] = p.descripcion + producto['unidad'] = p.producto.unidad.name + producto['cantidad'] = str(p.cantidad) + price_with_tax = cls._with_tax(cls, p) + producto['valorunitario'] = str(price_with_tax) + importe = round(price_with_tax * float(p.cantidad), DECIMALES) + producto['importe'] = str(importe) + data.append(producto) + + return data + + +class TicketsImpuestos(BaseModel): + ticket = ForeignKeyField(Tickets) + impuesto = ForeignKeyField(SATImpuestos) + base = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('ticket',) + indexes = ( + (('ticket', 'impuesto'), True), + ) + + +class SeriesProductos(BaseModel): + factura = ForeignKeyField(FacturasDetalle, null=True) + ticket = ForeignKeyField(TicketsDetalle, null=True) + serie = TextField(default='') + + class Meta: + order_by = ('serie',) + + +class Departamentos(BaseModel): + nombre = TextField(default='', unique=True) + descripcion = TextField(default='') + + class Meta: + order_by = ('nombre',) + + +class Puestos(BaseModel): + departamento = ForeignKeyField(Departamentos, null=True) + nombre = TextField(default='', unique=True) + descripcion = TextField(default='') + + class Meta: + order_by = ('nombre',) + + @classmethod + def get_by_depto(cls, puesto, depto): + departamento = None + if depto: + with database_proxy.transaction(): + departamento, _ = Departamentos.get_or_create(nombre=depto) + data = {'departamento': departamento, 'nombre': puesto} + obj, _ = Puestos.get_or_create(**data) + return obj + + +class Empleados(BaseModel): + num_empleado = TextField(default='') + rfc = TextField(default='', unique=True) + curp = TextField(default='', unique=True) + nombre = TextField(default='') + paterno = TextField(default='') + materno = TextField(default='') + nombre_completo = TextField(default='') + es_activo = BooleanField(default=True) + es_extranjero = BooleanField(default=False) + fecha_alta = DateField(default=util.now) + fecha_ingreso = DateField(null=True) + imss = TextField(default='') + tipo_contrato = ForeignKeyField(SATTipoContrato) + es_sindicalizado = BooleanField(default=False) + tipo_jornada = ForeignKeyField(SATTipoJornada, null=True) + tipo_regimen = ForeignKeyField(SATTipoRegimen) + puesto = ForeignKeyField(Puestos, null=True) + riesgo_puesto = ForeignKeyField(SATRiesgoPuesto, null=True) + periodicidad_pago = ForeignKeyField(SATPeriodicidadPago) + banco = ForeignKeyField(SATBancos, null=True) + cuenta_bancaria = TextField(default='') + clabe = TextField(default='') + salario_base = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + salario_diario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + estado = ForeignKeyField(SATEstados) + codigo_postal = TextField(default='') + notas = TextField(default='') + correo = TextField(default='') + + class Meta: + order_by = ('nombre_completo',) + indexes = ( + (('num_empleado', 'rfc'), True), + ) + + def _validate_import(self, row): + sn = {'si': True, 'no': False} + data = row.copy() + data['nombre_completo'] = '{} {} {}'.format( + row['nombre'], row['paterno'], row['materno']).strip() + 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']) + data['tipo_regimen'] = SATTipoRegimen.get_by_key(row['tipo_regimen']) + data['puesto'] = Puestos.get_by_depto(row['puesto'], row['departamento']) + data['riesgo_puesto'] = SATRiesgoPuesto.get_by_key(row['riesgo_puesto']) + data['periodicidad_pago'] = SATPeriodicidadPago.get_by_key(row['periodicidad_pago']) + data['banco'] = SATBancos.get_by_key(row['banco']) + data['estado'] = SATEstados.get_by_key(row['estado']) + del data['departamento'] + return data + + def _import(self): + emisor = Emisor.select()[0] + rows, msg = util.import_employees(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + 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(): + if Empleados.select().where(w).exists(): + q = Empleados.update(**data).where(w) + q.execute() + ea += 1 + else: + obj = Empleados.create(**data) + en += 1 + + msg = 'Empleados encontrados: {}
'.format(len(rows)) + msg += 'Empleados nuevos: {}
'.format(en) + msg += 'Empleados actualizados: {}
'.format(ea) + msg += 'Empleados no importados: {}'.format(len(rows) - en - ea) + return {'ok': True, 'msg': msg} + + def _get(self): + rows = (Empleados + .select( + Empleados.id, + Empleados.num_empleado, + Empleados.rfc, + Empleados.curp, + Empleados.nombre_completo, + Empleados.imss, + Empleados.salario_base, + Empleados.salario_diario, + Empleados.fecha_ingreso) + .dicts() + ) + return {'ok': True, 'rows': tuple(rows)} + + @classmethod + def get_by(cls, values): + if not 'opt' in values: + return cls._get(cls) + + if values['opt'] == 'import': + return cls._import(cls) + + @classmethod + def remove(cls, id): + try: + q = Empleados.delete().where(Empleados.id==id) + return bool(q.execute()) + except IntegrityError: + return False + + +class CfdiNomina(BaseModel): + empleado = ForeignKeyField(Empleados) + version = TextField(default=CURRENT_CFDI) + 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) + forma_pago = TextField(default='') + condiciones_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + moneda = TextField(default='MXN') + tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6, + auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + tipo_comprobante = TextField(default='N') + metodo_pago = TextField(default='PUE') + lugar_expedicion = TextField(default='') + confirmacion = TextField(default='') + uso_cfdi = TextField(default='') + total_retenciones = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + xml = TextField(default='') + uuid = UUIDField(null=True) + estatus = TextField(default='Guardado') + estatus_sat = TextField(default='Vigente') + regimen_fiscal = TextField(default='') + notas = TextField(default='') + saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + pagada = BooleanField(default=False) + cancelada = BooleanField(default=False) + fecha_cancelacion = DateTimeField(null=True) + acuse = TextField(default='') + tipo_relacion = TextField(default='') + error = TextField(default='') + version_nomina = TextField(default=CURRENT_CFDI_NOMINA) + registro_patronal = TextField(default='') + rfc_patron_origen = TextField(default='') + tipo_nomina = ForeignKeyField(SATTipoNomina) + fecha_pago = DateField() + fecha_inicial_pago = DateField() + fecha_final_pago = DateField() + dias_pagados = DecimalField(default=0.0, max_digits=12, decimal_places=2, + auto_round=True) + origen_recurso = ForeignKeyField(SATOrigenRecurso, null=True) + monto_recurso_propio = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + class Meta: + order_by = ('fecha',) + + 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 = inicio + 1 + + if folio > new: + new = folio + + return new + + def _validate_nomina(self, row): + sn = {'si': True, 'no': False} + data = row.copy() + 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 = 0.0 + exento = 0.0 + if isinstance(row[i * 2], float): + gravado = round(row[i * 2], DECIMALES) + if isinstance(row[i * 2 + 1], float): + 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 = 0.0 + if isinstance(value, float): + 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 = 0.0 + if isinstance(value, float): + 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 = 0 + if isinstance(row[i * 4], float): + days = int(row[i * 4]) + key = row[i * 4 + 1] + the = SATTipoHoras.get_by_key(key) + if the is None: + continue + + hours = 0 + if isinstance(row[i * 4 + 2], float): + hours = int(row[i * 4 + 2]) + importe = 0.0 + if isinstance(row[i * 4 + 3], float): + 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 = 0 + if isinstance(row[i * 3 + 1], float): + days = int(row[i * 3 + 1]) + importe = 0.0 + if isinstance(row[i * 3 + 2], float): + importe = round(row[i * 3 + 2], DECIMALES) + if not days or not importe: + continue + + new = { + 'dias': ti, + 'tipo': days, + 'importe': importe, + } + data.append(new) + + return data, '' + + def _validate_exists(self, values): + result = (CfdiNomina + .select() + .where( + (CfdiNomina.empleado==values['empleado']) & + (CfdiNomina.fecha_pago==values['fecha_pago']) & + (CfdiNomina.fecha_inicial_pago==values['fecha_inicial_pago']) & + (CfdiNomina.fecha_final_pago==values['fecha_final_pago']) & + (CfdiNomina.total==values['total'])) + .exists()) + return result + + def _import(self): + util.log_file('nomina', kill=True) + emisor = Emisor.select()[0] + data, msg = util.import_nomina(emisor.rfc) + if not data: + return {'ok': False, 'msg': msg} + + 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) + + new_nomina['subtotal'] = totals['subtotal'] + new_nomina['descuento'] = totals['descuento'] + new_nomina['total'] = totals['total'] + new_nomina['total_mn'] = totals['total'] + + if self._validate_exists(self, new_nomina): + info = '{}'.format(new_nomina['empleado'].nombre_completo) + msg = 'Nomina existente: {}'.format(info) + util.log_file('nomina', msg) + continue + + try: + with database_proxy.transaction(): + 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) + except Exception as e: + msg = 'ERROR: {}-{}'.format(new_nomina['serie'], new_nomina['folio']) + util.log_file('nomina', msg) + util.log_file('nomina', str(e)) + continue + + msg = 'Nómina importada correctamente' + return {'ok': True, 'msg': msg} + + def _get(self, where=''): + if not where: + where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) + rows = (CfdiNomina + .select( + 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) + .order_by(CfdiNomina.id) + .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.replace('-', '') + elif empleado.cuenta_bancaria: + nomina_receptor['CuentaBancaria'] = empleado.cuenta_bancaria.replace('-', '') + 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), + } + if totals.total_retenciones: + 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): + msg = '' + where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) + rows = CfdiNomina.select().where(where).order_by(CfdiNomina.id) + 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 + util.log_file('nomina', msg) + else: + msg = 'Error la timbrar: {}-{}, {}'.format(row.serie, row.folio, msg) + util.log_file('nomina', msg) + 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 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) + clave_sat = TextField(default=DEFAULT_SAT_NOMINA['CLAVE']) + cantidad = DecimalField(default=1.0, max_digits=18, decimal_places=6, + auto_round=True) + clave_unidad = TextField(default=DEFAULT_SAT_NOMINA['UNIDAD']) + descripcion = TextField(default=DEFAULT_SAT_NOMINA['DESCRIPCION']) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('cfdi',) + + +class CfdiNominaTotales(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_percepciones = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_deducciones = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_otros_pagos = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_sueldos = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_separacion = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_jubilacion = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_retenciones = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_otras_deducciones = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaJubilacion(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + total_una_exhibicion = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total_parcialidad = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + monto_diario = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaSeparacion(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + total_pagado = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + years_servicio = IntegerField(default=0) + ultimo_sueldo = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +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, + auto_round=True) + valor_mercado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + precio_al_ortorgarse = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +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) + + +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, + auto_round=True) + saldo_a_favor = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + year = IntegerField(default=0) + remanente_saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaIncapacidad(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + dias = IntegerField(default=0) + tipo = ForeignKeyField(SATTipoIncapacidad) + importe = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaHorasExtra(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + dias = IntegerField(default=0) + tipos_horas = ForeignKeyField(SATTipoHoras) + horas_extra = IntegerField(default=0) + importe_pagado = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaSubcontratos(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + rfc = TextField(default='') + porcentaje = DecimalField(default=0.0, max_digits=12, decimal_places=2, + auto_round=True) + + class Meta: + order_by = ('cfdi',) + + +class CfdiNominaOtros(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + node = TextField(default='') + key = TextField(default='') + value = TextField(default='') + + +class CfdiNominaRelacionados(BaseModel): + cfdi = ForeignKeyField(CfdiNomina, related_name='original') + cfdi_origen = ForeignKeyField(CfdiNomina, related_name='relacion') + + class Meta: + order_by = ('cfdi',) + + +def authenticate(args): + respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''} + values = util.get_con(args['rfc']) + if not values: + return respuesta, None + + conectar(values) + try: + obj = Usuarios.get(usuario=args['usuario'], es_activo=True) + except Usuarios.DoesNotExist: + return respuesta, None + except ProgrammingError as e: + log.error(e) + return respuesta, None + + if not obj.contraseña.check_password(args['contra']): + return respuesta, None + + obj.ultimo_ingreso = util.now() + obj.save() + respuesta['msg'] = '' + respuesta['login'] = True + respuesta['user'] = str(obj) + respuesta['super'] = obj.es_superusuario + #~ respuesta['admin'] = obj.es_superusuario or obj.es_admin + return respuesta, obj + + +def get_cp(cp): + con = sqlite3.connect(PATH_CP) + cursor = con.cursor() + sql = """ + SELECT colonia, municipio, estado + FROM colonias, municipios, estados + WHERE colonias.id_municipio=municipios.id + AND municipios.id_estado=estados.id + AND cp=? + ORDER BY colonia""" + cursor.execute(sql, (cp,)) + rows = cursor.fetchall() + cursor.close() + con.close() + + data = {} + if rows: + data = { + 'estado': rows[0][2], + 'municipio': rows[0][1], + } + if len(rows) == 1: + data['colonia'] = rows[0][0] + else: + data['colonia'] = [r[0] for r in rows] + return data + + +def get_sat_key(key): + return util.get_sat_key('productos', key) + + +def get_sat_monedas(key): + return util.get_sat_monedas(key) + + +def get_sat_unidades(key): + return util.get_sat_unidades(key) + + +def get_sat_productos(key): + return util.get_sat_productos(key) + + +def get_title_app(by=1): + html = { + 1: '{}', + 2: 'Bienvenido a {}', + 3: '{}', + } + return html[by].format(TITLE_APP) + + +def test_correo(values): + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': util.get_bool(values['correo_ssl']), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + ccp = values.get('correo_copia', '') + options = { + 'para': values['correo_usuario'], + 'copia': ccp, + 'confirmar': util.get_bool(values['correo_confirmacion']), + 'asunto': values['correo_asunto'], + 'mensaje': values['correo_mensaje'].replace('\n', '
'), + 'files': [], + } + data= { + 'server': server, + 'options': options, + } + return util.send_mail(data) + + +def _init_values(rfc): + data = ( + {'clave': 'version', 'valor': VERSION}, + {'clave': 'migracion', 'valor': '0'}, + {'clave': 'rfc_publico', 'valor': 'XAXX010101000'}, + {'clave': 'rfc_extranjero', 'valor': 'XEXX010101000'}, + {'clave': 'decimales', 'valor': '2'}, + {'clave': 'path_key', 'valor': ''}, + {'clave': 'path_cer', 'valor': ''}, + ) + for row in data: + try: + with database_proxy.atomic() as txn: + Configuracion.create(**row) + except IntegrityError: + pass + + if not Certificado.select().count(): + Certificado.create(rfc=rfc) + + log.info('Valores iniciales insertados...') + return + + +def _crear_tablas(rfc): + tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion, + Folios, Registro, CamposPersonalizados, + Emisor, Facturas, FacturasDetalle, FacturasImpuestos, FacturasPagos, + FacturasRelacionadas, FacturasComplementos, FacturasPersonalizados, + SeriesProductos, Almacenes, Productos, RangosPrecios, Sucursales, + PreFacturas, PreFacturasDetalle, PreFacturasImpuestos, + PreFacturasRelacionadas, Tickets, TicketsDetalle, TicketsImpuestos, + SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, + SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos, + SATNivelesEducativos, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago, + SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras, + SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago, + SATTipoPercepcion, SATTipoRegimen, + Socios, Contactos, ContactoCorreos, ContactoDirecciones, Empleados, + ContactoTelefonos, Departamentos, Puestos, + Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco, + TipoCorreo, TipoDireccion, TipoPariente, TipoResponsable, TipoTelefono, + TipoTitulo, TipoMovimientoAlumno, TipoMovimientoAlmacen, + CfdiPagos, NivelesEducativos, Alumnos, AlumnosParientes, Grupos, + ParienteDirecciones, ParienteTelefonos, ParienteCorreos, + CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle, + CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion, + CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones, + CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, + CfdiNominaTotales, + Emisor.regimenes.get_through_model(), + Socios.tags.get_through_model(), + Productos.impuestos.get_through_model(), + Productos.tags.get_through_model(), + ] + log.info('Creando tablas...') + database_proxy.create_tables(tablas, True) + log.info('Tablas creadas correctamente...') + + usuarios = ( + {'usuario': 'superadmin', 'contraseña': DEFAULT_PASSWORD, + 'es_superusuario': True}, + {'usuario': 'admin', 'contraseña': DEFAULT_PASSWORD, 'es_admin': True}, + ) + + for usuario in usuarios: + try: + with database_proxy.atomic() as txn: + Usuarios.create(**usuario) + log.info('Usuario creado correctamente...') + except IntegrityError: + log.info('Usuario ya existe...') + + _init_values(rfc) + _importar_valores('', rfc) + + return True + + +def _migrate_tables(): + from playhouse.migrate import PostgresqlMigrator, migrate + + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + tablas = [Sucursales, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago, + SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras, + SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago, + SATTipoPercepcion, SATTipoRegimen, Departamentos, Puestos, Empleados, + CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle, + CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion, + CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones, + CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, + CfdiNominaTotales, + ] + log.info('Creando tablas nuevas...') + database_proxy.create_tables(tablas, True) + log.info('Tablas creadas correctamente...') + + log.info('Iniciando migración de tablas...') + migrations = [] + migrator = PostgresqlMigrator(database_proxy) + + columns = [c.name for c in database_proxy.get_columns('usuarios')] + if not 'sucursal_id' in columns: + sucursal = ForeignKeyField(Sucursales, null=True, to_field=Sucursales.id) + migrations.append( + migrator.add_column('usuarios', 'sucursal_id', sucursal)) + + columns = [c.name for c in database_proxy.get_columns('emisor')] + if not 'registro_patronal' in columns: + registro_patronal = TextField(default='') + migrations.append( + migrator.add_column( + 'emisor', 'registro_patronal', registro_patronal)) + if not 'curp' in columns: + curp = TextField(default='') + migrations.append( + migrator.add_column('emisor', 'curp', curp)) + + if migrations: + with database_proxy.atomic() as txn: + migrate(*migrations) + + log.info('Tablas migradas correctamente...') + return + + +def _agregar_superusuario(): + args = util.get_con() + if not args: + return + + conectar(args) + usuario = input('Introduce el nuevo nombre para el superusuario: ').strip() + if not usuario: + msg = 'El nombre de usuario es requerido' + log.erro(msg) + return + ok, contraseña = util.get_pass() + if not ok: + log.error(contraseña) + return + try: + obj = Usuarios.create( + usuario=usuario, contraseña=contraseña, es_superusuario=True) + except IntegrityError: + msg = 'El usuario ya existe' + log.error(msg) + return + + log.info('SuperUsuario creado correctamente...') + return + + +def _cambiar_contraseña(): + args = util.get_con() + if not args: + return + + conectar(args) + usuario = input('Introduce el nombre de usuario: ').strip() + if not usuario: + msg = 'El nombre de usuario es requerido' + log.error(msg) + return + + try: + obj = Usuarios.get(usuario=usuario) + except Usuarios.DoesNotExist: + msg = 'El usuario no existe' + log.error(msg) + return + + ok, contraseña = util.get_pass() + if not ok: + log.error(contraseña) + return + + obj.contraseña = contraseña + obj.save() + + log.info('Contraseña cambiada correctamente...') + return + + +def _add_emisor(rfc, args): + util._valid_db_companies() + con = sqlite3.connect(COMPANIES) + cursor = con.cursor() + sql = """ + INSERT INTO names + VALUES (?, ?)""" + try: + cursor.execute(sql, (rfc, args)) + except sqlite3.IntegrityError as e: + log.error(e) + return False + + con.commit() + cursor.close() + con.close() + return True + + +def _delete_emisor(rfc): + util._valid_db_companies() + con = sqlite3.connect(COMPANIES) + cursor = con.cursor() + sql = """ + DELETE FROM names + WHERE rfc = ?""" + try: + cursor.execute(sql, (rfc,)) + except Exception as e: + log.error(e) + return False + + con.commit() + cursor.close() + con.close() + return True + + +def _iniciar_bd(): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + if _crear_tablas(rfc): + return + + log.error('No se pudieron crear las tablas') + return + + +def _agregar_rfc(no_bd): + rfc = input('Introduce el nuevo RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + datos = input('Introduce los datos de conexión: ').strip() + if not datos: + msg = 'Los datos de conexión son requeridos' + log.error(msg) + return + + opt = util.parse_con(datos) + if not opt: + log.error('Datos de conexión incompletos') + return + + args = opt.copy() + if conectar(args): + if _add_emisor(rfc, util.dumps(opt)): + if no_bd: + log.info('RFC agregado correctamente...') + return + _crear_tablas(rfc) + log.info('RFC agregado correctamente...') + return + + log.error('No se pudo agregar el RFC') + return + + +def _borrar_rfc(): + rfc = input('Introduce el RFC a borrar: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + confirm = input('¿Estás seguro de borrar el RFC?') + + if _delete_emisor(rfc): + util.delete_db(rfc.lower()) + log.info('RFC borrado correctamente...') + return + + log.error('No se pudo borrar el RFC') + return + + +def _listar_rfc(detalle): + data = util.get_rfcs() + for row in data: + if detalle: + msg = 'RFC: {}\n\t{}'.format(row[0], row[1]) + else: + msg = 'RFC: {}'.format(row[0]) + log.info(msg) + return + + +def get_empresas(): + data = util.get_rfcs() + rows = [] + for row in data: + rows.append({'delete': '-', 'rfc': row[0]}) + return tuple(rows) + + +def empresa_agregar(rfc, no_bd): + rfc = rfc.upper() + if util.get_con(rfc): + msg = 'El RFC ya esta dado de alta' + return {'ok': False, 'msg': msg} + + user = rfc.replace('&', '').lower() + if not no_bd: + if not util.crear_rol(user): + msg = 'No se pudo crear el usuario, es probable que ya exista' + return {'ok': False, 'msg': msg} + + if not util.crear_db(user): + msg = 'No se pudo crear la base de datos' + return {'ok': False, 'msg': msg} + + args = { + "type": "postgres", + "name": user, + "user": user, + "password": user, + } + if not no_bd: + if not conectar(args.copy()): + msg = 'No se pudo conectar a la base de datos' + return {'ok': False, 'msg': msg} + + if not _add_emisor(rfc, util.dumps(args)): + msg = 'No se pudo guardar el nuevo emisor' + return {'ok': False, 'msg': msg} + + if not no_bd: + if not _crear_tablas(rfc): + msg = 'No se pudo crear las tablas' + return {'ok': False, 'msg': msg} + + msg = 'Emisor dado de alta correctamente' + row = {'delete': '-', 'rfc': rfc} + return {'ok': True, 'msg': msg, 'row': row} + + +def empresa_borrar(rfc): + if _delete_emisor(rfc): + util.delete_db(rfc.lower()) + return True + + +def _importar_valores(archivo='', rfc=''): + if not rfc: + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + if not archivo: + archivo = INIT_VALUES + + log.info('Importando datos...') + regimen = '' + rows = util.loads(open(archivo, 'r').read()) + for row in rows: + log.info('\tImportando tabla: {}'.format(row['tabla'])) + table = globals()[row['tabla']] + for r in row['datos']: + try: + with database_proxy.atomic() as txn: + table.create(**r) + except IntegrityError: + pass + + log.info('Importación terminada...') + return + + +def _importar_socios(rows): + log.info('\tImportando Clientes...') + totals = len(rows) + for i, row in enumerate(rows): + msg = '\tGuardando cliente {} de {}'.format(i+1, totals) + log.info(msg) + try: + with database_proxy.atomic() as txn: + Socios.create(**row) + except IntegrityError: + msg = '\tSocio existente: {}'.format(row['nombre']) + log.info(msg) + log.info('\tClientes importados...') + return + + +def _existe_factura(row): + filtro = (Facturas.uuid==row['uuid']) + if row['uuid'] is None: + filtro = ( + (Facturas.serie==row['serie']) & + (Facturas.folio==row['folio']) + ) + return Facturas.select().where(filtro).exists() + + +def _importar_facturas(rows): + log.info('\tImportando Facturas...') + totals = len(rows) + for i, row in enumerate(rows): + msg = '\tGuardando factura {} de {}'.format(i+1, totals) + log.info(msg) + + try: + detalles = row.pop('detalles') + impuestos = row.pop('impuestos') + cliente = row.pop('cliente') + row['cliente'] = Socios.get(**cliente) + with database_proxy.atomic() as txn: + if _existe_factura(row): + msg = '\tFactura existente: {}{}'.format( + row['serie'], row['folio']) + log.info(msg) + continue + obj = Facturas.create(**row) + for detalle in detalles: + detalle['factura'] = obj + FacturasDetalle.create(**detalle) + for impuesto in impuestos: + imp = SATImpuestos.get(**impuesto['filtro']) + new = { + 'factura': obj, + 'impuesto': imp, + 'importe': impuesto['importe'], + } + try: + with database_proxy.atomic() as txn: + FacturasImpuestos.create(**new) + except IntegrityError as e: + pass + + except IntegrityError as e: + print (e) + msg = '\tFactura: id: {}'.format(row['serie'] + str(row['folio'])) + log.error(msg) + break + + log.info('\tFacturas importadas...') + return + + +def _importar_categorias(rows): + log.info('\tImportando Categorías...') + for row in rows: + with database_proxy.atomic() as txn: + try: + Categorias.create(**row) + except IntegrityError: + msg = '\tCategoria: ({}) {}'.format(row['padre'], row['categoria']) + log.error(msg) + + log.info('\tCategorías importadas...') + return + + +def _get_id_unidad(unidad): + try: + if 'pieza' in unidad.lower(): + unidad = 'pieza' + if 'metros' in unidad.lower(): + unidad = 'metro' + if 'tramo' in unidad.lower(): + unidad = 'paquete' + if 'juego' in unidad.lower(): + unidad = 'par' + if 'bolsa' in unidad.lower(): + unidad = 'globo' + if unidad.lower() == 'no aplica': + unidad = 'servicio' + + obj = SATUnidades.get(SATUnidades.name.contains(unidad)) + except SATUnidades.DoesNotExist: + msg = '\tNo se encontró la unidad: {}'.format(unidad) + # ~ log.error(msg) + return unidad + + return str(obj.id) + + +def _get_impuestos(impuestos): + lines = '|' + for impuesto in impuestos: + if impuesto['tasa'] == '-2/3': + tasa = str(round(2/3, 6)) + else: + if impuesto['tasa'] == 'EXENTO': + tasa = '0.00' + else: + tasa = str(round(float(impuesto['tasa']) / 100.0, 6)) + + info = ( + IMPUESTOS.get(impuesto['nombre']), + impuesto['nombre'], + impuesto['tipo'][0], + tasa, + ) + lines += '|'.join(info) + '|' + return lines + + +def _generar_archivo_productos(archivo): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + log.info('Importando datos...') + app = util.ImportFacturaLibre(archivo, rfc) + if not app.is_connect: + log.error('\t{}'.format(app._error)) + return + + rows = app.import_productos() + + p, _, _, _ = util.get_path_info(archivo) + path_txt = util._join(p, 'productos_{}.txt'.format(rfc)) + log.info('\tGenerando archivo: {}'.format(path_txt)) + + fields = ( + 'clave', + 'clave_sat', + 'unidad', + 'categoria', + 'descripcion', + 'valor_unitario', + 'existencia', + 'inventario', + 'codigo_barras', + 'cuenta_predial', + 'ultimo_precio', + 'minimo', + ) + + data = ['|'.join(fields)] + not_units = [] + for row in rows: + impuestos = row.pop('impuestos', ()) + line = [str(row[r]) for r in fields] + if line[10] == 'None': + line[10] = '0.0' + line[2] = _get_id_unidad(line[2]) + try: + int(line[2]) + except ValueError: + if not line[2] in not_units: + not_units.append(line[2]) + msg = 'No se encontró la unidad: {}'.format(line[2]) + log.error(msg) + continue + line = '|'.join(line) + _get_impuestos(impuestos) + data.append(line) + + with open(path_txt, 'w') as fh: + fh.write('\n'.join(data)) + + log.info('\tArchivo generado: {}'.format(path_txt)) + return + + +def importar_bdfl(): + try: + emisor = Emisor.select()[0] + except IndexError: + msg = 'Configura primero al emisor' + return {'ok': False, 'msg': msg} + + name = '{}.sqlite'.format(emisor.rfc.lower()) + path = util._join('/tmp', name) + + log.info('Importando datos...') + app = util.ImportFacturaLibre(path, emisor.rfc) + if not app.is_connect: + msg = app._error + log.error('\t{}'.format(msg)) + return {'ok': False, 'msg': msg} + + data = app.import_data() + + _importar_socios(data['Socios']) + _importar_facturas(data['Facturas']) + _importar_categorias(data['Categorias']) + + msg = 'Importación terminada...' + log.info(msg) + + return {'ok': True, 'msg': msg} + + +def _importar_factura_libre(archivo): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + log.info('Importando datos...') + app = util.ImportFacturaLibre(archivo, rfc) + if not app.is_connect: + log.error('\t{}'.format(app._error)) + return + + data = app.import_data() + + _importar_socios(data['Socios']) + _importar_facturas(data['Facturas']) + _importar_categorias(data['Categorias']) + + log.info('Importación terminada...') + return + + +def _importar_factura_libre_gambas(conexion): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + log.info('Importando datos...') + app = util.ImportFacturaLibreGambas(conexion, rfc) + if not app.is_connect: + log.error('\t{}'.format(app._error)) + return + + data = app.import_data() + + _importar_socios(data['Socios']) + _importar_facturas(data['Facturas']) + _importar_categorias(data['Categorias']) + _importar_productos_gambas(data['Productos']) + _import_tickets(data['Tickets']) + + log.info('Importación terminada...') + return + + +def _exist_ticket(row): + filters = ( + (Tickets.serie==row['serie']) & + (Tickets.folio==row['folio']) + ) + return Tickets.select().where(filters).exists() + + +def _import_tickets(rows): + log.info('\tImportando Tickets...') + for row in rows: + try: + details = row.pop('details') + taxes = row.pop('taxes') + with database_proxy.atomic() as txn: + if _exist_ticket(row): + msg = '\tTicket existente: {}{}'.format( + row['serie'], row['folio']) + log.info(msg) + continue + + if not row['factura'] is None and row['factura']: + row['factura'] = Facturas.get( + Facturas.serie==row['factura']['serie'], + Facturas.folio==row['factura']['folio']) + else: + row['factura'] = None + + obj = Tickets.create(**row) + for detail in details: + detail['ticket'] = obj + TicketsDetalle.create(**detail) + for tax in taxes: + imp = SATImpuestos.get(**tax['filter']) + new = { + 'ticket': obj, + 'impuesto': imp, + 'importe': tax['import'], + } + TicketsImpuestos.create(**new) + except IntegrityError as e: + # ~ print (e) + msg = '\tTicket: id: {}'.format(row['serie'] + str(row['folio'])) + log.error(msg) + + log.info('\tTickets importadas...') + return + + +def _importar_productos_gambas(rows): + log.info('Importando productos...') + + KEYS = { + 'Exento': '000', + 'ISR': '001', + 'IVA': '002', + } + + totals = len(rows) + for i, row in enumerate(rows): + msg = '\tGuardando producto {} de {}'.format(i+1, totals) + log.info(msg) + + source_taxes = row.pop('impuestos') + row['unidad'] = SATUnidades.get(SATUnidades.key==row['unidad']) + taxes = [] + for tax in source_taxes: + w = { + 'key': KEYS[tax[0]], + 'name': tax[0], + 'tasa': float(tax[1]), + 'tipo': tax[2][0], + } + taxes.append(SATImpuestos.get_o_crea(w)) + + with database_proxy.transaction(): + try: + obj = Productos.create(**row) + obj.impuestos = taxes + except IntegrityError as e: + msg = '\tProducto ya existe' + log.info(msg) + + log.info('Importación terminada...') + return + + +def _importar_productos(archivo): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + log.info('Importando productos...') + + fields = ( + 'clave', + 'clave_sat', + 'unidad', + 'categoria', + 'descripcion', + 'valor_unitario', + 'existencia', + 'inventario', + 'codigo_barras', + 'cuenta_predial', + 'ultimo_precio', + 'minimo', + ) + + rows = util.read_file(archivo, 'r').split('\n') + for i, row in enumerate(rows): + if i == 0: + continue + data = row.split('|') + # ~ print (data) + new = {} + for i, f in enumerate(fields): + if not len(data[0]): + continue + + if i in (2, 3): + try: + new[f] = int(data[i]) + except ValueError: + continue + elif i in (5, 6, 10, 11): + new[f] = float(data[i]) + elif i == 7: + new[f] = bool(data[i]) + else: + new[f] = data[i] + + impuestos = data[i + 1:-1] + if not impuestos: + taxes = [SATImpuestos.select().where(SATImpuestos.id==6)] + else: + taxes = [] + try: + for i in range(0, len(impuestos), 4): + w = { + 'key': impuestos[i], + 'name': impuestos[i+1], + 'tipo': impuestos[i+2], + 'tasa': float(impuestos[i+3]), + } + taxes.append(SATImpuestos.get_o_crea(w)) + except IndexError: + print ('IE', data) + continue + + with database_proxy.transaction(): + try: + obj = Productos.create(**new) + obj.impuestos = taxes + except IntegrityError as e: + pass + + log.info('Importación terminada...') + return + + +def _import_from_folder(path): + files = util.get_files(path, 'json') + if not files: + msg = 'No se encontraron archivos para importar' + log.error(msg) + return + + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + log.info('Importando valores...') + for p in files: + msg = '\tImportando tabla: {}' + data = util.import_json(p) + log.info(msg.format(data['tabla'])) + table = globals()[data['tabla']] + for r in data['datos']: + try: + with database_proxy.atomic() as txn: + table.create(**r) + except IntegrityError: + pass + + log.info('Valores importados...') + return + + +def _test(): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + return + + +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) +help_create_tables = 'Crea las tablas en la base de datos' +help_migrate_db = 'Migra las tablas en la base de datos' +help_superuser = 'Crea un nuevo super usuario' +help_change_pass = 'Cambia la contraseña a un usuario' +help_rfc = 'Agrega un nuevo RFC' +help_br = 'Elimina un RFC' +help_lr = 'Listar RFCs' + +@click.command(context_settings=CONTEXT_SETTINGS) +@click.option('-bd', '--iniciar-bd',help=help_create_tables, + is_flag=True, default=False) +@click.option('-m', '--migrar-bd', help=help_migrate_db, + is_flag=True, default=False) +@click.option('-ns', '--nuevo-superusuario', help=help_superuser, + is_flag=True, default=False) +@click.option('-cc', '--cambiar-contraseña', help=help_change_pass, + is_flag=True, default=False) +@click.option('-ar', '--agregar-rfc', help=help_rfc, is_flag=True, default=False) +@click.option('-br', '--borrar-rfc', help=help_br, is_flag=True, default=False) +@click.option('-lr', '--listar-rfc', help=help_lr, is_flag=True, default=False) +@click.option('-i', '--importar-valores', is_flag=True, default=False) +@click.option('-f', '--archivo') +@click.option('-c', '--conexion') +@click.option('-fl', '--factura-libre', is_flag=True, default=False) +@click.option('-flg', '--factura-libre-gambas', is_flag=True, default=False) +@click.option('-t', '--test', is_flag=True, default=False) +@click.option('-gap', '--generar-archivo-productos', is_flag=True, default=False) +@click.option('-ip', '--importar-productos', is_flag=True, default=False) +@click.option('-bk', '--backup-dbs', is_flag=True, default=False) +@click.option('-n', '--no-bd', is_flag=True, default=False) +@click.option('-a', '--alta', is_flag=True, default=False) +@click.option('-r', '--rfc') +@click.option('-d', '--detalle', is_flag=True, default=False) +@click.option('-id', '--importar-directorio') +def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, + agregar_rfc, borrar_rfc, listar_rfc, importar_valores, archivo, conexion, + factura_libre, factura_libre_gambas, test, generar_archivo_productos, + importar_productos, backup_dbs, no_bd, alta, rfc, detalle, + importar_directorio): + + opt = locals() + + if opt['test']: + _test() + sys.exit(0) + + if opt['alta']: + if not opt['rfc']: + msg = 'Falta el RFC' + raise click.ClickException(msg) + empresa_agregar(opt['rfc'], no_bd) + sys.exit(0) + + if opt['iniciar_bd']: + _iniciar_bd() + sys.exit(0) + + if opt['migrar_bd']: + _migrate_tables() + sys.exit(0) + + if opt['nuevo_superusuario']: + _agregar_superusuario() + sys.exit(0) + + if opt['cambiar_contraseña']: + _cambiar_contraseña() + sys.exit(0) + + if opt['agregar_rfc']: + _agregar_rfc(no_bd) + sys.exit(0) + + if opt['borrar_rfc']: + _borrar_rfc() + sys.exit(0) + + if opt['listar_rfc']: + _listar_rfc(opt['detalle']) + sys.exit(0) + + if opt['importar_valores']: + if not opt['archivo']: + msg = 'Falta la ruta del archivo importar' + raise click.ClickException(msg) + if not util.is_file(opt['archivo']): + msg = 'No es un archivo' + raise click.ClickException(msg) + + _importar_valores(opt['archivo']) + sys.exit(0) + + if opt['factura_libre']: + if not opt['archivo']: + msg = 'Falta la ruta de la base de datos' + raise click.ClickException(msg) + if not util.is_file(opt['archivo']): + msg = 'No es un archivo' + raise click.ClickException(msg) + _, _, _, ext = util.get_path_info(opt['archivo']) + if ext != '.sqlite': + msg = 'No es una base de datos' + raise click.ClickException(msg) + + _importar_factura_libre(opt['archivo']) + sys.exit(0) + + if opt['factura_libre_gambas']: + if not opt['conexion']: + msg = 'Falta los datos de conexión' + raise click.ClickException(msg) + _importar_factura_libre_gambas(opt['conexion']) + sys.exit(0) + + if opt['generar_archivo_productos']: + if not opt['archivo']: + msg = 'Falta la ruta de la base de datos' + raise click.ClickException(msg) + if not util.is_file(opt['archivo']): + msg = 'No es un archivo' + raise click.ClickException(msg) + _, _, _, ext = util.get_path_info(opt['archivo']) + if ext != '.sqlite': + msg = 'No es una base de datos' + raise click.ClickException(msg) + + _generar_archivo_productos(opt['archivo']) + sys.exit(0) + + if opt['importar_productos']: + if not opt['archivo']: + msg = 'Falta la ruta del archivo' + raise click.ClickException(msg) + if not util.is_file(opt['archivo']): + msg = 'No es un archivo' + raise click.ClickException(msg) + _, _, _, ext = util.get_path_info(opt['archivo']) + if ext != '.txt': + msg = 'No es un archivo de texto' + raise click.ClickException(msg) + + _importar_productos(opt['archivo']) + sys.exit(0) + + if opt['importar_directorio']: + _import_from_folder(opt['importar_directorio']) + + if opt['backup_dbs']: + util.backup_dbs() + + return + + +if __name__ == '__main__': + main() + diff --git a/source/app/settings.py b/source/app/settings.py index f27a2dc..487caae 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -115,6 +115,7 @@ PRE = { } CURRENT_CFDI = '3.3' +CURRENT_CFDI_NOMINA = '1.2' DECIMALES = 2 DECIMALES_TAX = 4 IMPUESTOS = { @@ -134,4 +135,12 @@ DEFAULT_SERIE_TICKET = 'T' DIR_FACTURAS = 'facturas' USAR_TOKEN = False CANCEL_SIGNATURE = False -PUBLIC = 'Público en general' \ No newline at end of file +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', +} \ No newline at end of file diff --git a/source/db/valores_iniciales.json b/source/db/valores_iniciales.json index 28e6252..b7c4274 100644 --- a/source/db/valores_iniciales.json +++ b/source/db/valores_iniciales.json @@ -240,5 +240,1857 @@ {"key": "D10", "name": "Pagos por servicios educativos (colegiaturas)", "activo": true}, {"key": "P01", "name": "Por definir", "moral": true, "activo": true} ] +}, +{ + "datos": [ + { + "key": "AGU", + "name": "Aguascalientes", + "pais": "MEX" + }, + { + "key": "BCN", + "name": "Baja California", + "pais": "MEX" + }, + { + "key": "BCS", + "name": "Baja California Sur", + "pais": "MEX" + }, + { + "key": "CAM", + "name": "Campeche", + "pais": "MEX" + }, + { + "key": "CHP", + "name": "Chiapas", + "pais": "MEX" + }, + { + "key": "CHH", + "name": "Chihuahua", + "pais": "MEX" + }, + { + "key": "COA", + "name": "Coahuila", + "pais": "MEX" + }, + { + "key": "COL", + "name": "Colima", + "pais": "MEX" + }, + { + "key": "DIF", + "name": "Ciudad de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "DUR", + "name": "Durango", + "pais": "MEX" + }, + { + "key": "GUA", + "name": "Guanajuato", + "pais": "MEX" + }, + { + "key": "GRO", + "name": "Guerrero", + "pais": "MEX" + }, + { + "key": "HID", + "name": "Hidalgo", + "pais": "MEX" + }, + { + "key": "JAL", + "name": "Jalisco", + "pais": "MEX" + }, + { + "key": "MEX", + "name": "Estado de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "MIC", + "name": "Michoac\u00e1n", + "pais": "MEX" + }, + { + "key": "MOR", + "name": "Morelos", + "pais": "MEX" + }, + { + "key": "NAY", + "name": "Nayarit", + "pais": "MEX" + }, + { + "key": "NLE", + "name": "Nuevo Le\u00f3n", + "pais": "MEX" + }, + { + "key": "OAX", + "name": "Oaxaca", + "pais": "MEX" + }, + { + "key": "PUE", + "name": "Puebla", + "pais": "MEX" + }, + { + "key": "QUE", + "name": "Quer\u00e9taro", + "pais": "MEX" + }, + { + "key": "ROO", + "name": "Quintana Roo", + "pais": "MEX" + }, + { + "key": "SLP", + "name": "San Luis Potos\u00ed", + "pais": "MEX" + }, + { + "key": "SIN", + "name": "Sinaloa", + "pais": "MEX" + }, + { + "key": "SON", + "name": "Sonora", + "pais": "MEX" + }, + { + "key": "TAB", + "name": "Tabasco", + "pais": "MEX" + }, + { + "key": "TAM", + "name": "Tamaulipas", + "pais": "MEX" + }, + { + "key": "TLA", + "name": "Tlaxcala", + "pais": "MEX" + }, + { + "key": "VER", + "name": "Veracruz", + "pais": "MEX" + }, + { + "key": "YUC", + "name": "Yucat\u00e1n", + "pais": "MEX" + }, + { + "key": "ZAC", + "name": "Zacatecas", + "pais": "MEX" + }, + { + "key": "AL", + "name": "Alabama", + "pais": "USA" + }, + { + "key": "AK", + "name": "Alaska", + "pais": "USA" + }, + { + "key": "AZ", + "name": "Arizona", + "pais": "USA" + }, + { + "key": "AR", + "name": "Arkansas", + "pais": "USA" + }, + { + "key": "CA", + "name": "California", + "pais": "USA" + }, + { + "key": "NC", + "name": "Carolina del Norte", + "pais": "USA" + }, + { + "key": "SC", + "name": "Carolina del Sur", + "pais": "USA" + }, + { + "key": "CO", + "name": "Colorado", + "pais": "USA" + }, + { + "key": "CT", + "name": "Connecticut", + "pais": "USA" + }, + { + "key": "ND", + "name": "Dakota del Norte", + "pais": "USA" + }, + { + "key": "SD", + "name": "Dakota del Sur", + "pais": "USA" + }, + { + "key": "DE", + "name": "Delaware", + "pais": "USA" + }, + { + "key": "FL", + "name": "Florida", + "pais": "USA" + }, + { + "key": "GA", + "name": "Georgia", + "pais": "USA" + }, + { + "key": "HI", + "name": "Haw\u00e1i", + "pais": "USA" + }, + { + "key": "ID", + "name": "Idaho", + "pais": "USA" + }, + { + "key": "IL", + "name": "Illinois", + "pais": "USA" + }, + { + "key": "IN", + "name": "Indiana", + "pais": "USA" + }, + { + "key": "IA", + "name": "Iowa", + "pais": "USA" + }, + { + "key": "KS", + "name": "Kansas", + "pais": "USA" + }, + { + "key": "KY", + "name": "Kentucky", + "pais": "USA" + }, + { + "key": "LA", + "name": "Luisiana", + "pais": "USA" + }, + { + "key": "ME", + "name": "Maine", + "pais": "USA" + }, + { + "key": "MD", + "name": "Maryland", + "pais": "USA" + }, + { + "key": "MA", + "name": "Massachusetts", + "pais": "USA" + }, + { + "key": "MI", + "name": "M\u00edchigan", + "pais": "USA" + }, + { + "key": "MN", + "name": "Minnesota", + "pais": "USA" + }, + { + "key": "MS", + "name": "Misisipi", + "pais": "USA" + }, + { + "key": "MO", + "name": "Misuri", + "pais": "USA" + }, + { + "key": "MT", + "name": "Montana", + "pais": "USA" + }, + { + "key": "NE", + "name": "Nebraska", + "pais": "USA" + }, + { + "key": "NV", + "name": "Nevada", + "pais": "USA" + }, + { + "key": "NJ", + "name": "Nueva Jersey", + "pais": "USA" + }, + { + "key": "NY", + "name": "Nueva York", + "pais": "USA" + }, + { + "key": "NH", + "name": "Nuevo Hampshire", + "pais": "USA" + }, + { + "key": "NM", + "name": "Nuevo M\u00e9xico", + "pais": "USA" + }, + { + "key": "OH", + "name": "Ohio", + "pais": "USA" + }, + { + "key": "OK", + "name": "Oklahoma", + "pais": "USA" + }, + { + "key": "OR", + "name": "Oreg\u00f3n", + "pais": "USA" + }, + { + "key": "PA", + "name": "Pensilvania", + "pais": "USA" + }, + { + "key": "RI", + "name": "Rhode Island", + "pais": "USA" + }, + { + "key": "TN", + "name": "Tennessee", + "pais": "USA" + }, + { + "key": "TX", + "name": "Texas", + "pais": "USA" + }, + { + "key": "UT", + "name": "Utah", + "pais": "USA" + }, + { + "key": "VT", + "name": "Vermont", + "pais": "USA" + }, + { + "key": "VA", + "name": "Virginia", + "pais": "USA" + }, + { + "key": "WV", + "name": "Virginia Occidental", + "pais": "USA" + }, + { + "key": "WA", + "name": "Washington", + "pais": "USA" + }, + { + "key": "WI", + "name": "Wisconsin", + "pais": "USA" + }, + { + "key": "WY", + "name": "Wyoming", + "pais": "USA" + }, + { + "key": "ON", + "name": "Ontario\u00a0", + "pais": "CAN" + }, + { + "key": "QC", + "name": "Quebec\u00a0", + "pais": "CAN" + }, + { + "key": "NS", + "name": "Nueva Escocia", + "pais": "CAN" + }, + { + "key": "NB", + "name": "Nuevo Brunswick\u00a0", + "pais": "CAN" + }, + { + "key": "MB", + "name": "Manitoba", + "pais": "CAN" + }, + { + "key": "BC", + "name": "Columbia Brit\u00e1nica", + "pais": "CAN" + }, + { + "key": "PE", + "name": "Isla del Pr\u00edncipe Eduardo", + "pais": "CAN" + }, + { + "key": "SK", + "name": "Saskatchewan", + "pais": "CAN" + }, + { + "key": "AB", + "name": "Alberta", + "pais": "CAN" + }, + { + "key": "NL", + "name": "Terranova y Labrador", + "pais": "CAN" + }, + { + "key": "NT", + "name": "Territorios del Noroeste", + "pais": "CAN" + }, + { + "key": "YT", + "name": "Yuk\u00f3n", + "pais": "CAN" + }, + { + "key": "UN", + "name": "Nunavut", + "pais": "CAN" + } + ], + "tabla": "SATEstados" +}, +{ + "datos": [ + { + "key": "AGU", + "name": "Aguascalientes", + "pais": "MEX" + }, + { + "key": "BCN", + "name": "Baja California", + "pais": "MEX" + }, + { + "key": "BCS", + "name": "Baja California Sur", + "pais": "MEX" + }, + { + "key": "CAM", + "name": "Campeche", + "pais": "MEX" + }, + { + "key": "CHP", + "name": "Chiapas", + "pais": "MEX" + }, + { + "key": "CHH", + "name": "Chihuahua", + "pais": "MEX" + }, + { + "key": "COA", + "name": "Coahuila", + "pais": "MEX" + }, + { + "key": "COL", + "name": "Colima", + "pais": "MEX" + }, + { + "key": "DIF", + "name": "Ciudad de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "DUR", + "name": "Durango", + "pais": "MEX" + }, + { + "key": "GUA", + "name": "Guanajuato", + "pais": "MEX" + }, + { + "key": "GRO", + "name": "Guerrero", + "pais": "MEX" + }, + { + "key": "HID", + "name": "Hidalgo", + "pais": "MEX" + }, + { + "key": "JAL", + "name": "Jalisco", + "pais": "MEX" + }, + { + "key": "MEX", + "name": "Estado de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "MIC", + "name": "Michoac\u00e1n", + "pais": "MEX" + }, + { + "key": "MOR", + "name": "Morelos", + "pais": "MEX" + }, + { + "key": "NAY", + "name": "Nayarit", + "pais": "MEX" + }, + { + "key": "NLE", + "name": "Nuevo Le\u00f3n", + "pais": "MEX" + }, + { + "key": "OAX", + "name": "Oaxaca", + "pais": "MEX" + }, + { + "key": "PUE", + "name": "Puebla", + "pais": "MEX" + }, + { + "key": "QUE", + "name": "Quer\u00e9taro", + "pais": "MEX" + }, + { + "key": "ROO", + "name": "Quintana Roo", + "pais": "MEX" + }, + { + "key": "SLP", + "name": "San Luis Potos\u00ed", + "pais": "MEX" + }, + { + "key": "SIN", + "name": "Sinaloa", + "pais": "MEX" + }, + { + "key": "SON", + "name": "Sonora", + "pais": "MEX" + }, + { + "key": "TAB", + "name": "Tabasco", + "pais": "MEX" + }, + { + "key": "TAM", + "name": "Tamaulipas", + "pais": "MEX" + }, + { + "key": "TLA", + "name": "Tlaxcala", + "pais": "MEX" + }, + { + "key": "VER", + "name": "Veracruz", + "pais": "MEX" + }, + { + "key": "YUC", + "name": "Yucat\u00e1n", + "pais": "MEX" + }, + { + "key": "ZAC", + "name": "Zacatecas", + "pais": "MEX" + }, + { + "key": "AL", + "name": "Alabama", + "pais": "USA" + }, + { + "key": "AK", + "name": "Alaska", + "pais": "USA" + }, + { + "key": "AZ", + "name": "Arizona", + "pais": "USA" + }, + { + "key": "AR", + "name": "Arkansas", + "pais": "USA" + }, + { + "key": "CA", + "name": "California", + "pais": "USA" + }, + { + "key": "NC", + "name": "Carolina del Norte", + "pais": "USA" + }, + { + "key": "SC", + "name": "Carolina del Sur", + "pais": "USA" + }, + { + "key": "CO", + "name": "Colorado", + "pais": "USA" + }, + { + "key": "CT", + "name": "Connecticut", + "pais": "USA" + }, + { + "key": "ND", + "name": "Dakota del Norte", + "pais": "USA" + }, + { + "key": "SD", + "name": "Dakota del Sur", + "pais": "USA" + }, + { + "key": "DE", + "name": "Delaware", + "pais": "USA" + }, + { + "key": "FL", + "name": "Florida", + "pais": "USA" + }, + { + "key": "GA", + "name": "Georgia", + "pais": "USA" + }, + { + "key": "HI", + "name": "Haw\u00e1i", + "pais": "USA" + }, + { + "key": "ID", + "name": "Idaho", + "pais": "USA" + }, + { + "key": "IL", + "name": "Illinois", + "pais": "USA" + }, + { + "key": "IN", + "name": "Indiana", + "pais": "USA" + }, + { + "key": "IA", + "name": "Iowa", + "pais": "USA" + }, + { + "key": "KS", + "name": "Kansas", + "pais": "USA" + }, + { + "key": "KY", + "name": "Kentucky", + "pais": "USA" + }, + { + "key": "LA", + "name": "Luisiana", + "pais": "USA" + }, + { + "key": "ME", + "name": "Maine", + "pais": "USA" + }, + { + "key": "MD", + "name": "Maryland", + "pais": "USA" + }, + { + "key": "MA", + "name": "Massachusetts", + "pais": "USA" + }, + { + "key": "MI", + "name": "M\u00edchigan", + "pais": "USA" + }, + { + "key": "MN", + "name": "Minnesota", + "pais": "USA" + }, + { + "key": "MS", + "name": "Misisipi", + "pais": "USA" + }, + { + "key": "MO", + "name": "Misuri", + "pais": "USA" + }, + { + "key": "MT", + "name": "Montana", + "pais": "USA" + }, + { + "key": "NE", + "name": "Nebraska", + "pais": "USA" + }, + { + "key": "NV", + "name": "Nevada", + "pais": "USA" + }, + { + "key": "NJ", + "name": "Nueva Jersey", + "pais": "USA" + }, + { + "key": "NY", + "name": "Nueva York", + "pais": "USA" + }, + { + "key": "NH", + "name": "Nuevo Hampshire", + "pais": "USA" + }, + { + "key": "NM", + "name": "Nuevo M\u00e9xico", + "pais": "USA" + }, + { + "key": "OH", + "name": "Ohio", + "pais": "USA" + }, + { + "key": "OK", + "name": "Oklahoma", + "pais": "USA" + }, + { + "key": "OR", + "name": "Oreg\u00f3n", + "pais": "USA" + }, + { + "key": "PA", + "name": "Pensilvania", + "pais": "USA" + }, + { + "key": "RI", + "name": "Rhode Island", + "pais": "USA" + }, + { + "key": "TN", + "name": "Tennessee", + "pais": "USA" + }, + { + "key": "TX", + "name": "Texas", + "pais": "USA" + }, + { + "key": "UT", + "name": "Utah", + "pais": "USA" + }, + { + "key": "VT", + "name": "Vermont", + "pais": "USA" + }, + { + "key": "VA", + "name": "Virginia", + "pais": "USA" + }, + { + "key": "WV", + "name": "Virginia Occidental", + "pais": "USA" + }, + { + "key": "WA", + "name": "Washington", + "pais": "USA" + }, + { + "key": "WI", + "name": "Wisconsin", + "pais": "USA" + }, + { + "key": "WY", + "name": "Wyoming", + "pais": "USA" + }, + { + "key": "ON", + "name": "Ontario\u00a0", + "pais": "CAN" + }, + { + "key": "QC", + "name": "Quebec\u00a0", + "pais": "CAN" + }, + { + "key": "NS", + "name": "Nueva Escocia", + "pais": "CAN" + }, + { + "key": "NB", + "name": "Nuevo Brunswick\u00a0", + "pais": "CAN" + }, + { + "key": "MB", + "name": "Manitoba", + "pais": "CAN" + }, + { + "key": "BC", + "name": "Columbia Brit\u00e1nica", + "pais": "CAN" + }, + { + "key": "PE", + "name": "Isla del Pr\u00edncipe Eduardo", + "pais": "CAN" + }, + { + "key": "SK", + "name": "Saskatchewan", + "pais": "CAN" + }, + { + "key": "AB", + "name": "Alberta", + "pais": "CAN" + }, + { + "key": "NL", + "name": "Terranova y Labrador", + "pais": "CAN" + }, + { + "key": "NT", + "name": "Territorios del Noroeste", + "pais": "CAN" + }, + { + "key": "YT", + "name": "Yuk\u00f3n", + "pais": "CAN" + }, + { + "key": "UN", + "name": "Nunavut", + "pais": "CAN" + } + ], + "tabla": "SATEstados" +}, +{ + "datos": [ + { + "key": "IP", + "name": "Ingresos propios." + }, + { + "key": "IF", + "name": "Ingreso federales." + }, + { + "key": "IM", + "name": "Ingresos mixtos." + } + ], + "tabla": "SATOrigenRecurso" +}, +{ + "datos": [ + { + "key": "01", + "name": "Diario" + }, + { + "key": "02", + "name": "Semanal" + }, + { + "key": "03", + "name": "Catorcenal" + }, + { + "key": "04", + "name": "Quincenal" + }, + { + "key": "05", + "name": "Mensual" + }, + { + "key": "06", + "name": "Bimestral" + }, + { + "key": "07", + "name": "Unidad obra" + }, + { + "key": "08", + "name": "Comisi\u00f3n" + }, + { + "key": "09", + "name": "Precio alzado" + }, + { + "key": "10", + "name": "Decenal" + }, + { + "key": "99", + "name": "Otra Periodicidad" + } + ], + "tabla": "SATPeriodicidadPago" +}, +{ + "datos": [ + { + "key": "1", + "name": "Clase I" + }, + { + "key": "2", + "name": "Clase II" + }, + { + "key": "3", + "name": "Clase III" + }, + { + "key": "4", + "name": "Clase IV" + }, + { + "key": "5", + "name": "Clase V" + }, + { + "key": "99", + "name": "No aplica" + } + ], + "tabla": "SATRiesgoPuesto" +}, +{ + "datos": [ + { + "key": "01", + "name": "Contrato de trabajo por tiempo indeterminado" + }, + { + "key": "02", + "name": "Contrato de trabajo para obra determinada" + }, + { + "key": "03", + "name": "Contrato de trabajo por tiempo determinado" + }, + { + "key": "04", + "name": "Contrato de trabajo por temporada" + }, + { + "key": "05", + "name": "Contrato de trabajo sujeto a prueba" + }, + { + "key": "06", + "name": "Contrato de trabajo con capacitaci\u00f3n inicial" + }, + { + "key": "07", + "name": "Modalidad de contrataci\u00f3n por pago de hora laborada" + }, + { + "key": "08", + "name": "Modalidad de trabajo por comisi\u00f3n laboral" + }, + { + "key": "09", + "name": "Modalidades de contrataci\u00f3n donde no existe relaci\u00f3n de trabajo" + }, + { + "key": "10", + "name": "Jubilaci\u00f3n, pensi\u00f3n, retiro." + }, + { + "key": "99", + "name": "Otro contrato" + } + ], + "tabla": "SATTipoContrato" +}, +{ + "datos": [ + { + "key": "001", + "name": "Seguridad social" + }, + { + "key": "002", + "name": "ISR" + }, + { + "key": "003", + "name": "Aportaciones a retiro, cesant\u00eda en edad avanzada y vejez." + }, + { + "key": "004", + "name": "Otros" + }, + { + "key": "005", + "name": "Aportaciones a Fondo de vivienda" + }, + { + "key": "006", + "name": "Descuento por incapacidad" + }, + { + "key": "007", + "name": "Pensi\u00f3n alimenticia" + }, + { + "key": "008", + "name": "Renta" + }, + { + "key": "009", + "name": "Pr\u00e9stamos provenientes del Fondo Nacional de la Vivienda para los Trabajadores" + }, + { + "key": "010", + "name": "Pago por cr\u00e9dito de vivienda" + }, + { + "key": "011", + "name": "Pago de abonos INFONACOT" + }, + { + "key": "012", + "name": "Anticipo de salarios" + }, + { + "key": "013", + "name": "Pagos hechos con exceso al trabajador" + }, + { + "key": "014", + "name": "Errores" + }, + { + "key": "015", + "name": "P\u00e9rdidas" + }, + { + "key": "016", + "name": "Aver\u00edas" + }, + { + "key": "017", + "name": "Adquisici\u00f3n de art\u00edculos producidos por la empresa o establecimiento" + }, + { + "key": "018", + "name": "Cuotas para la constituci\u00f3n y fomento de sociedades cooperativas y de cajas de ahorro" + }, + { + "key": "019", + "name": "Cuotas sindicales" + }, + { + "key": "020", + "name": "Ausencia (Ausentismo)" + }, + { + "key": "021", + "name": "Cuotas obrero patronales" + }, + { + "key": "022", + "name": "Impuestos Locales" + }, + { + "key": "023", + "name": "Aportaciones voluntarias" + }, + { + "key": "024", + "name": "Ajuste en Gratificaci\u00f3n Anual (Aguinaldo) Exento" + }, + { + "key": "025", + "name": "Ajuste en Gratificaci\u00f3n Anual (Aguinaldo) Gravado" + }, + { + "key": "026", + "name": "Ajuste en Participaci\u00f3n de los Trabajadores en las Utilidades PTU Exento" + }, + { + "key": "027", + "name": "Ajuste en Participaci\u00f3n de los Trabajadores en las Utilidades PTU Gravado" + }, + { + "key": "028", + "name": "Ajuste en Reembolso de Gastos M\u00e9dicos Dentales y Hospitalarios Exento" + }, + { + "key": "029", + "name": "Ajuste en Fondo de ahorro Exento" + }, + { + "key": "030", + "name": "Ajuste en Caja de ahorro Exento" + }, + { + "key": "031", + "name": "Ajuste en Contribuciones a Cargo del Trabajador Pagadas por el Patr\u00f3n Exento" + }, + { + "key": "032", + "name": "Ajuste en Premios por puntualidad Gravado" + }, + { + "key": "033", + "name": "Ajuste en Prima de Seguro de vida Exento" + }, + { + "key": "034", + "name": "Ajuste en Seguro de Gastos M\u00e9dicos Mayores Exento" + }, + { + "key": "035", + "name": "Ajuste en Cuotas Sindicales Pagadas por el Patr\u00f3n Exento" + }, + { + "key": "036", + "name": "Ajuste en Subsidios por incapacidad Exento" + }, + { + "key": "037", + "name": "Ajuste en Becas para trabajadores y/o hijos Exento" + }, + { + "key": "038", + "name": "Ajuste en Horas extra Exento" + }, + { + "key": "039", + "name": "Ajuste en Horas extra Gravado" + }, + { + "key": "040", + "name": "Ajuste en Prima dominical Exento" + }, + { + "key": "041", + "name": "Ajuste en Prima dominical Gravado" + }, + { + "key": "042", + "name": "Ajuste en Prima vacacional Exento" + }, + { + "key": "043", + "name": "Ajuste en Prima vacacional Gravado" + }, + { + "key": "044", + "name": "Ajuste en Prima por antig\u00fcedad Exento" + }, + { + "key": "045", + "name": "Ajuste en Prima por antig\u00fcedad Gravado" + }, + { + "key": "046", + "name": "Ajuste en Pagos por separaci\u00f3n Exento" + }, + { + "key": "047", + "name": "Ajuste en Pagos por separaci\u00f3n Gravado" + }, + { + "key": "048", + "name": "Ajuste en Seguro de retiro Exento" + }, + { + "key": "049", + "name": "Ajuste en Indemnizaciones Exento" + }, + { + "key": "050", + "name": "Ajuste en Indemnizaciones Gravado" + }, + { + "key": "051", + "name": "Ajuste en Reembolso por funeral Exento" + }, + { + "key": "052", + "name": "Ajuste en Cuotas de seguridad social pagadas por el patr\u00f3n Exento" + }, + { + "key": "053", + "name": "Ajuste en Comisiones Gravado" + }, + { + "key": "054", + "name": "Ajuste en Vales de despensa Exento" + }, + { + "key": "055", + "name": "Ajuste en Vales de restaurante Exento" + }, + { + "key": "056", + "name": "Ajuste en Vales de gasolina Exento" + }, + { + "key": "057", + "name": "Ajuste en Vales de ropa Exento" + }, + { + "key": "058", + "name": "Ajuste en Ayuda para renta Exento" + }, + { + "key": "059", + "name": "Ajuste en Ayuda para art\u00edculos escolares Exento" + }, + { + "key": "060", + "name": "Ajuste en Ayuda para anteojos Exento" + }, + { + "key": "061", + "name": "Ajuste en Ayuda para transporte Exento" + }, + { + "key": "062", + "name": "Ajuste en Ayuda para gastos de funeral Exento" + }, + { + "key": "063", + "name": "Ajuste en Otros ingresos por salarios Exento" + }, + { + "key": "064", + "name": "Ajuste en Otros ingresos por salarios Gravado" + }, + { + "key": "065", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro Exento" + }, + { + "key": "066", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro Gravado" + }, + { + "key": "067", + "name": "Ajuste en Pagos por separaci\u00f3n Acumulable" + }, + { + "key": "068", + "name": "Ajuste en Pagos por separaci\u00f3n No acumulable" + }, + { + "key": "069", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro Acumulable" + }, + { + "key": "070", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro No acumulable" + }, + { + "key": "071", + "name": "Ajuste en Subsidio para el empleo (efectivamente entregado al trabajador)" + }, + { + "key": "072", + "name": "Ajuste en Ingresos en acciones o t\u00edtulos valor que representan bienes Exento" + }, + { + "key": "073", + "name": "Ajuste en Ingresos en acciones o t\u00edtulos valor que representan bienes Gravado" + }, + { + "key": "074", + "name": "Ajuste en Alimentaci\u00f3n Exento" + }, + { + "key": "075", + "name": "Ajuste en Alimentaci\u00f3n Gravado" + }, + { + "key": "076", + "name": "Ajuste en Habitaci\u00f3n Exento" + }, + { + "key": "077", + "name": "Ajuste en Habitaci\u00f3n Gravado" + }, + { + "key": "078", + "name": "Ajuste en Premios por asistencia" + }, + { + "key": "079", + "name": "Ajuste en Pagos distintos a los listados y que no deben considerarse como ingreso por sueldos, salarios o ingresos asimilados." + }, + { + "key": "080", + "name": "Ajuste en Vi\u00e1ticos gravados" + }, + { + "key": "081", + "name": "Ajuste en Vi\u00e1ticos (entregados al trabajador)" + }, + { + "key": "082", + "name": "Ajuste en Fondo de ahorro Gravado" + }, + { + "key": "083", + "name": "Ajuste en Caja de ahorro Gravado" + }, + { + "key": "084", + "name": "Ajuste en Prima de Seguro de vida Gravado" + }, + { + "key": "085", + "name": "Ajuste en Seguro de Gastos M\u00e9dicos Mayores Gravado" + }, + { + "key": "086", + "name": "Ajuste en Subsidios por incapacidad Gravado" + }, + { + "key": "087", + "name": "Ajuste en Becas para trabajadores y/o hijos Gravado" + }, + { + "key": "088", + "name": "Ajuste en Seguro de retiro Gravado" + }, + { + "key": "089", + "name": "Ajuste en Vales de despensa Gravado" + }, + { + "key": "090", + "name": "Ajuste en Vales de restaurante Gravado" + }, + { + "key": "091", + "name": "Ajuste en Vales de gasolina Gravado" + }, + { + "key": "092", + "name": "Ajuste en Vales de ropa Gravado" + }, + { + "key": "093", + "name": "Ajuste en Ayuda para renta Gravado" + }, + { + "key": "094", + "name": "Ajuste en Ayuda para art\u00edculos escolares Gravado" + }, + { + "key": "095", + "name": "Ajuste en Ayuda para anteojos Gravado" + }, + { + "key": "096", + "name": "Ajuste en Ayuda para transporte Gravado" + }, + { + "key": "097", + "name": "Ajuste en Ayuda para gastos de funeral Gravado" + }, + { + "key": "098", + "name": "Ajuste a ingresos asimilados a salarios gravados" + }, + { + "key": "099", + "name": "Ajuste a ingresos por sueldos y salarios gravados" + }, + { + "key": "100", + "name": "Ajuste en Vi\u00e1ticos exentos" + }, + { + "key": "101", + "name": "ISR Retenido de ejercicio anterior" + } + ], + "tabla": "SATTipoDeduccion" +}, +{ + "datos": [ + { + "key": "01", + "name": "Dobles" + }, + { + "key": "02", + "name": "Triples" + }, + { + "key": "03", + "name": "Simples" + } + ], + "tabla": "SATTipoHoras" +}, +{ + "datos": [ + { + "key": "01", + "name": "Riesgo de trabajo." + }, + { + "key": "02", + "name": "Enfermedad en general." + }, + { + "key": "03", + "name": "Maternidad." + } + ], + "tabla": "SATTipoIncapacidad" +}, +{ + "datos": [ + { + "key": "01", + "name": "Diurna" + }, + { + "key": "02", + "name": "Nocturna" + }, + { + "key": "03", + "name": "Mixta" + }, + { + "key": "04", + "name": "Por hora" + }, + { + "key": "05", + "name": "Reducida" + }, + { + "key": "06", + "name": "Continuada" + }, + { + "key": "07", + "name": "Partida" + }, + { + "key": "08", + "name": "Por turnos" + }, + { + "key": "99", + "name": "Otra Jornada" + } + ], + "tabla": "SATTipoJornada" +}, +{ + "datos": [ + { + "key": "O", + "name": "N\u00f3mina ordinaria" + }, + { + "key": "E", + "name": "N\u00f3mina extraordinaria" + } + ], + "tabla": "SATTipoNomina" +}, +{ + "datos": [ + { + "key": "001", + "name": "Reintegro de ISR pagado en exceso (siempre que no haya sido enterado al SAT)." + }, + { + "key": "002", + "name": "Subsidio para el empleo (efectivamente entregado al trabajador)." + }, + { + "key": "003", + "name": "Vi\u00e1ticos (entregados al trabajador)." + }, + { + "key": "004", + "name": "Aplicaci\u00f3n de saldo a favor por compensaci\u00f3n anual." + }, + { + "key": "005", + "name": "Reintegro de ISR retenido en exceso de ejercicio anterior (siempre que no haya sido enterado al SAT)." + }, + { + "key": "999", + "name": "Pagos distintos a los listados y que no deben considerarse como ingreso por sueldos, salarios o ingresos asimilados." + } + ], + "tabla": "SATTipoOtroPago" +}, +{ + "datos": [ + { + "key": "001", + "name": "Sueldos, Salarios Rayas y Jornales" + }, + { + "key": "002", + "name": "Gratificaci\u00f3n Anual (Aguinaldo)" + }, + { + "key": "003", + "name": "Participaci\u00f3n de los Trabajadores en las Utilidades PTU" + }, + { + "key": "004", + "name": "Reembolso de Gastos M\u00e9dicos Dentales y Hospitalarios" + }, + { + "key": "005", + "name": "Fondo de Ahorro" + }, + { + "key": "006", + "name": "Caja de ahorro" + }, + { + "key": "009", + "name": "Contribuciones a Cargo del Trabajador Pagadas por el Patr\u00f3n" + }, + { + "key": "010", + "name": "Premios por puntualidad" + }, + { + "key": "011", + "name": "Prima de Seguro de vida" + }, + { + "key": "012", + "name": "Seguro de Gastos M\u00e9dicos Mayores" + }, + { + "key": "013", + "name": "Cuotas Sindicales Pagadas por el Patr\u00f3n" + }, + { + "key": "014", + "name": "Subsidios por incapacidad" + }, + { + "key": "015", + "name": "Becas para trabajadores y/o hijos" + }, + { + "key": "019", + "name": "Horas extra" + }, + { + "key": "020", + "name": "Prima dominical" + }, + { + "key": "021", + "name": "Prima vacacional" + }, + { + "key": "022", + "name": "Prima por antig\u00fcedad" + }, + { + "key": "023", + "name": "Pagos por separaci\u00f3n" + }, + { + "key": "024", + "name": "Seguro de retiro" + }, + { + "key": "025", + "name": "Indemnizaciones" + }, + { + "key": "026", + "name": "Reembolso por funeral" + }, + { + "key": "027", + "name": "Cuotas de seguridad social pagadas por el patr\u00f3n" + }, + { + "key": "028", + "name": "Comisiones" + }, + { + "key": "029", + "name": "Vales de despensa" + }, + { + "key": "030", + "name": "Vales de restaurante" + }, + { + "key": "031", + "name": "Vales de gasolina" + }, + { + "key": "032", + "name": "Vales de ropa" + }, + { + "key": "033", + "name": "Ayuda para renta" + }, + { + "key": "034", + "name": "Ayuda para art\u00edculos escolares" + }, + { + "key": "035", + "name": "Ayuda para anteojos" + }, + { + "key": "036", + "name": "Ayuda para transporte" + }, + { + "key": "037", + "name": "Ayuda para gastos de funeral" + }, + { + "key": "038", + "name": "Otros ingresos por salarios" + }, + { + "key": "039", + "name": "Jubilaciones, pensiones o haberes de retiro" + }, + { + "key": "044", + "name": "Jubilaciones, pensiones o haberes de retiro en parcialidades" + }, + { + "key": "045", + "name": "Ingresos en acciones o t\u00edtulos valor que representan bienes" + }, + { + "key": "046", + "name": "Ingresos asimilados a salarios" + }, + { + "key": "047", + "name": "Alimentaci\u00f3n" + }, + { + "key": "048", + "name": "Habitaci\u00f3n" + }, + { + "key": "049", + "name": "Premios por asistencia" + }, + { + "key": "050", + "name": "Vi\u00e1ticos" + } + ], + "tabla": "SATTipoPercepcion" +}, +{ + "datos": [ + { + "key": "02", + "name": "Sueldos" + }, + { + "key": "03", + "name": "Jubilados" + }, + { + "key": "04", + "name": "Pensionados" + }, + { + "key": "05", + "name": "Asimilados Miembros Sociedades Cooperativas Produccion" + }, + { + "key": "06", + "name": "Asimilados Integrantes Sociedades Asociaciones Civiles" + }, + { + "key": "07", + "name": "Asimilados Miembros consejos" + }, + { + "key": "08", + "name": "Asimilados comisionistas" + }, + { + "key": "09", + "name": "Asimilados Honorarios" + }, + { + "key": "10", + "name": "Asimilados acciones" + }, + { + "key": "11", + "name": "Asimilados otros" + }, + { + "key": "12", + "name": "Jubilados o Pensionados" + }, + { + "key": "99", + "name": "Otro Regimen" + } + ], + "tabla": "SATTipoRegimen" } ] diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 5e8fa9c..ca1bc11 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -75,6 +75,7 @@ var controllers = { $$('chk_ticket_edit_cant').attachEvent('onItemClick', chk_config_item_click) $$('chk_ticket_total_up').attachEvent('onItemClick', chk_config_item_click) $$('txt_ticket_printer').attachEvent('onKeyPress', txt_ticket_printer_key_press) + $$('chk_usar_nomina').attachEvent('onItemClick', chk_config_item_click) $$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click) $$('up_bdfl').attachEvent('onUploadComplete', up_bdfl_upload_complete) @@ -198,6 +199,9 @@ function get_emisor(){ if(emisor.regimenes){ $$('lst_emisor_regimen').select(emisor.regimenes) } + if(emisor.emisor_rfc.length == 12){ + show('emisor_curp', false) + } }else{ msg_error(values.msg) } @@ -1014,8 +1018,8 @@ function txt_plantilla_donataria_click(e){ function tab_options_change(nv, ov){ var cv = { - Plantillas: 'templates', - Otros: 'configotros', + tab_admin_templates: 'templates', + tab_admin_otros: 'configotros', } get_config_values(cv[nv]) } diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index fb26e80..e2d8e26 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -1617,7 +1617,8 @@ function generate_pdf(id){ function grid_preinvoices_click(id, e, node){ if(id.column == 'pdf'){ - generate_pdf(id.row) + //~ generate_pdf(id.row) + window.open('/doc/pre/' + id, '_blank') }else if(id.column == 'email'){ enviar_prefactura(id) } diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 5e4575d..4463fc1 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -9,7 +9,6 @@ function configuracion_inicial(){ webix.ajax().get('/values/main', function(text, data){ var values = data.json() $$('lbl_title_main').setValue(values.empresa) - //~ showvar() var pos = 4 if(values.escuela){ var node = { @@ -19,6 +18,14 @@ function configuracion_inicial(){ $$('main_sidebar').add(node, pos) pos += 1 } + if(values.nomina){ + var node = { + id: 'app_nomina', + icon: 'users', + value: 'Nómina'} + $$('main_sidebar').add(node, pos) + pos += 1 + } if(values.punto_de_venta){ var node = { id: 'app_tickets', @@ -55,6 +62,7 @@ var controllers = { bancos_controllers.init() invoices_controllers.init() controllers_school.init() + nomina_controllers.init() tickets_controllers.init() } } @@ -156,6 +164,14 @@ function multi_change(prevID, nextID){ return } + if(nextID == 'app_nomina'){ + active = $$('multi_nomina').getActiveId() + if(active == 'nomina_home'){ + default_config_nomina() + } + return + } + if(nextID == 'app_tickets'){ active = $$('multi_tickets').getActiveId() if(active == 'tickets_home'){ diff --git a/source/static/js/controller/main.js.orig b/source/static/js/controller/main.js.orig new file mode 100644 index 0000000..9f19ebe --- /dev/null +++ b/source/static/js/controller/main.js.orig @@ -0,0 +1,210 @@ +var gi = null + + +function configuracion_inicial(){ + webix.ajax().get('/values/admin', function(text, data){ + var values = data.json() + show('cmd_ir_al_admin', values) + }) + webix.ajax().get('/values/main', function(text, data){ + var values = data.json() + $$('lbl_title_main').setValue(values.empresa) +<<<<<<< HEAD + //~ showvar() + var pos = 4 + if(values.escuela){ + var node = { + id: 'app_school', + icon: 'graduation-cap', + value: 'Escuela'} +======= + var pos = 4 + if(values.nomina){ + var node = { + id: 'app_nomina', + icon: 'users', + value: 'Nómina'} +>>>>>>> nomina + $$('main_sidebar').add(node, pos) + pos += 1 + } + if(values.punto_de_venta){ + var node = { + id: 'app_tickets', + icon: 'money', + value: 'Punto de venta'} + $$('main_sidebar').add(node, pos) + } + }) + +} + + +function cmd_ir_al_admin_click(){ + window.location = '/admin' +} + + +var controllers = { + init: function(){ + //~ Main + $$('menu_user').attachEvent('onMenuItemClick', menu_user_click); + configuracion_inicial() + + var tb_invoice = $$('tv_invoice').getTabbar() + tb_invoice.attachEvent('onChange', tb_invoice_change) + $$('prefilter_year').attachEvent('onChange', prefilter_year_change) + $$('prefilter_month').attachEvent('onChange', prefilter_month_change) + $$('cmd_delete_preinvoice').attachEvent('onItemClick', cmd_delete_preinvoice_click) + $$('cmd_facturar_preinvoice').attachEvent('onItemClick', cmd_facturar_preinvoice_click) + $$('grid_preinvoices').attachEvent('onItemClick', grid_preinvoices_click) + + partners_controllers.init() + products_controllers.init() + bancos_controllers.init() + invoices_controllers.init() +<<<<<<< HEAD + controllers_school.init() +======= + nomina_controllers.init() +>>>>>>> nomina + tickets_controllers.init() + } +} + + +function get_uso_cfdi_to_table(){ + webix.ajax().sync().get('/values/usocfdi', function(text, data){ + var values = data.json() + table_usocfdi.clear() + table_usocfdi.insert(values) + }) +} + + +function get_partners(){ + webix.ajax().get('/partners', {}, { + error: function(text, data, xhr) { + msg_error('Error al consultar') + }, + success: function(text, data, xhr) { + var values = data.json(); + $$('grid_partners').clearAll(); + if (values.data){ + $$('grid_partners').parse(values.data, 'json'); + }; + } + }) +} + + +function menu_user_click(id, e, node){ + if (id == 1){ + window.location = '/logout'; + return + } +} + + +function current_dates(){ + var fy = $$('filter_year') + var fm = $$('filter_month') + var pfy = $$('prefilter_year') + var pfm = $$('prefilter_month') + var d = new Date() + + fy.blockEvent() + fm.blockEvent() + pfy.blockEvent() + pfm.blockEvent() + + fm.setValue(d.getMonth() + 1) + pfm.setValue(d.getMonth() + 1) + webix.ajax().sync().get('/values/filteryears', function(text, data){ + var values = data.json() + fy.getList().parse(values[0]) + pfy.getList().parse(values[1]) + fy.setValue(d.getFullYear()) + pfy.setValue(d.getFullYear()) + }) + + fy.unblockEvent() + fm.unblockEvent() + pfy.unblockEvent() + pfm.unblockEvent() +} + + +function multi_change(prevID, nextID){ + + if(nextID == 'app_partners'){ + active = $$('multi_partners').getActiveId() + if(active == 'partners_home'){ + get_partners() + } + return + } + + if(nextID == 'app_products'){ + active = $$('multi_products').getActiveId() + if(active == 'products_home'){ + get_products() + } + return + } + + if(nextID == 'app_bancos'){ + active = $$('multi_bancos').getActiveId() + if(active == 'banco_home'){ + get_cuentas_banco() + } + return + } + + if(nextID == 'app_school'){ + active = $$('multi_school').getActiveId() + if(active == 'school_home'){ + init_config_school() + } + return + } + + if(nextID == 'app_tickets'){ + active = $$('multi_tickets').getActiveId() + if(active == 'tickets_home'){ + configuracion_inicial_ticket() + } + return + } + + if(nextID == 'app_nomina'){ + active = $$('multi_nomina').getActiveId() + if(active == 'nomina_home'){ + default_config_nomina() + } + return + } + + if(nextID == 'app_invoices'){ + active = $$('multi_invoices').getActiveId() + if(active == 'invoices_home'){ + current_dates() + get_invoices() + validar_timbrar() + } + gi = $$('grid_invoices') + return + } + +} + + +function get_taxes(){ + webix.ajax().sync().get('/values/taxes', function(text, data){ + var values = data.json() + table_taxes.clear() + table_taxes.insert(values) + $$("grid_product_taxes").clearAll() + $$("grid_product_taxes").parse(values, 'json') + }) +} diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js new file mode 100644 index 0000000..5f4e6aa --- /dev/null +++ b/source/static/js/controller/nomina.js @@ -0,0 +1,477 @@ +var query = [] +var cfg_nomina = new Object() + + +var nomina_controllers = { + init: function(){ + $$('cmd_nomina_import').attachEvent('onItemClick', cmd_nomina_import_click) + $$('cmd_empleados').attachEvent('onItemClick', cmd_empleados_click) + $$('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_log').attachEvent('onItemClick', cmd_nomina_log_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() +} + + +function current_dates_nomina(){ + var fy = $$('filter_year_nomina') + var fm = $$('filter_month_nomina') + var d = new Date() + + fy.blockEvent() + fm.blockEvent() + + fm.setValue(d.getMonth() + 1) + webix.ajax().sync().get('/values/filteryearsnomina', function(text, data){ + var values = data.json() + fy.getList().parse(values) + fy.setValue(d.getFullYear()) + }) + + fy.unblockEvent() + fm.unblockEvent() +} + + +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) + } + } + }) +} + + +function cmd_nomina_import_click(){ + win_import_nomina.init() + $$('win_import_nomina').show() +} + + +function cmd_import_template_nomina_click(){ + var form = $$('form_upload_nomina') + + var values = form.getValues() + + if(!$$('lst_upload_nomina').count()){ + $$('win_import_nomina').close() + return + } + + if($$('lst_upload_nomina').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('up_nomina').files.getItem($$('up_nomina').files.getFirstId()) + + if(template.type.toLowerCase() != 'ods'){ + msg = 'Archivo inválido.\n\nSe requiere un archivo ODS' + msg_error(msg) + return + } + + msg = '¿Estás seguro de importar este archivo?' + webix.confirm({ + title: 'Importar Nómina', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_nomina').send() + } + } + }) +} + + +function up_nomina_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir el archivo' + msg_error(msg) + return + } + msg = 'Archivo subido correctamente.\n\nComenzando importación.' + msg_ok(msg) + $$('win_import_nomina').close() + + webix.ajax().get('/nomina', {opt: 'import'}, { + error: function(text, data, xhr) { + msg = 'Error al importar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg_ok(values.msg) + get_nomina() + }else{ + msg_error(values.msg) + } + } + }) +} + + +function get_employees(){ + webix.ajax().get('/employees', { + 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_employees').clearAll(); + $$('grid_employees').parse(values.rows, 'json'); + }else{ + msg_error(values.msg) + } + } + }) +} + + +function cmd_empleados_click(){ + get_employees() + $$('multi_nomina').setValue('nomina_empleados') +} + + +function cmd_close_empleados_click(){ + $$('multi_nomina').setValue('nomina_home') +} + + +function cmd_import_empleados_click(){ + win_import_employees.init() + $$('win_import_employees').show() +} + +function cmd_import_employees_click(){ + var form = $$('form_upload_employees') + + var values = form.getValues() + + if(!$$('lst_upload_employees').count()){ + $$('win_import_employees').close() + return + } + + if($$('lst_upload_employees').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('up_employees').files.getItem($$('up_employees').files.getFirstId()) + + if(template.type.toLowerCase() != 'ods'){ + msg = 'Archivo inválido.\n\nSe requiere un archivo ODS' + msg_error(msg) + return + } + + msg = '¿Estás seguro de importar este archivo?' + webix.confirm({ + title: 'Importar Empleados', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_employees').send() + } + } + }) +} + + +function up_employees_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir el archivo' + msg_error(msg) + return + } + msg = 'Archivo subido correctamente.\n\nComenzando importación.' + msg_ok(msg) + $$('win_import_employees').close() + + webix.ajax().get('/employees', {opt: 'import'}, { + error: function(text, data, xhr) { + msg = 'Error al importar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg_ok(values.msg) + get_employees() + }else{ + msg_error(values.msg) + } + } + }) +} + + +function delete_empleado(id){ + webix.ajax().del('/employees', {id: id}, function(text, xml, xhr){ + var msg = 'Empleado eliminado correctamente' + if (xhr.status == 200){ + $$('grid_employees').remove(id); + msg_ok(msg) + } else { + msg = 'El Empleado tiene recibos timbrados' + msg_error(msg) + } + }) +} + + +function cmd_delete_empleado_click(){ + var row = $$('grid_employees').getSelectedItem() + + if (row == undefined){ + msg = 'Selecciona un Empleado' + msg_error(msg) + return + } + + msg = '¿Estás seguro de eliminar al Empleado?

' + msg += row['nombre_completo'] + ' (' + row['rfc'] + ')' + msg += '

ESTA ACCIÓN NO SE PUEDE DESHACER

' + webix.confirm({ + title: 'Eliminar Empleado', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if (result){ + delete_empleado(row['id']) + } + } + }) +} + + +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) + } + } + }) +} + + +function cmd_nomina_log_click(){ + location = '/doc/nomlog/0' +} \ No newline at end of file diff --git a/source/static/js/controller/util.js b/source/static/js/controller/util.js index de495ba..0ab79f8 100644 --- a/source/static/js/controller/util.js +++ b/source/static/js/controller/util.js @@ -16,6 +16,7 @@ var table_series = db.addCollection('series') var table_usocfdi = db.addCollection('usocfdi') var table_relaciones = db.addCollection('relaciones') +var msg = '' var months = [ {id: -1, value: 'Todos'}, diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 76b5f75..906df6a 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -144,9 +144,14 @@ var sidebar_admin = { var emisor_datos_fiscales = [ {template: 'Datos SAT', type: 'section'}, - {cols: [{view: 'text', id: 'emisor_rfc', name: 'emisor_rfc', label: 'RFC: ', - width: 300, required: true, invalidMessage: 'RFC inválido', - readonly: true, attributes: {maxlength: 13}}, {}]}, + {cols: [ + {view: 'text', id: 'emisor_rfc', name: 'emisor_rfc', label: 'RFC: ', + width: 300, required: true, invalidMessage: 'RFC inválido', + readonly: true, attributes: {maxlength: 13}}, + {view: 'text', id: 'emisor_curp', name: 'emisor_curp', label: 'CURP: ', + width: 350, labelWidth: 100, attributes: {maxlength: 18}, + placeholder: 'Solo si timbran nómina'}, + {}]}, {view: 'text', id: 'emisor_nombre', name: 'emisor_nombre', label: 'Razón Social: ', required: true, invalidMessage: 'La Razón Social es requerida'}, @@ -182,16 +187,25 @@ var emisor_datos_fiscales = [ var emisor_otros_datos= [ {template: 'Generales', type: 'section'}, - {view: 'search', id: 'emisor_logo', icon: 'file-image-o', - name: 'emisor_logo', label: 'Logotipo: '}, - {view: 'text', id: 'emisor_nombre_comercial', - name: 'emisor_nombre_comercial', label: 'Nombre comercial: '}, - {view: 'text', id: 'emisor_telefono', name: 'emisor_telefono', - label: 'Teléfonos: '}, - {view: 'text', id: 'emisor_correo', name: 'emisor_correo', - label: 'Correos: '}, - {view: 'text', id: 'emisor_web', name: 'emisor_web', - label: 'Página Web: '}, + {cols: [ + {view: 'search', id: 'emisor_logo', icon: 'file-image-o', + name: 'emisor_logo', label: 'Logotipo: '}, + {view: 'text', id: 'emisor_nombre_comercial', + name: 'emisor_nombre_comercial', label: 'Nombre comercial: '}, + ]}, + {cols: [ + {view: 'text', id: 'emisor_telefono', name: 'emisor_telefono', + label: 'Teléfonos: '}, + {view: 'text', id: 'emisor_correo', name: 'emisor_correo', + label: 'Correos: '}, + ]}, + {cols: [ + {view: 'text', id: 'emisor_registro_patronal', attributes: {maxlength: 20}, + name: 'emisor_registro_patronal', label: 'Registro Patronal: ', + placeholder: 'Solo para timbrado de nómina'}, + {view: 'text', id: 'emisor_web', name: 'emisor_web', + label: 'Página Web: '}, + ]}, {template: 'Escuela', type: 'section'}, {cols: [{view: 'checkbox', id: 'chk_escuela', name: 'es_escuela', label: 'Es Escuela'}, @@ -473,7 +487,6 @@ var controls_folios = [ { view: 'tabview', id: 'tab_folios', - //~ tabbar: {options: ['Folios']}, animate: true, cells: [ {id: 'Folios', rows: emisor_folios}, @@ -497,7 +510,6 @@ var controls_correo = [ var form_folios = { type: 'space', - //~ responsive: true, cols: [{ view: 'form', id: 'form_folios', @@ -528,7 +540,6 @@ var form_correo = { labelWidth: 150, labelAlign: 'right' }, - //~ autoheight: true }], } @@ -629,23 +640,25 @@ var options_admin_otros = [ {view: 'checkbox', id: 'chk_ticket_total_up', labelWidth: 0, labelRight: 'Mostrar total arriba'}, {}]}, - + {maxHeight: 20}, + {template: 'Nómina', type: 'section'}, + {cols: [{maxWidth: 15}, + {view: 'checkbox', id: 'chk_usar_nomina', labelWidth: 0, + labelRight: 'Usar timbrado de Nómina'}, + {}]}, {}] - -var body_admin_otros = { - view: 'scrollview', body: {rows: options_admin_otros}, -} - var tab_options = { view: 'tabview', id: 'tab_options', animate: true, cells: [ - {id: 'Plantillas', rows: options_templates}, - {id: 'Otros', rows: options_admin_otros}, - ], + {header: 'Plantillas', body: {id: 'tab_admin_templates', + rows: options_templates}}, + {header: 'Otros', body: {id: 'tab_admin_otros', view: 'scrollview', + body: {rows: options_admin_otros}}}, + ] } @@ -725,7 +738,6 @@ var grid_admin_taxes = { view: 'datatable', id: 'grid_admin_taxes', select: 'cell', - //~ multiselect: true, adjust: true, autoheight: true, autowidth: true, diff --git a/source/static/js/ui/main.js b/source/static/js/ui/main.js index 7462eae..0fe6ecf 100644 --- a/source/static/js/ui/main.js +++ b/source/static/js/ui/main.js @@ -2,7 +2,7 @@ var menu_data = [ {id: 'app_home', icon: 'dashboard', value: 'Inicio'}, - {id: 'app_partners', icon: 'users', value: 'Clientes y Proveedores'}, + {id: 'app_partners', icon: 'address-book-o', value: 'Clientes y Proveedores'}, {id: 'app_products', icon: 'server', value: 'Productos y Servicios'}, {id: 'app_bancos', icon: 'university', value: 'Bancos'}, {id: 'app_invoices', icon: 'file-code-o', value: 'Facturas'}, @@ -38,6 +38,7 @@ var multi_main = { app_products, app_bancos, app_school, + app_nomina, app_tickets, app_invoices, ], diff --git a/source/static/js/ui/main.js.orig b/source/static/js/ui/main.js.orig new file mode 100644 index 0000000..77c5018 --- /dev/null +++ b/source/static/js/ui/main.js.orig @@ -0,0 +1,90 @@ + + +var menu_data = [ + {id: 'app_home', icon: 'dashboard', value: 'Inicio'}, + {id: 'app_partners', icon: 'address-book-o', value: 'Clientes y Proveedores'}, + {id: 'app_products', icon: 'server', value: 'Productos y Servicios'}, + {id: 'app_bancos', icon: 'university', value: 'Bancos'}, + {id: 'app_invoices', icon: 'file-code-o', value: 'Facturas'}, +] + + +var sidebar = { + view: 'sidebar', + id: 'main_sidebar', + data: menu_data, + ready: function(){ + this.select('app_home'); + this.open(this.getParentId('app_home')); + }, + on:{ + onAfterSelect: function(id){ + $$('multi').setValue(id) + } + }, +} + + +var multi_main = { + id: 'multi', + animate: true, + cells:[ + { + id: 'app_home', + view: 'template', + template: 'HOME' + }, + app_partners, + app_products, + app_bancos, +<<<<<<< HEAD + app_school, +======= + app_nomina, +>>>>>>> nomina + app_tickets, + app_invoices, + ], +} + + +var menu_user = { + view: 'menu', + id: 'menu_user', + width: 150, + autowidth: true, + data: [ + {id: '0', value: 'User...', submenu:[{id:1, value:'Cerrar Sesión'}]}, + ], + type: { + subsign: true, + }, +} + + +var ui_main = { + rows: [ + {view: 'toolbar', padding: 3, elements: [ + {view: 'button', type: 'icon', icon: 'bars', + width: 37, align: 'left', css: 'app_button', click: function(){ + $$('main_sidebar').toggle() + } + }, + {view: 'label', id: 'lbl_title_main', label: 'Empresa Libre'}, + {}, + menu_user, + {view: 'button', type: 'icon', width: 45, css: 'app_button', + icon: 'bell-o', badge: 0}, + {view: 'button', type: 'icon', width: 45, css: 'app_button', + icon: 'cogs', id: 'cmd_ir_al_admin', hidden: true, + click: 'cmd_ir_al_admin_click'} + ] + }, + { + cols:[ + sidebar, + multi_main, + ] + } + ] +}; diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js new file mode 100644 index 0000000..69dcc65 --- /dev/null +++ b/source/static/js/ui/nomina.js @@ -0,0 +1,242 @@ + +var toolbar_nomina = [ + {view: 'button', id: 'cmd_empleados', label: 'Empleados', type: 'iconButton', + autowidth: true, icon: 'users'}, + {}, + {view: 'button', id: 'cmd_nomina_report', label: 'Reporte', type: 'iconButton', + autowidth: true, icon: 'table'}, + {}, + {view: 'button', id: 'cmd_nomina_delete', label: 'Eliminar', + type: 'iconButton', autowidth: true, icon: 'minus'}, +] + + +var toolbar_nomina_util = [ + {view: 'button', id: 'cmd_nomina_import', label: 'Importar', + type: 'iconButton', autowidth: true, icon: 'upload'}, + {view: 'button', id: 'cmd_nomina_timbrar', label: 'Timbrar', + type: 'iconButton', autowidth: true, icon: 'ticket'}, + {view: 'button', id: 'cmd_nomina_sat', label: 'SAT', + type: 'iconButton', autowidth: true, icon: 'check-circle'}, + {view: 'button', id: 'cmd_nomina_log', label: 'Log', + type: 'iconButton', autowidth: true, icon: 'download'}, + {}, + {view: 'button', id: 'cmd_nomina_cancel', label: 'Cancelar', + type: 'iconButton', autowidth: true, icon: 'ban'}, +] + + +var toolbar_nomina_filter = [ + {view: 'richselect', id: 'filter_year_nomina', label: 'Año', + labelAlign: 'right', labelWidth: 50, width: 150, options: []}, + {view: 'richselect', id: 'filter_month_nomina', label: 'Mes', + 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'}, +] + + +var grid_cols_nomina = [ + {id: 'index', header: '#', adjust: 'data', css: 'right', + footer: {content: 'countRows', colspan: 3, css: 'right'}}, + {id: "id", header:"ID", hidden:true}, + {id: "serie", header: ["Serie"], adjust: "header"}, + {id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header', + sort: 'int', css: 'right', footer: {text: 'Recibos', colspan: 3}}, + {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', {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')}, +] + + +var grid_nomina = { + view: 'datatable', + id: 'grid_nomina', + select: 'row', + multiselect: true, + scrollY: true, + adjust: true, + footer: true, + resizeColumn: true, + headermenu: true, + columns: grid_cols_nomina, + scheme:{ + $change:function(item){ + if (item.estatus == 'Cancelado'){ + item.$css = 'cancel' + } + } + }, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + }, +} + + +var rows_nomina_home = [ + {view: 'toolbar', elements: toolbar_nomina}, + {view: 'toolbar', elements: toolbar_nomina_util}, + {view: 'toolbar', elements: toolbar_nomina_filter}, + grid_nomina, +] + + +var toolbar_nomina_empleados = [ + {view: 'button', id: 'cmd_new_empleado', label: 'Nuevo', type: 'iconButton', + autowidth: true, icon: 'user-plus'}, + {view: 'button', id: 'cmd_edit_empleado', label: 'Editar', type: 'iconButton', + autowidth: true, icon: 'user'}, + {view: 'button', id: 'cmd_delete_empleado', label: 'Eliminar', type: 'iconButton', + autowidth: true, icon: 'user-times'}, + {}, + {view: 'button', id: 'cmd_import_empleados', label: 'Importar', + type: 'iconButton', autowidth: true, icon: 'upload'}, + {}, + {view: 'button', id: 'cmd_close_empleados', label: 'Cerrar', type: 'iconButton', + autowidth: true, icon: 'times-circle-o'}, +] + + +var grid_cols_empleados = [ + {id: 'index', header: '#', adjust: 'data', css: 'right', + footer: {content: 'countRows', colspan: 3, css: 'right'}}, + {id: "id", header:"ID", hidden:true}, + {id: "num_empleado", header: ["No Empleado"], adjust: "data"}, + {id: "rfc", header: ["RFC", {content: 'textFilter'}], adjust: "data", sort: "string"}, + {id: "curp", header: ["CURP"], adjust: "data", hidden:true, sort: "string"}, + {id: "nombre_completo", header: ["Empleado", {content: 'textFilter'}], + adjust: "data", fillspace: true, sort: "string"}, + {id: 'imss', header: ['IMSS'], adjust: 'data'}, + {id: 'salario_base', header: ['Salario Base'], adjust: 'header', + format: webix.i18n.priceFormat, css: 'right'}, + {id: 'salario_diario', header: ['Salario Diario'], adjust: 'header', + format: webix.i18n.priceFormat, css: 'right'}, + {id: "fecha_ingreso", header: ["Fecha de Ingreso"], adjust: "header", + sort: "string"}, +] + + +var grid_empleados = { + view: 'datatable', + id: 'grid_employees', + select: 'row', + scrollY: true, + adjust: true, + footer: true, + resizeColumn: true, + headermenu: true, + columns: grid_cols_empleados, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + }, +} + + +var rows_nomina_empleados = [ + {view: 'toolbar', elements: toolbar_nomina_empleados}, + grid_empleados, +] + + +var multi_nomina = { + id: 'multi_nomina', + view: 'multiview', + animate: true, + cells:[ + {id: 'nomina_home', rows: rows_nomina_home}, + {id: 'nomina_empleados', rows: rows_nomina_empleados}, + ], +} + + +var app_nomina = { + id: 'app_nomina', + rows:[ + {view: 'template', type: 'header', template: 'Timbrado de Nómina'}, + multi_nomina + ], +} + + +var body_import_employees = {rows: [ + {view: 'form', id: 'form_upload_employees', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_employees', autosend: false, + link: 'lst_upload_employees', value: 'Seleccionar Plantilla', + upload: '/files/employees'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_upload_employees', name: 'lst_employees', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_import_employees', + label: 'Importar Empleados'}, {}]}, + ]}, +],} + + +var win_import_employees = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_import_employees', + width: 400, + modal: true, + position: 'center', + head: 'Importar Empleados', + body: body_import_employees, + }) + $$('cmd_import_employees').attachEvent('onItemClick', cmd_import_employees_click) + $$('up_employees').attachEvent('onUploadComplete', up_employees_upload_complete) + } +} + + +var body_import_nomina = {rows: [ + {view: 'form', id: 'form_upload_nomina', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_nomina', autosend: false, + link: 'lst_upload_nomina', value: 'Seleccionar Plantilla', + upload: '/files/nomina'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_upload_nomina', name: 'lst_nomina', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_import_template_nomina', + label: 'Importar Nómina'}, {}]}, + ]}, +],} + + +var win_import_nomina = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_import_nomina', + width: 400, + modal: true, + position: 'center', + head: 'Importar Nómina', + body: body_import_nomina, + }) + $$('cmd_import_template_nomina').attachEvent('onItemClick', cmd_import_template_nomina_click) + $$('up_nomina').attachEvent('onUploadComplete', up_nomina_upload_complete) + } +} diff --git a/source/static/js/ui/products.js b/source/static/js/ui/products.js index d83904e..72dd511 100644 --- a/source/static/js/ui/products.js +++ b/source/static/js/ui/products.js @@ -209,9 +209,10 @@ var multi_products = { var app_products = { - id: "app_products", + id: 'app_products', rows:[ - {view: "template", id: "th_products", type: "header", template:"Administración de Productos" }, + {view: 'template', id: 'th_products', type: 'header', + template: 'Administración de Productos y Servicios'}, multi_products ], } diff --git a/source/templates/main.html b/source/templates/main.html index eb2a7aa..cdfb842 100644 --- a/source/templates/main.html +++ b/source/templates/main.html @@ -9,6 +9,7 @@ + @@ -17,6 +18,7 @@ + diff --git a/source/templates/main.html.orig b/source/templates/main.html.orig new file mode 100644 index 0000000..b5ee9cb --- /dev/null +++ b/source/templates/main.html.orig @@ -0,0 +1,50 @@ +<%inherit file="base.html"/> + +<%block name="media"> + + + + + + + +<<<<<<< HEAD + +======= + +>>>>>>> nomina + + + + + + + +<<<<<<< HEAD + +======= + +>>>>>>> nomina + + + + + + +<%block name="content"> + + + + + + diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura.ods index 1f332ce..c66d20f 100644 Binary files a/source/templates/plantilla_factura.ods and b/source/templates/plantilla_factura.ods differ