diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index e0cc794..a7e6d08 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', @@ -554,14 +554,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 ef011d6..2e96d8c 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -2760,6 +2760,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 = '{} ({})' @@ -7223,6 +7224,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',) @@ -7346,6 +7348,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 @@ -7385,6 +7388,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) @@ -7402,7 +7406,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'), @@ -7419,13 +7501,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: @@ -7437,7 +7527,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] @@ -7450,9 +7561,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 @@ -7473,6 +7584,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: @@ -7487,19 +7600,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 @@ -7514,10 +7631,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, @@ -10732,6 +10851,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: @@ -10781,6 +10904,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 3728c9f..3ef0bcd 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -169,7 +169,8 @@ DEFAULT_CFDIPAY = { 'TYPE': 'P', 'WAYPAY': 'PPD', 'CURRENCY': 'XXX', - 'USED': 'P01', + 'TC': '1', + 'USED': 'CP01', 'KEYSAT': '84111506', 'UNITKEY': 'ACT', 'DESCRIPTION': 'Pago', @@ -182,7 +183,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',