From bebcd297102d97e7b80d7752920a5d2314ad5554 Mon Sep 17 00:00:00 2001 From: El Mau Date: Mon, 31 Jan 2022 18:42:02 -0600 Subject: [PATCH 1/5] Agregar cadena para Cfdi 4 --- source/xslt/cadena.xslt | 748 ++++++++++++++++++---------------- source/xslt/cadena3.3.xslt | 347 ++++++++++++++++ source/xslt/utilerias.xslt | 36 +- source/xslt/utilerias1.1.xslt | 22 + 4 files changed, 788 insertions(+), 365 deletions(-) create mode 100644 source/xslt/cadena3.3.xslt create mode 100644 source/xslt/utilerias1.1.xslt diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt index f77402c..1ca5fff 100644 --- a/source/xslt/cadena.xslt +++ b/source/xslt/cadena.xslt @@ -1,347 +1,401 @@ - - - - - - - - - - - - - - - - - - - - - - - - ||| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + ||| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/cadena3.3.xslt b/source/xslt/cadena3.3.xslt new file mode 100644 index 0000000..f77402c --- /dev/null +++ b/source/xslt/cadena3.3.xslt @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + ||| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/utilerias.xslt b/source/xslt/utilerias.xslt index d5dd14e..4ae4bf4 100644 --- a/source/xslt/utilerias.xslt +++ b/source/xslt/utilerias.xslt @@ -1,22 +1,22 @@ - + - - - | - - - + + + | + + + - - - - | - - - - - - - + + + + | + + + + + + + diff --git a/source/xslt/utilerias1.1.xslt b/source/xslt/utilerias1.1.xslt new file mode 100644 index 0000000..d5dd14e --- /dev/null +++ b/source/xslt/utilerias1.1.xslt @@ -0,0 +1,22 @@ + + + + + + | + + + + + + + + | + + + + + + + + From 1991c68b3ba8123bb2cf268b98f98bf7247fd1ca Mon Sep 17 00:00:00 2001 From: El Mau Date: Thu, 10 Mar 2022 20:29:50 -0600 Subject: [PATCH 2/5] Soporte basico para Comercio Exterior --- CHANGELOG.md | 6 + VERSION | 2 +- source/app/controllers/cfdi_xml.py | 25 +++- source/app/controllers/main.py | 12 ++ source/app/controllers/util.py | 11 +- source/app/controllers/utils.py | 8 +- source/app/main.py | 2 + source/app/models/db.py | 3 + source/app/models/main.py | 144 +++++++++++++++++++++--- source/app/settings.py | 5 +- source/static/js/controller/invoices.js | 13 +++ source/static/js/controller/partners.js | 41 +++++++ source/static/js/ui/invoices.js | 5 + source/static/js/ui/partners.js | 10 +- 14 files changed, 257 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6843a0..8961f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +v 2.0.0 [31-Mar-2022] +---------------------- + - Primera versión de timbrado con CFDI4 + - **IMPORTANTE** NO intentes timbrar si **antes** no has validado en nuestro demo que puedes timbrar tus CFDIs habituales. + + v 1.47.0 [28-Mar-2022] ---------------------- - Mejora: Soporte basico para complemento Comercio Exterior. diff --git a/VERSION b/VERSION index 21998d3..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.47.0 +2.0.0 diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 5e17926..3dafadd 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -25,16 +25,21 @@ from logbook import Logger log = Logger('XML') -CFDI_ACTUAL = 'cfdi33' +CFDI_ACTUAL = 'cfdi40' NOMINA_ACTUAL = 'nomina12' +DEFAULT = { + 'exportacion': '01', +} + + SAT = { 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'cfdi32': { - 'version': '3.2', + 'cfdi40': { + 'version': '4.0', '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', + 'xmlns': 'http://www.sat.gob.mx/cfd/4', + 'schema': 'http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd', }, 'cfdi33': { 'version': '3.3', @@ -42,6 +47,12 @@ SAT = { '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', }, + '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', + }, 'nomina11': { 'version': '1.1', 'prefix': 'nomina', @@ -270,6 +281,10 @@ class CFDI(object): if not 'Fecha' in attributes: attributes['Fecha'] = self._now() + # ~ cfdi4 + if not 'Exportacion' in attributes: + attributes['Exportacion'] = DEFAULT['exportacion'] + self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes) return diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 1b333ed..5c25183 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -797,3 +797,15 @@ class AppSATUnidadesPeso(object): user = req.env['beaker.session']['userobj'] req.context['result'] = self._db.sat_unidades_peso_post(values, user) resp.status = falcon.HTTP_200 + + +class AppSATRegimenes(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + user = req.env['beaker.session']['userobj'] + req.context['result'] = self._db.sat_regimenes_get(values, user) + resp.status = falcon.HTTP_200 diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 548bbf1..68023b5 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -78,6 +78,7 @@ import segno from .pacs.cfdi_cert import SATCertificate from settings import ( + CFDI_VERSIONS, EXT, MXN, PATHS, @@ -1754,7 +1755,7 @@ def _comprobante(doc, options): data['tiporelacion'] = options.get('tiporelacion', '') return data - if data['version'] == '3.3': + if data['version'] in CFDI_VERSIONS: tipos = { 'I': 'ingreso', 'E': 'egreso', @@ -1857,7 +1858,7 @@ def _conceptos(doc, version, options): data.append(values) continue - if version == '3.3': + if version in CFDI_VERSIONS: if 'noidentificacion' in values: values['noidentificacion'] = '{}\n(SAT {})'.format( values['noidentificacion'], values['ClaveProdServ']) @@ -1921,7 +1922,7 @@ def _totales(doc, cfdi, version): # ~ for n in node.getchildren(): for n in list(node): tmp = CaseInsensitiveDict(n.attrib.copy()) - if version == '3.3': + if version in CFDI_VERSIONS: tasa = round(float(tmp['tasaocuota']), DECIMALES) title = 'Traslado {} {}'.format(tn.get(tmp['impuesto']), tasa) else: @@ -1933,7 +1934,7 @@ def _totales(doc, cfdi, version): # ~ for n in node.getchildren(): for n in list(node): tmp = CaseInsensitiveDict(n.attrib.copy()) - if version == '3.3': + if version in CFDI_VERSIONS: title = 'Retención {} {}'.format( tn.get(tmp['impuesto']), '') else: @@ -1965,7 +1966,7 @@ def _totales(doc, cfdi, version): def _timbre(doc, version, values, pdf_from='1'): CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||' - if version == '3.3': + if version in CFDI_VERSIONS: CADENA = '||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||' node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format( PRE[version], PRE['TIMBRE'])) diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py index 6df126f..4b14959 100644 --- a/source/app/controllers/utils.py +++ b/source/app/controllers/utils.py @@ -255,8 +255,11 @@ class SendMail(object): class CfdiToDict(object): + NS_VERSION = { + 'cfdi3.3': 'http://www.sat.gob.mx/cfd/3', + 'cfdi4.0': 'http://www.sat.gob.mx/cfd/4', + } NS = { - 'cfdi': 'http://www.sat.gob.mx/cfd/3', 'divisas': 'http://www.sat.gob.mx/divisas', 'leyendasFisc': 'http://www.sat.gob.mx/leyendasFiscales', 'cartaporte20': 'http://www.sat.gob.mx/CartaPorte20', @@ -318,6 +321,9 @@ class CfdiToDict(object): return self._values def _get_values(self): + version = self._root.attrib['Version'] + ns = f'cfdi{version}' + self.NS['cfdi'] = self.NS_VERSION[ns] self._complementos() return diff --git a/source/app/main.py b/source/app/main.py index 6346cd4..9eddc7a 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -25,6 +25,7 @@ from controllers.main import (AppEmpresas, AppWareHouse, AppWareHouseProduct, AppSATUnidadesPeso, + AppSATRegimenes, ) @@ -78,6 +79,7 @@ api.add_route('/warehouseproduct', AppWareHouseProduct(db)) api.add_route('/ticketsdetails', AppTicketsDetails(db)) api.add_route('/users', AppUsers(db)) api.add_route('/satunidadespeso', AppSATUnidadesPeso(db)) +api.add_route('/satregimenes', AppSATRegimenes(db)) session_options = { diff --git a/source/app/models/db.py b/source/app/models/db.py index 3d55374..80a0ba3 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -526,6 +526,9 @@ class StorageEngine(object): def sat_unidades_peso_post(self, args, user): return main.SATUnidadesPeso.post(args, user) + def sat_regimenes_get(self, filters, user): + return main.SATRegimenes.get_data(filters, user) + # Companies only in MV def _get_empresas(self, values): return main.companies_get() diff --git a/source/app/models/main.py b/source/app/models/main.py index afd8add..8f01c77 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -939,6 +939,26 @@ class SATRegimenes(BaseModel): ) return tuple(rows) + @classmethod + def _get_actives(cls, filters, user): + where = ((SATRegimenes.activo==True) & (SATRegimenes.fisica==True)) + if (filters['morales']=='true'): + where = ((SATRegimenes.activo==True) & (SATRegimenes.moral==True)) + + rows = (SATRegimenes + .select( + SATRegimenes.id, + SATRegimenes.name.alias('value')) + .where(where) + .dicts() + ) + return tuple(rows) + + @classmethod + def get_data(cls, filters, user): + opt = filters['opt'] + return getattr(cls, f'_get_{opt}')(filters, user) + class Emisor(BaseModel): rfc = TextField(unique=True) @@ -1027,8 +1047,6 @@ class Emisor(BaseModel): '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] @@ -2787,6 +2805,13 @@ class Socios(BaseModel): if fields['pais'] != 'México': fields['pais'] = fields['pais'].upper() + if 'regimenes' in fields: + fields['regimenes'] = utils.loads(fields['regimenes']) + if isinstance(fields['regimenes'], list): + fields['regimenes'] = tuple(map(int, fields['regimenes'])) + else: + fields['regimenes'] = (fields['regimenes'],) + return fields @classmethod @@ -2802,18 +2827,9 @@ class Socios(BaseModel): str(CondicionesPago.get(id=row['condicion_pago'])) row['partner_balance'] = row.pop('saldo_cliente') row['partner_email_fp'] = row.pop('correo_facturasp') + row['regimenes'] = SociosRegimenes.get_by_socio(row['id']) 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 @@ -2829,19 +2845,23 @@ class Socios(BaseModel): @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')) + SATUsoCfdi.key.alias('uso_cfdi'), + Socios.codigo_postal) .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]} + client = row[0] + client['regimenes'] = SociosRegimenes.get_by_key(client['id']) + return {'ok': True, 'row': client} return {'ok': False} name = values.get('name', '') @@ -2849,7 +2869,8 @@ class Socios(BaseModel): rows = (Socios .select(Socios.id, Socios.nombre, Socios.rfc, SATFormaPago.key.alias('forma_pago'), - SATUsoCfdi.key.alias('uso_cfdi')) + SATUsoCfdi.key.alias('uso_cfdi'), + Socios.codigo_postal) .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) .where((Socios.es_cliente==True & Socios.es_activo==True) & @@ -2863,6 +2884,7 @@ class Socios(BaseModel): def add(cls, values): accounts = util.loads(values.pop('accounts', '[]')) fields = cls._clean(cls, values) + regimenes = fields.pop('regimenes', ()) w = ((Socios.rfc==fields['rfc']) & (Socios.slug==fields['slug'])) if Socios.select().where(w).exists(): @@ -2892,6 +2914,16 @@ class Socios(BaseModel): except IntegrityError: pass + for regimen in regimenes: + try: + fields = { + 'socio': obj, + 'regimen': regimen, + } + SociosRegimenes.create(**fields) + except IntegrityError: + pass + row = { 'id': obj.id, 'rfc': obj.rfc, @@ -2905,6 +2937,8 @@ class Socios(BaseModel): def actualizar(cls, values, id): fields = cls._clean(cls, values) fields.pop('accounts', '') + regimenes = fields.pop('regimenes', ()) + try: q = Socios.update(**fields).where(Socios.id==id) q.execute() @@ -2913,6 +2947,19 @@ class Socios(BaseModel): data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} return data + obj = Socios.get(Socios.id==id) + q = SociosRegimenes.delete().where(SociosRegimenes.socio==id) + q.execute() + for regimen in regimenes: + try: + fields = { + 'socio': obj, + 'regimen': regimen, + } + SociosRegimenes.create(**fields) + except IntegrityError: + pass + obj = Socios.get(Socios.id==id) row = { 'id': id, @@ -2934,6 +2981,8 @@ class Socios(BaseModel): q = SociosCuentasBanco.delete().where(SociosCuentasBanco.socio==id) q.execute() + q = SociosRegimenes.delete().where(SociosRegimenes.socio==id) + q.execute() q = Socios.delete().where(Socios.id==id) return bool(q.execute()) @@ -3040,6 +3089,41 @@ class SociosCuentasBanco(BaseModel): return account.socio == invoice.cliente +class SociosRegimenes(BaseModel): + socio = ForeignKeyField(Socios) + regimen = ForeignKeyField(SATRegimenes) + + class Meta: + indexes = ( + (('socio', 'regimen'), True), + ) + + @classmethod + def get_by_key(self, socio): + fields = (SATRegimenes.key.alias('id'), SATRegimenes.name.alias('value')) + where = (SociosRegimenes.socio == socio) + regimenes = (SociosRegimenes + .select(*fields) + .where(where) + .join(SATRegimenes).switch(SociosRegimenes) + .dicts() + ) + return tuple(regimenes) + + @classmethod + def get_by_socio(self, socio): + fields = (SATRegimenes.id,) + where = (SociosRegimenes.socio == socio) + regimenes = (SociosRegimenes + .select(*fields) + .where(where) + .join(SATRegimenes).switch(SociosRegimenes) + .tuples() + ) + regimenes = [r[0] for r in regimenes] + return regimenes + + class Contactos(BaseModel): socio = ForeignKeyField(Socios) titulo = ForeignKeyField(TipoTitulo) @@ -3806,6 +3890,7 @@ class Productos(BaseModel): cantidad_empaque = DecimalField(default=0.0, max_digits=14, decimal_places=4, auto_round=True) is_discontinued = BooleanField(default=False) + objeto_impuesto = TextField(default='02') class Meta: order_by = ('descripcion',) @@ -4389,6 +4474,8 @@ class Facturas(BaseModel): egreso_anticipo = BooleanField(default=False) tipo_relacion = TextField(default='') error = TextField(default='') + exportacion = TextField(default='01') + receptor_regimen = TextField(default='') class Meta: order_by = ('fecha',) @@ -5590,6 +5677,8 @@ class Facturas(BaseModel): 'Rfc': invoice.cliente.rfc, 'Nombre': invoice.cliente.nombre, 'UsoCFDI': invoice.uso_cfdi, + 'DomicilioFiscalReceptor': invoice.cliente.codigo_postal, + 'RegimenFiscalReceptor': invoice.receptor_regimen } if invoice.cliente.tipo_persona == 4: if invoice.cliente.pais: @@ -5701,6 +5790,16 @@ class Facturas(BaseModel): taxes['retenciones'] = retenciones concepto['impuestos'] = taxes + + # cfdi4 + if row.producto.objeto_impuesto: + concepto['ObjetoImp'] = row.producto.objeto_impuesto + else: + if taxes: + concepto['ObjetoImp'] = '02' + else: + concepto['ObjetoImp'] = '01' + conceptos.append(concepto) impuestos = {} @@ -5749,11 +5848,14 @@ class Facturas(BaseModel): if tax_decimals: xml_importe = FORMAT_TAX.format(tax.importe) + xml_tax_base = FORMAT_TAX.format(tax.base) else: xml_importe = FORMAT.format(tax.importe) + xml_tax_base = FORMAT.format(tax.base) if tax.impuesto.tipo == 'T': traslado = { + "Base": xml_tax_base, "Impuesto": tax.impuesto.key, "TipoFactor": tipo_factor, "TasaOCuota": str(tax.impuesto.tasa), @@ -10546,6 +10648,7 @@ def _crear_tablas(rfc): PartnerInvoices, WareHouseProduct, SATUnidadesPeso, + SociosRegimenes, ] log.info('Creando tablas...') database_proxy.create_tables(tablas, True) @@ -10604,6 +10707,7 @@ def _migrate_tables(rfc=''): PartnerInvoices, WareHouseProduct, SATUnidadesPeso, + SociosRegimenes, ] log.info('Creando tablas nuevas...') database_proxy.create_tables(tablas, True) @@ -10729,6 +10833,10 @@ def _migrate_tables(rfc=''): is_discontinued = BooleanField(default=False) migrations.append(migrator.add_column( table, 'is_discontinued', is_discontinued)) + if not 'objeto_impuesto' in columns: + objeto_impuesto = TextField(default='02') + migrations.append(migrator.add_column(table, 'objeto_impuesto', objeto_impuesto)) + if 'almacen_id' in columns: migrations.append(migrator.drop_column(table, 'almacen_id')) @@ -10745,6 +10853,12 @@ def _migrate_tables(rfc=''): if not 'divisas' in columns: divisas = TextField(default='') migrations.append(migrator.add_column(table, 'divisas', divisas)) + if not 'exportacion' in columns: + new_field = TextField(default='01') + migrations.append(migrator.add_column(table, 'exportacion', new_field)) + if not 'receptor_regimen' in columns: + receptor_regimen = TextField(default='') + migrations.append(migrator.add_column(table, 'receptor_regimen', receptor_regimen)) table = 'almacenes' columns = [c.name for c in database_proxy.get_columns(table)] diff --git a/source/app/settings.py b/source/app/settings.py index 00624bc..1f404ea 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -42,7 +42,8 @@ except ImportError: DEBUG = DEBUG -VERSION = '1.47.0' +VERSION = '2.0.0' + EMAIL_SUPPORT = ('soporte@empresalibre.mx',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) @@ -133,6 +134,7 @@ PRE = { '3.0': '{http://www.sat.gob.mx/cfd/3}', '3.2': '{http://www.sat.gob.mx/cfd/3}', '3.3': '{http://www.sat.gob.mx/cfd/3}', + '4.0': '{http://www.sat.gob.mx/cfd/4}', 'TIMBRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}', 'DONATARIA': '{http://www.sat.gob.mx/donat}', 'INE': '{http://www.sat.gob.mx/ine}', @@ -193,6 +195,7 @@ CURRENCY_MN = 'MXN' # ~ v2 CANCEL_VERSION = ('3.3', '4.0') +CFDI_VERSIONS = CANCEL_VERSION IS_MV = MV DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index e060d42..22949b6 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -685,6 +685,7 @@ function guardar_y_timbrar(values){ data['metodo_pago'] = $$('lst_metodo_pago').getValue() data['uso_cfdi'] = $$('lst_uso_cfdi').getValue() data['regimen_fiscal'] = $$('lst_regimen_fiscal').getValue() + data['receptor_regimen'] = $$('lst_invoice_client_regimen').getValue() data['relacionados'] = ids data['tipo_relacion'] = tipo_relacion data['anticipo'] = anticipo @@ -904,6 +905,12 @@ function search_client_by_id(id){ function set_client(row){ + if(!row.codigo_postal){ + msg = 'El cliente no tiene capturado su Código Postal, es obligatorio.' + msg_error(msg) + return + } + var form = $$('form_invoice') var html = '' form.setValues({ @@ -913,6 +920,12 @@ function set_client(row){ html += row.nombre + ' (' + row.rfc + ')' $$('lbl_client').setValue(html) $$('cmd_cfdi_relacionados').enable() + + var lst = $$('lst_invoice_client_regimen') + lst.getList().clearAll() + lst.getList().parse(row.regimenes) + lst.setValue(lst.getPopup().getList().getFirstId()) + form.focus('search_product_id') } diff --git a/source/static/js/controller/partners.js b/source/static/js/controller/partners.js index e6dd2ab..87d2269 100644 --- a/source/static/js/controller/partners.js +++ b/source/static/js/controller/partners.js @@ -95,6 +95,7 @@ function cmd_new_partner_click(id, e, node){ $$('partner_balance').define('readonly', !cfg_partners['chk_config_change_balance_partner']) get_partner_banks() get_partner_accounts_bank(0) + get_sat_regimenes() } @@ -108,6 +109,8 @@ function cmd_edit_partner_click(){ var msg = '' var row = $$('grid_partners').getSelectedItem() + get_sat_regimenes() + $$('form_partner_account_bank').clearValidation() if (row == undefined){ @@ -145,6 +148,7 @@ function cmd_edit_partner_click(){ $$('cuenta_proveedor').enable() } get_partner_accounts_bank(row['id']) + $$('lst_receptor_regimenes_fiscales').select(values.regimenes) } }) @@ -239,7 +243,17 @@ function cmd_save_partner_click(id, e, node){ } } + var ids_regimenes = $$('lst_receptor_regimenes_fiscales').getSelectedId() + if(values.tipo_persona < 3){ + if(!ids_regimenes){ + msg = 'Selecciona al menos un Regimen Fiscal' + msg_error(msg) + return + } + } + values['accounts'] = $$('grid_partner_account_bank').data.getRange() + values['regimenes'] = ids_regimenes webix.ajax().post('/partners', values, { error:function(text, data, XmlHttpRequest){ @@ -343,9 +357,16 @@ function opt_tipo_change(new_value, old_value){ $$('id_fiscal').define('value', '') show('id_fiscal', new_value == 4) + $$('lst_receptor_regimenes_fiscales').clearAll() + if (new_value == 1 || new_value == 2){ $$("rfc").define("value", "") $$("rfc").define("readonly", false) + moral = false + if(new_value == 2){ + moral = true + } + get_sat_regimenes(moral) } else if (new_value == 3) { $$("rfc").define("value", RFC_PUBLICO) $$("nombre").define("value", PUBLICO) @@ -376,6 +397,8 @@ function opt_tipo_change(new_value, old_value){ } $$('lst_uso_cfdi_socio').getList().parse(query) $$('lst_uso_cfdi_socio').refresh() + + } @@ -619,3 +642,21 @@ function partner_delete_account_bank(row){ } }) } + + +function get_sat_regimenes(morales=false){ + var data = {opt: 'actives', morales: morales} + webix.ajax().get('/satregimenes', data, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json() + $$('lst_receptor_regimenes_fiscales').clearAll() + $$('lst_receptor_regimenes_fiscales').parse(values) + } + }) + +} + diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 4e488f2..9369461 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -418,6 +418,7 @@ var suggest_partners = { {id: 'rfc', adjust: 'data'}, {id: 'forma_pago', hidden: true}, {id: 'uso_cfdi', hidden: true}, + {id: 'codigo_postal', hidden: true}, ], dataFeed:function(text){ if (text.length > 2){ @@ -596,6 +597,10 @@ var controls_generate = [ autowidth:true}, {view: 'label', id: 'lbl_client', name: 'lbl_client', label: 'Ninguno'}, + ]}, + {cols: [{ + view: 'richselect', id: 'lst_invoice_client_regimen', + label: 'Regimen Fiscal: ', labelWidth: 150, options: []} ]} ]}}, {view: 'fieldset', label: 'Buscar Producto', body: {rows: [ diff --git a/source/static/js/ui/partners.js b/source/static/js/ui/partners.js index d7aeab2..7a125c0 100644 --- a/source/static/js/ui/partners.js +++ b/source/static/js/ui/partners.js @@ -96,7 +96,7 @@ var controls_fiscales = [ {cols: [{view: 'text', id: 'no_interior', name: 'no_interior', width: 300, label: 'No Interior: '},{}]}, {cols: [{view: 'search', id: 'codigo_postal', name: 'codigo_postal', - width: 300, label: 'C.P.: ', attributes: {maxlength: 5}},{}]}, + width: 300, label: 'C.P.: ', attributes: {maxlength: 5}, required: true},{}]}, {view: 'text', id: 'colonia', name: 'colonia', label: 'Colonia: '}, {view: 'text', id: 'municipio', name: 'municipio', label: 'Municipio: '}, {view: 'text', id: 'estado', name: 'estado', label: 'Estado: '}, @@ -122,6 +122,12 @@ var controls_fiscales = [ {view: 'richselect', id: 'lst_uso_cfdi_socio', name: 'uso_cfdi_socio', label: 'Uso del CFDI', options: []}, {}, + ]}, + {template: 'Regimenes Fiscales', type: 'section'}, + {cols: [ + {view: 'list', id: 'lst_receptor_regimenes_fiscales', data: [], + select: 'multiselect', width: 600, height: 125, required: true}, + {}, ]} ] @@ -159,7 +165,7 @@ var controls_others = [ label: 'Cuenta Proveedor: ', disabled: true}, {}] }, {view: 'checkbox', name: 'es_ong', label: 'Es ONG: ', value: false}, - {view: 'text', name: 'tags', label: 'Etiquetas', + {view: 'text', name: 'tags', label: 'Etiquetas', disabled: true, tooltip: 'Utiles para filtrados rápidos. Separa por comas.'}, {view: 'textarea' , height: 200, name: 'notas', label: 'Notas'}, ] From a7bc6d6f6cda8e24f2c3874f29a9b19c4ac6c072 Mon Sep 17 00:00:00 2001 From: El Mau Date: Tue, 22 Feb 2022 14:53:44 -0600 Subject: [PATCH 3/5] Agregar xlst para pagos 2.0 --- source/xslt/cadena.xslt | 4 +- source/xslt/pagos20.xslt | 233 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 source/xslt/pagos20.xslt diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt index 1ca5fff..db72919 100644 --- a/source/xslt/cadena.xslt +++ b/source/xslt/cadena.xslt @@ -5,6 +5,8 @@ + + diff --git a/source/xslt/pagos20.xslt b/source/xslt/pagos20.xslt new file mode 100644 index 0000000..1e6cf98 --- /dev/null +++ b/source/xslt/pagos20.xslt @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7ca87d181171266690d910a11704af68203e9ff3 Mon Sep 17 00:00:00 2001 From: El Mau Date: Tue, 22 Feb 2022 22:52:47 -0600 Subject: [PATCH 4/5] Timbrado de Complemento de Pagos 2.0 --- source/app/controllers/cfdi_xml.py | 37 ++++++-- source/app/models/main.py | 136 +++++++++++++++++++++++++++-- source/app/settings.py | 5 +- 3 files changed, 166 insertions(+), 12 deletions(-) diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 3dafadd..c06682d 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -91,10 +91,10 @@ SAT = { 'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/iedu/iedu.xsd', }, 'pagos': { - 'version': '1.0', - 'prefix': 'pago10', - 'xmlns': 'http://www.sat.gob.mx/Pagos', - 'schema': ' http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd', + 'version': '2.0', + 'prefix': 'pago20', + 'xmlns': 'http://www.sat.gob.mx/Pagos20', + 'schema': ' http://www.sat.gob.mx/Pagos20 http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos20.xsd', }, 'divisas': { 'version': '1.0', @@ -568,14 +568,41 @@ class CFDI(object): if 'pagos' in datos: datos = datos.pop('pagos') + totales = datos.pop('totales') relacionados = datos.pop('relacionados') + taxes_pay = datos.pop('taxes_pay') pre = SAT['pagos']['prefix'] + attributes = {'Version': SAT['pagos']['version']} pagos = ET.SubElement( self._complemento, '{}:Pagos'.format(pre), attributes) + + ET.SubElement(pagos, '{}:Totales'.format(pre), totales) + node_pago = ET.SubElement(pagos, '{}:Pago'.format(pre), datos) for row in relacionados: - ET.SubElement(node_pago, '{}:DoctoRelacionado'.format(pre), row) + taxes = row.pop('taxes') + node = ET.SubElement(node_pago, f'{pre}:DoctoRelacionado', row) + node_tax = ET.SubElement(node, f'{pre}:ImpuestosDR') + if taxes['retenciones']: + node = ET.SubElement(node_tax, f'{pre}:RetencionsDR') + for tax in taxes['retenciones']: + ET.SubElement(node, f'{pre}:RetencionDR', tax) + if taxes['traslados']: + node = ET.SubElement(node_tax, f'{pre}:TrasladosDR') + for tax in taxes['traslados']: + ET.SubElement(node, f'{pre}:TrasladoDR', tax) + + node_tax = ET.SubElement(node_pago, f'{pre}:ImpuestosP') + if taxes_pay['retenciones']: + node = ET.SubElement(node_tax, f'{pre}:RetencionsP') + for key, importe in taxes_pay['retenciones'].items(): + attr = {'ImpuestoP': key, 'ImporteP': importe} + ET.SubElement(node, f'{pre}:RetencionP', attr) + if taxes_pay['traslados']: + node = ET.SubElement(node_tax, f'{pre}:TrasladosP') + for key, tax in taxes_pay['traslados'].items(): + ET.SubElement(node, f'{pre}:TrasladoP', tax) if 'leyendas' in datos: pre = SAT['leyendas']['prefix'] diff --git a/source/app/models/main.py b/source/app/models/main.py index 8f01c77..c92b155 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -2762,6 +2762,7 @@ class Socios(BaseModel): uso_cfdi = ForeignKeyField(SATUsoCfdi, null=True) tags = ManyToManyField(Tags, related_name='socios_tags') plantilla = TextField(default='') + regimen_fiscal = TextField(default='') def __str__(self): t = '{} ({})' @@ -7242,6 +7243,7 @@ class CfdiPagos(BaseModel): error = TextField(default='') cancelada = BooleanField(default=False) fecha_cancelacion = DateTimeField(null=True) + receptor_regimen = TextField(default='') class Meta: order_by = ('movimiento',) @@ -7365,6 +7367,7 @@ class CfdiPagos(BaseModel): partner = related[0].factura.cliente partner_name = related[0].factura.cliente.nombre + receptor_regimen = related[0].factura.receptor_regimen emisor = Emisor.select()[0] # ~ regimen_fiscal = related[0].factura.regimen_fiscal @@ -7404,6 +7407,7 @@ class CfdiPagos(BaseModel): fields['folio'] = self._get_folio(self, serie) fields['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal fields['regimen_fiscal'] = regimen_fiscal + fields['receptor_regimen'] = receptor_regimen with database_proxy.atomic() as txn: obj = CfdiPagos.create(**fields) @@ -7421,7 +7425,85 @@ class CfdiPagos(BaseModel): data = {'ok': True, 'row': row, 'new': True} return data + def _get_taxes_by_pay(self, pay, taxes_pay): + # ~ print(pay['ImpPagado'] + invoice = Facturas.get(Facturas.uuid==pay['IdDocumento']) + impuestos = {} + traslados = [] + retenciones = [] + + where = (FacturasImpuestos.factura==invoice) + taxes = FacturasImpuestos.select().where(where) + + 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) + # ~ xml_tax_base = FORMAT_TAX.format(tax.base) + # ~ else: + xml_importe = FORMAT.format(tax.importe) + xml_tax_base = FORMAT.format(tax.base) + + values = { + "BaseDR": xml_tax_base, + "ImpuestoDR": tax.impuesto.key, + "TipoFactorDR": tipo_factor, + "TasaOCuotaDR": str(tax.impuesto.tasa), + "ImporteDR": xml_importe, + } + tax_key = tax.impuesto.key + if tax.impuesto.tipo == 'T': + traslados.append(values) + if tax_key in taxes_pay['traslados']: + taxes_pay['traslados'][tax_key]['ImporteP'] += tax.importe + else: + values = { + "BaseP": tax.base, + "ImpuestoP": tax.impuesto.key, + "TipoFactorP": tipo_factor, + "TasaOCuotaP": str(tax.impuesto.tasa), + "ImporteP": tax.importe, + } + taxes_pay['traslados'][tax_key] = values + else: + retenciones.append(values) + if tax_key in taxes_pay['retenciones']: + taxes_pay['retenciones'][tax_key] += tax.importe + else: + taxes_pay['retenciones'][tax_key] = tax.importe + + impuestos['traslados'] = traslados + impuestos['retenciones'] = retenciones + + return impuestos + def _get_related_xml(self, id_mov, currency): + TAX_IVA_16 = '002|0.160000' + filters = (FacturasPagos.movimiento==id_mov) related = tuple(FacturasPagos.select( Facturas.uuid.alias('IdDocumento'), @@ -7438,13 +7520,21 @@ class CfdiPagos(BaseModel): .where(filters) .dicts()) + taxes_pay = {'retenciones': {}, 'traslados': {}, 'totales': {}} + for r in related: + r['taxes'] = self._get_taxes_by_pay(self, r, taxes_pay) # ~ print('\n\nMONEDA', currency, r['MonedaDR']) r['IdDocumento'] = str(r['IdDocumento']) r['Folio'] = str(r['Folio']) r['NumParcialidad'] = str(r['NumParcialidad']) r['TipoCambioDR'] = FORMAT6.format(r['TipoCambioDR']) - r['MetodoDePagoDR'] = DEFAULT_CFDIPAY['WAYPAY'] + # ~ r['MetodoDePagoDR'] = DEFAULT_CFDIPAY['WAYPAY'] + + # REVISAR + r['EquivalenciaDR'] = '1' + r['ObjetoImpDR'] = '02' + r['ImpSaldoAnt'] = FORMAT.format(r['ImpSaldoAnt']) r['ImpPagado'] = FORMAT.format(r['ImpPagado']) if round(r['ImpSaldoInsoluto'], 2) == 0.0: @@ -7456,7 +7546,28 @@ class CfdiPagos(BaseModel): if not r['Serie']: del r['Serie'] - return related + total_tax_iva_16_base = 0 + total_tax_iva_16_importe = 0 + + for key, importe in taxes_pay['retenciones'].items(): + taxes_pay['retenciones'][key] = FORMAT.format(importe) + for k, tax in taxes_pay['traslados'].items(): + tax_type = taxes_pay['traslados'][k]['ImpuestoP'] + tax_tasa = taxes_pay['traslados'][k]['TasaOCuotaP'] + tax_base = taxes_pay['traslados'][k]['BaseP'] + importe = taxes_pay['traslados'][k]['ImporteP'] + if f'{tax_type}|{tax_tasa}' == TAX_IVA_16: + total_tax_iva_16_base += tax_base + total_tax_iva_16_importe += importe + taxes_pay['traslados'][k]['BaseP'] = FORMAT.format(tax_base) + taxes_pay['traslados'][k]['ImporteP'] = FORMAT.format(importe) + + taxes_pay['totales'] = { + 'TotalTrasladosBaseIVA16': FORMAT.format(total_tax_iva_16_base), + 'TotalTrasladosImpuestoIVA16': FORMAT.format(total_tax_iva_16_importe), + } + + return related, taxes_pay def _generate_xml(self, invoice): emisor = Emisor.select()[0] @@ -7469,9 +7580,9 @@ class CfdiPagos(BaseModel): cfdi['Folio'] = str(invoice.folio) cfdi['Fecha'] = invoice.fecha.isoformat()[:19] cfdi['NoCertificado'] = certificado.serie - # ~ cfdi['Certificado'] = cert.cer_txt cfdi['SubTotal'] = '0' cfdi['Moneda'] = DEFAULT_CFDIPAY['CURRENCY'] + # ~ cfdi['TipoCambio'] = DEFAULT_CFDIPAY['TC'] cfdi['Total'] = '0' cfdi['TipoDeComprobante'] = invoice.tipo_comprobante cfdi['LugarExpedicion'] = invoice.lugar_expedicion @@ -7492,6 +7603,8 @@ class CfdiPagos(BaseModel): 'Rfc': invoice.socio.rfc, 'Nombre': invoice.socio.nombre, 'UsoCFDI': DEFAULT_CFDIPAY['USED'], + 'DomicilioFiscalReceptor': invoice.socio.codigo_postal, + 'RegimenFiscalReceptor': invoice.receptor_regimen } if invoice.socio.tipo_persona == 4: if invoice.socio.pais: @@ -7506,19 +7619,23 @@ class CfdiPagos(BaseModel): 'Descripcion': DEFAULT_CFDIPAY['DESCRIPTION'], 'ValorUnitario': '0', 'Importe': '0', + 'ObjetoImp': '01', },) impuestos = {} mov = invoice.movimiento currency = mov.moneda - related_docs = self._get_related_xml(self, invoice.movimiento, currency) + related_docs, taxes_pay = self._get_related_xml(self, invoice.movimiento, currency) + totales = taxes_pay.pop('totales') pagos = { 'FechaPago': mov.fecha.isoformat()[:19], 'FormaDePagoP': mov.forma_pago.key, 'MonedaP': currency, + 'TipoCambioP': '1', 'Monto': FORMAT.format(mov.deposito), 'relacionados': related_docs, + 'taxes_pay': taxes_pay, } if mov.numero_operacion: pagos['NumOperacion'] = mov.numero_operacion @@ -7533,10 +7650,12 @@ class CfdiPagos(BaseModel): pagos['RfcEmisorCtaBen'] = mov.cuenta.banco.rfc pagos['CtaBeneficiario'] = mov.cuenta.cuenta - if currency != CURRENCY_MN: pagos['TipoCambioP'] = FORMAT_TAX.format(mov.tipo_cambio) + totales['MontoTotalPagos'] = pagos['Monto'] + pagos['totales'] = totales + complementos = {'pagos': pagos} data = { 'comprobante': cfdi, @@ -10751,6 +10870,10 @@ def _migrate_tables(rfc=''): correo_facturasp = TextField(default='') migrations.append( migrator.add_column('socios', 'correo_facturasp', correo_facturasp)) + if not 'regimen_fiscal' in columns: + regimen_fiscal = TextField(default='') + migrations.append( + migrator.add_column('socios', 'regimen_fiscal', regimen_fiscal)) columns = [c.name for c in database_proxy.get_columns('folios')] if not 'plantilla' in columns: @@ -10800,6 +10923,9 @@ def _migrate_tables(rfc=''): migrations.append(migrator.add_column('cfdipagos', 'socio_id', socio)) migrations.append(migrator.drop_column('cfdipagos', 'cancelado')) migrations.append(migrator.add_column('cfdipagos', 'cancelada', cancelada)) + if not 'receptor_regimen' in columns: + receptor_regimen = TextField(default='') + migrations.append(migrator.add_column('cfdipagos', 'receptor_regimen', receptor_regimen)) if not 'fecha_cancelacion' in columns: fecha_cancelacion = DateTimeField(null=True) diff --git a/source/app/settings.py b/source/app/settings.py index 1f404ea..9d3d1bd 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -170,7 +170,8 @@ DEFAULT_CFDIPAY = { 'TYPE': 'P', 'WAYPAY': 'PPD', 'CURRENCY': 'XXX', - 'USED': 'P01', + 'TC': '1', + 'USED': 'CP01', 'KEYSAT': '84111506', 'UNITKEY': 'ACT', 'DESCRIPTION': 'Pago', @@ -183,7 +184,7 @@ PUBLIC = 'Público en general' DEFAULT_SAT_NOMINA = { 'SERIE': 'N', 'FORMA_PAGO': '99', - 'USO_CFDI': 'P01', + 'USO_CFDI': 'CN01', 'CLAVE': '84111505', 'UNIDAD': 'ACT', 'DESCRIPCION': 'Pago de nómina', From 1978edfaf013c14e80824d4df301cd18976a4354 Mon Sep 17 00:00:00 2001 From: El Mau Date: Thu, 10 Mar 2022 11:42:39 -0600 Subject: [PATCH 5/5] Tablas para borrar --- docs/delete tables.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/delete tables.md diff --git a/docs/delete tables.md b/docs/delete tables.md new file mode 100644 index 0000000..422755c --- /dev/null +++ b/docs/delete tables.md @@ -0,0 +1,18 @@ +prefacturasdetalle; +prefacturasimpuestos; +prefacturas; + +ticketsimpuestos; +ticketsdetalle; +tickets; + +facturasrelacionadas; +facturaspersonalizados; +facturaspagos; +facturasimpuestos; +facturasdetalle; +facturascomplementos; +facturas; + +cfdipagos; +movimientosbanco;