feat(pos): add CFDI 4.0 XML builder — Ingreso, Egreso, Pago with lxml
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,451 @@
|
||||
# /home/Autopartes/pos/services/cfdi_builder.py
|
||||
"""CFDI 4.0 XML builder — stub for future implementation (Plan 4, Task 2).
|
||||
"""CFDI 4.0 XML builder using lxml.
|
||||
|
||||
Full implementation will build CFDI 4.0 compliant XML for ingreso, egreso,
|
||||
and pago voucher types using lxml.
|
||||
Builds unsigned CFDI XML documents for:
|
||||
- Ingreso (sale invoice)
|
||||
- Egreso (credit note / refund)
|
||||
- Pago (payment complement for credit sales)
|
||||
|
||||
The unsigned XML is sent to Horux360 for CSD signing and PAC timbrado.
|
||||
All XML follows the SAT CFDI 4.0 schema:
|
||||
http://www.sat.gob.mx/cfd/4
|
||||
|
||||
CFDI 4.0 mandatory fields handled:
|
||||
- Version="4.0"
|
||||
- Exportacion="01" (no aplica)
|
||||
- ObjetoImp="02" (si, objeto de impuesto) on each Concepto
|
||||
- InformacionGlobal for publico general (RFC: XAXX010101000)
|
||||
- Emisor: Rfc, Nombre, RegimenFiscal
|
||||
- Receptor: Rfc, Nombre, RegimenFiscalReceptor, UsoCFDI, DomicilioFiscalReceptor
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from lxml import etree
|
||||
|
||||
def build_ingreso_xml(sale, tenant_config, customer):
|
||||
"""Build unsigned CFDI 4.0 XML for an ingreso (income) voucher."""
|
||||
raise NotImplementedError("cfdi_builder.build_ingreso_xml not yet implemented (Plan 4, Task 2)")
|
||||
# SAT XML namespaces
|
||||
CFDI_NS = 'http://www.sat.gob.mx/cfd/4'
|
||||
PAGO_NS = 'http://www.sat.gob.mx/Pagos20'
|
||||
XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'
|
||||
|
||||
CFDI_SCHEMA_LOCATION = (
|
||||
'http://www.sat.gob.mx/cfd/4 '
|
||||
'http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd'
|
||||
)
|
||||
PAGO_SCHEMA_LOCATION = (
|
||||
'http://www.sat.gob.mx/Pagos20 '
|
||||
'http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos20.xsd'
|
||||
)
|
||||
|
||||
# RFC for publico en general
|
||||
RFC_PUBLICO_GENERAL = 'XAXX010101000'
|
||||
# RFC for foreign customers
|
||||
RFC_EXTRANJERO = 'XEXX010101000'
|
||||
|
||||
|
||||
def _to_dec(val):
|
||||
if val is None:
|
||||
return Decimal('0')
|
||||
return Decimal(str(val))
|
||||
|
||||
|
||||
TWO = Decimal('0.01')
|
||||
SIX = Decimal('0.000001')
|
||||
|
||||
|
||||
def _format_amount(val):
|
||||
"""Format a Decimal to 2 decimal places as string."""
|
||||
return str(_to_dec(val).quantize(TWO, ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _format_rate(val):
|
||||
"""Format a tax rate to 6 decimal places as string."""
|
||||
return str(_to_dec(val).quantize(SIX, ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _make_element(parent, tag, attribs=None, ns=CFDI_NS):
|
||||
"""Create a subelement with the given namespace."""
|
||||
elem = etree.SubElement(parent, f'{{{ns}}}{tag}')
|
||||
if attribs:
|
||||
for k, v in attribs.items():
|
||||
if v is not None:
|
||||
elem.set(k, str(v))
|
||||
return elem
|
||||
|
||||
|
||||
def build_ingreso_xml(sale, tenant_config, customer=None):
|
||||
"""Build CFDI 4.0 XML for a sale (Comprobante tipo Ingreso).
|
||||
|
||||
Args:
|
||||
sale: dict with keys:
|
||||
id, subtotal, discount_total, tax_total, total, created_at,
|
||||
metodo_pago_sat ('PUE'|'PPD'), forma_pago_sat ('01'|'03'|'04'|'99'),
|
||||
items: [{part_number, name, quantity, unit_price, discount_amount,
|
||||
tax_rate, tax_amount, subtotal,
|
||||
clave_prod_serv, clave_unidad}]
|
||||
tenant_config: dict with keys:
|
||||
rfc, razon_social, regimen_fiscal, cp (codigo postal),
|
||||
serie (optional), nombre_comercial (optional)
|
||||
customer: dict or None with keys:
|
||||
rfc, razon_social, regimen_fiscal, uso_cfdi, cp
|
||||
If None, generates factura a publico general.
|
||||
|
||||
Returns:
|
||||
str: XML string (unsigned, ready for Horux)
|
||||
"""
|
||||
nsmap = {
|
||||
'cfdi': CFDI_NS,
|
||||
'xsi': XSI_NS,
|
||||
}
|
||||
|
||||
# Root: Comprobante
|
||||
root = etree.Element(f'{{{CFDI_NS}}}Comprobante', nsmap=nsmap)
|
||||
root.set(f'{{{XSI_NS}}}schemaLocation', CFDI_SCHEMA_LOCATION)
|
||||
root.set('Version', '4.0')
|
||||
root.set('Serie', tenant_config.get('serie', 'A'))
|
||||
root.set('Folio', str(sale['id']))
|
||||
root.set('Fecha', datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))
|
||||
root.set('FormaPago', sale.get('forma_pago_sat', '01'))
|
||||
root.set('SubTotal', _format_amount(sale['subtotal']))
|
||||
|
||||
discount_total = _to_dec(sale.get('discount_total', 0))
|
||||
if discount_total > 0:
|
||||
root.set('Descuento', _format_amount(discount_total))
|
||||
|
||||
root.set('Moneda', 'MXN')
|
||||
root.set('Total', _format_amount(sale['total']))
|
||||
root.set('TipoDeComprobante', 'I') # Ingreso
|
||||
root.set('Exportacion', '01') # No aplica
|
||||
root.set('MetodoPago', sale.get('metodo_pago_sat', 'PUE'))
|
||||
root.set('LugarExpedicion', tenant_config.get('cp', '00000'))
|
||||
|
||||
# InformacionGlobal (required for publico general)
|
||||
is_publico_general = (customer is None or
|
||||
customer.get('rfc') in (None, '', RFC_PUBLICO_GENERAL))
|
||||
|
||||
if is_publico_general:
|
||||
info_global = _make_element(root, 'InformacionGlobal')
|
||||
info_global.set('Periodicidad', '01') # Diario
|
||||
now = datetime.now()
|
||||
info_global.set('Meses', f'{now.month:02d}')
|
||||
info_global.set('Anio', str(now.year))
|
||||
|
||||
# Emisor
|
||||
emisor = _make_element(root, 'Emisor')
|
||||
emisor.set('Rfc', tenant_config['rfc'])
|
||||
emisor.set('Nombre', tenant_config['razon_social'])
|
||||
emisor.set('RegimenFiscal', tenant_config.get('regimen_fiscal', '601'))
|
||||
|
||||
# Receptor
|
||||
receptor = _make_element(root, 'Receptor')
|
||||
if is_publico_general:
|
||||
receptor.set('Rfc', RFC_PUBLICO_GENERAL)
|
||||
receptor.set('Nombre', 'PUBLICO EN GENERAL')
|
||||
receptor.set('DomicilioFiscalReceptor', tenant_config.get('cp', '00000'))
|
||||
receptor.set('RegimenFiscalReceptor', '616') # Sin obligaciones fiscales
|
||||
receptor.set('UsoCFDI', 'S01') # Sin efectos fiscales
|
||||
else:
|
||||
receptor.set('Rfc', customer['rfc'])
|
||||
receptor.set('Nombre', customer.get('razon_social', customer.get('name', '')))
|
||||
receptor.set('DomicilioFiscalReceptor', customer.get('cp', '00000'))
|
||||
receptor.set('RegimenFiscalReceptor', customer.get('regimen_fiscal', '616'))
|
||||
receptor.set('UsoCFDI', customer.get('uso_cfdi', 'G03'))
|
||||
|
||||
# Conceptos
|
||||
conceptos = _make_element(root, 'Conceptos')
|
||||
|
||||
for item in sale.get('items', []):
|
||||
qty = int(item.get('quantity', 1))
|
||||
unit_price = _to_dec(item.get('unit_price', 0))
|
||||
discount_amount = _to_dec(item.get('discount_amount', 0))
|
||||
tax_rate = _to_dec(item.get('tax_rate', '0.16'))
|
||||
tax_amount = _to_dec(item.get('tax_amount', 0))
|
||||
|
||||
# Importe = qty * unit_price (before discount)
|
||||
importe = (unit_price * qty).quantize(TWO, ROUND_HALF_UP)
|
||||
# Base for tax = importe - discount
|
||||
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
|
||||
|
||||
concepto = _make_element(conceptos, 'Concepto')
|
||||
concepto.set('ClaveProdServ', item.get('clave_prod_serv', '25174800')) # Default: autopartes
|
||||
concepto.set('NoIdentificacion', item.get('part_number', ''))
|
||||
concepto.set('Cantidad', str(qty))
|
||||
concepto.set('ClaveUnidad', item.get('clave_unidad', 'H87')) # H87 = Pieza
|
||||
concepto.set('Unidad', 'PZA')
|
||||
concepto.set('Descripcion', item.get('name', 'Autoparte'))
|
||||
concepto.set('ValorUnitario', _format_amount(unit_price))
|
||||
concepto.set('Importe', _format_amount(importe))
|
||||
concepto.set('ObjetoImp', '02') # Si objeto de impuesto
|
||||
|
||||
if discount_amount > 0:
|
||||
concepto.set('Descuento', _format_amount(discount_amount))
|
||||
|
||||
# Impuestos del concepto
|
||||
impuestos_concepto = _make_element(concepto, 'Impuestos')
|
||||
traslados_concepto = _make_element(impuestos_concepto, 'Traslados')
|
||||
traslado = _make_element(traslados_concepto, 'Traslado')
|
||||
traslado.set('Base', _format_amount(base))
|
||||
traslado.set('Impuesto', '002') # IVA
|
||||
traslado.set('TipoFactor', 'Tasa')
|
||||
traslado.set('TasaOCuota', _format_rate(tax_rate))
|
||||
traslado.set('Importe', _format_amount(tax_amount))
|
||||
|
||||
# Impuestos totales
|
||||
impuestos = _make_element(root, 'Impuestos')
|
||||
impuestos.set('TotalImpuestosTrasladados', _format_amount(sale['tax_total']))
|
||||
traslados = _make_element(impuestos, 'Traslados')
|
||||
traslado_total = _make_element(traslados, 'Traslado')
|
||||
traslado_total.set('Base', _format_amount(sale['subtotal']))
|
||||
traslado_total.set('Impuesto', '002')
|
||||
traslado_total.set('TipoFactor', 'Tasa')
|
||||
traslado_total.set('TasaOCuota', '0.160000')
|
||||
traslado_total.set('Importe', _format_amount(sale['tax_total']))
|
||||
|
||||
return etree.tostring(root, xml_declaration=True, encoding='UTF-8',
|
||||
pretty_print=True).decode('utf-8')
|
||||
|
||||
|
||||
def build_egreso_xml(sale, tenant_config, customer, original_uuid):
|
||||
"""Build unsigned CFDI 4.0 XML for an egreso (credit note) voucher."""
|
||||
raise NotImplementedError("cfdi_builder.build_egreso_xml not yet implemented (Plan 4, Task 2)")
|
||||
"""Build CFDI 4.0 XML for a credit note (Comprobante tipo Egreso).
|
||||
|
||||
Used for cancellations or returns that require a nota de credito.
|
||||
|
||||
Args:
|
||||
sale: same as build_ingreso_xml
|
||||
tenant_config: same as build_ingreso_xml
|
||||
customer: same as build_ingreso_xml
|
||||
original_uuid: str UUID of the original CFDI being credited
|
||||
|
||||
Returns:
|
||||
str: XML string
|
||||
"""
|
||||
nsmap = {
|
||||
'cfdi': CFDI_NS,
|
||||
'xsi': XSI_NS,
|
||||
}
|
||||
|
||||
root = etree.Element(f'{{{CFDI_NS}}}Comprobante', nsmap=nsmap)
|
||||
root.set(f'{{{XSI_NS}}}schemaLocation', CFDI_SCHEMA_LOCATION)
|
||||
root.set('Version', '4.0')
|
||||
root.set('Serie', 'NC')
|
||||
root.set('Folio', str(sale['id']))
|
||||
root.set('Fecha', datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))
|
||||
root.set('FormaPago', sale.get('forma_pago_sat', '01'))
|
||||
root.set('SubTotal', _format_amount(sale['subtotal']))
|
||||
|
||||
discount_total = _to_dec(sale.get('discount_total', 0))
|
||||
if discount_total > 0:
|
||||
root.set('Descuento', _format_amount(discount_total))
|
||||
|
||||
root.set('Moneda', 'MXN')
|
||||
root.set('Total', _format_amount(sale['total']))
|
||||
root.set('TipoDeComprobante', 'E') # Egreso
|
||||
root.set('Exportacion', '01')
|
||||
root.set('MetodoPago', 'PUE')
|
||||
root.set('LugarExpedicion', tenant_config.get('cp', '00000'))
|
||||
|
||||
# CfdiRelacionados - references the original CFDI
|
||||
cfdi_relacionados = _make_element(root, 'CfdiRelacionados')
|
||||
cfdi_relacionados.set('TipoRelacion', '01') # Nota de credito
|
||||
cfdi_relacionado = _make_element(cfdi_relacionados, 'CfdiRelacionado')
|
||||
cfdi_relacionado.set('UUID', original_uuid)
|
||||
|
||||
# Emisor
|
||||
emisor = _make_element(root, 'Emisor')
|
||||
emisor.set('Rfc', tenant_config['rfc'])
|
||||
emisor.set('Nombre', tenant_config['razon_social'])
|
||||
emisor.set('RegimenFiscal', tenant_config.get('regimen_fiscal', '601'))
|
||||
|
||||
# Receptor
|
||||
is_publico_general = (customer is None or
|
||||
customer.get('rfc') in (None, '', RFC_PUBLICO_GENERAL))
|
||||
receptor = _make_element(root, 'Receptor')
|
||||
if is_publico_general:
|
||||
receptor.set('Rfc', RFC_PUBLICO_GENERAL)
|
||||
receptor.set('Nombre', 'PUBLICO EN GENERAL')
|
||||
receptor.set('DomicilioFiscalReceptor', tenant_config.get('cp', '00000'))
|
||||
receptor.set('RegimenFiscalReceptor', '616')
|
||||
receptor.set('UsoCFDI', 'S01')
|
||||
else:
|
||||
receptor.set('Rfc', customer['rfc'])
|
||||
receptor.set('Nombre', customer.get('razon_social', customer.get('name', '')))
|
||||
receptor.set('DomicilioFiscalReceptor', customer.get('cp', '00000'))
|
||||
receptor.set('RegimenFiscalReceptor', customer.get('regimen_fiscal', '616'))
|
||||
receptor.set('UsoCFDI', customer.get('uso_cfdi', 'G03'))
|
||||
|
||||
# Conceptos (same as ingreso but for the credited amounts)
|
||||
conceptos = _make_element(root, 'Conceptos')
|
||||
|
||||
for item in sale.get('items', []):
|
||||
qty = int(item.get('quantity', 1))
|
||||
unit_price = _to_dec(item.get('unit_price', 0))
|
||||
discount_amount = _to_dec(item.get('discount_amount', 0))
|
||||
tax_rate = _to_dec(item.get('tax_rate', '0.16'))
|
||||
tax_amount = _to_dec(item.get('tax_amount', 0))
|
||||
importe = (unit_price * qty).quantize(TWO, ROUND_HALF_UP)
|
||||
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
|
||||
|
||||
concepto = _make_element(conceptos, 'Concepto')
|
||||
concepto.set('ClaveProdServ', item.get('clave_prod_serv', '25174800'))
|
||||
concepto.set('NoIdentificacion', item.get('part_number', ''))
|
||||
concepto.set('Cantidad', str(qty))
|
||||
concepto.set('ClaveUnidad', item.get('clave_unidad', 'H87'))
|
||||
concepto.set('Unidad', 'PZA')
|
||||
concepto.set('Descripcion', item.get('name', 'Autoparte'))
|
||||
concepto.set('ValorUnitario', _format_amount(unit_price))
|
||||
concepto.set('Importe', _format_amount(importe))
|
||||
concepto.set('ObjetoImp', '02')
|
||||
|
||||
if discount_amount > 0:
|
||||
concepto.set('Descuento', _format_amount(discount_amount))
|
||||
|
||||
impuestos_concepto = _make_element(concepto, 'Impuestos')
|
||||
traslados_concepto = _make_element(impuestos_concepto, 'Traslados')
|
||||
traslado = _make_element(traslados_concepto, 'Traslado')
|
||||
traslado.set('Base', _format_amount(base))
|
||||
traslado.set('Impuesto', '002')
|
||||
traslado.set('TipoFactor', 'Tasa')
|
||||
traslado.set('TasaOCuota', _format_rate(tax_rate))
|
||||
traslado.set('Importe', _format_amount(tax_amount))
|
||||
|
||||
# Impuestos totales
|
||||
impuestos = _make_element(root, 'Impuestos')
|
||||
impuestos.set('TotalImpuestosTrasladados', _format_amount(sale['tax_total']))
|
||||
traslados = _make_element(impuestos, 'Traslados')
|
||||
traslado_total = _make_element(traslados, 'Traslado')
|
||||
traslado_total.set('Base', _format_amount(sale['subtotal']))
|
||||
traslado_total.set('Impuesto', '002')
|
||||
traslado_total.set('TipoFactor', 'Tasa')
|
||||
traslado_total.set('TasaOCuota', '0.160000')
|
||||
traslado_total.set('Importe', _format_amount(sale['tax_total']))
|
||||
|
||||
return etree.tostring(root, xml_declaration=True, encoding='UTF-8',
|
||||
pretty_print=True).decode('utf-8')
|
||||
|
||||
|
||||
def build_pago_xml(sale, tenant_config, customer, payments):
|
||||
"""Build unsigned CFDI 4.0 XML for a pago (payment) complement."""
|
||||
raise NotImplementedError("cfdi_builder.build_pago_xml not yet implemented (Plan 4, Task 2)")
|
||||
def build_pago_xml(payment, tenant_config, customer, original_uuid):
|
||||
"""Build CFDI 4.0 XML with Complemento de Pago 2.0.
|
||||
|
||||
Used for credit sale payments (MetodoPago PPD). When a customer
|
||||
pays an outstanding credit sale, this generates the payment complement.
|
||||
|
||||
Args:
|
||||
payment: dict with keys:
|
||||
id, amount, payment_method ('efectivo'|'transferencia'|'tarjeta'),
|
||||
date (ISO string), reference (optional)
|
||||
tenant_config: same as build_ingreso_xml
|
||||
customer: dict with RFC data
|
||||
original_uuid: str UUID of the original CFDI (Ingreso with PPD)
|
||||
|
||||
Returns:
|
||||
str: XML string
|
||||
"""
|
||||
nsmap = {
|
||||
'cfdi': CFDI_NS,
|
||||
'pago20': PAGO_NS,
|
||||
'xsi': XSI_NS,
|
||||
}
|
||||
|
||||
forma_pago_map = {
|
||||
'efectivo': '01', 'transferencia': '03', 'tarjeta': '04', 'mixto': '99'
|
||||
}
|
||||
|
||||
root = etree.Element(f'{{{CFDI_NS}}}Comprobante', nsmap=nsmap)
|
||||
root.set(f'{{{XSI_NS}}}schemaLocation',
|
||||
f'{CFDI_SCHEMA_LOCATION} {PAGO_SCHEMA_LOCATION}')
|
||||
root.set('Version', '4.0')
|
||||
root.set('Serie', 'P')
|
||||
root.set('Folio', str(payment.get('id', '')))
|
||||
root.set('Fecha', datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))
|
||||
root.set('SubTotal', '0')
|
||||
root.set('Moneda', 'XXX') # Required for Pago type
|
||||
root.set('Total', '0')
|
||||
root.set('TipoDeComprobante', 'P') # Pago
|
||||
root.set('Exportacion', '01')
|
||||
root.set('LugarExpedicion', tenant_config.get('cp', '00000'))
|
||||
|
||||
# Emisor
|
||||
emisor = _make_element(root, 'Emisor')
|
||||
emisor.set('Rfc', tenant_config['rfc'])
|
||||
emisor.set('Nombre', tenant_config['razon_social'])
|
||||
emisor.set('RegimenFiscal', tenant_config.get('regimen_fiscal', '601'))
|
||||
|
||||
# Receptor
|
||||
receptor = _make_element(root, 'Receptor')
|
||||
receptor.set('Rfc', customer['rfc'])
|
||||
receptor.set('Nombre', customer.get('razon_social', customer.get('name', '')))
|
||||
receptor.set('DomicilioFiscalReceptor', customer.get('cp', '00000'))
|
||||
receptor.set('RegimenFiscalReceptor', customer.get('regimen_fiscal', '616'))
|
||||
receptor.set('UsoCFDI', 'CP01') # Pagos
|
||||
|
||||
# Conceptos (mandatory placeholder for Pago type)
|
||||
conceptos = _make_element(root, 'Conceptos')
|
||||
concepto = _make_element(conceptos, 'Concepto')
|
||||
concepto.set('ClaveProdServ', '84111506') # Servicios de facturacion
|
||||
concepto.set('Cantidad', '1')
|
||||
concepto.set('ClaveUnidad', 'ACT') # Actividad
|
||||
concepto.set('Descripcion', 'Pago')
|
||||
concepto.set('ValorUnitario', '0')
|
||||
concepto.set('Importe', '0')
|
||||
concepto.set('ObjetoImp', '01') # No objeto de impuesto
|
||||
|
||||
# Complemento de Pago 2.0
|
||||
complemento = _make_element(root, 'Complemento')
|
||||
pagos_elem = etree.SubElement(complemento, f'{{{PAGO_NS}}}Pagos')
|
||||
pagos_elem.set('Version', '2.0')
|
||||
|
||||
# Totales
|
||||
totales = etree.SubElement(pagos_elem, f'{{{PAGO_NS}}}Totales')
|
||||
amount = _to_dec(payment['amount'])
|
||||
# Calculate base and IVA from total (total includes 16% IVA)
|
||||
base_pago = (amount / Decimal('1.16')).quantize(TWO, ROUND_HALF_UP)
|
||||
iva_pago = (amount - base_pago).quantize(TWO, ROUND_HALF_UP)
|
||||
totales.set('TotalTrasladosBaseIVA16', _format_amount(base_pago))
|
||||
totales.set('TotalTrasladosImpuestoIVA16', _format_amount(iva_pago))
|
||||
totales.set('MontoTotalPagos', _format_amount(amount))
|
||||
|
||||
# Pago
|
||||
pago = etree.SubElement(pagos_elem, f'{{{PAGO_NS}}}Pago')
|
||||
payment_date = payment.get('date', datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))
|
||||
pago.set('FechaPago', payment_date if 'T' in str(payment_date)
|
||||
else f'{payment_date}T12:00:00')
|
||||
pago.set('FormaDePagoP', forma_pago_map.get(payment.get('payment_method', 'efectivo'), '01'))
|
||||
pago.set('MonedaP', 'MXN')
|
||||
pago.set('Monto', _format_amount(amount))
|
||||
|
||||
# DoctoRelacionado (the original invoice being paid)
|
||||
docto = etree.SubElement(pago, f'{{{PAGO_NS}}}DoctoRelacionado')
|
||||
docto.set('IdDocumento', original_uuid)
|
||||
docto.set('Serie', 'A')
|
||||
docto.set('Folio', str(payment.get('sale_id', '')))
|
||||
docto.set('MonedaDR', 'MXN')
|
||||
docto.set('NumParcialidad', str(payment.get('num_parcialidad', 1)))
|
||||
docto.set('ImpSaldoAnt', _format_amount(payment.get('saldo_anterior', amount)))
|
||||
docto.set('ImpPagado', _format_amount(amount))
|
||||
saldo_insoluto = _to_dec(payment.get('saldo_anterior', amount)) - amount
|
||||
docto.set('ImpSaldoInsoluto', _format_amount(max(saldo_insoluto, Decimal('0'))))
|
||||
docto.set('ObjetoImpDR', '02')
|
||||
docto.set('EquivalenciaDR', '1')
|
||||
|
||||
# ImpuestosDR
|
||||
impuestos_dr = etree.SubElement(docto, f'{{{PAGO_NS}}}ImpuestosDR')
|
||||
traslados_dr = etree.SubElement(impuestos_dr, f'{{{PAGO_NS}}}TrasladosDR')
|
||||
traslado_dr = etree.SubElement(traslados_dr, f'{{{PAGO_NS}}}TrasladoDR')
|
||||
traslado_dr.set('BaseDR', _format_amount(base_pago))
|
||||
traslado_dr.set('ImpuestoDR', '002')
|
||||
traslado_dr.set('TipoFactorDR', 'Tasa')
|
||||
traslado_dr.set('TasaOCuotaDR', '0.160000')
|
||||
traslado_dr.set('ImporteDR', _format_amount(iva_pago))
|
||||
|
||||
# ImpuestosP (pago-level taxes)
|
||||
impuestos_p = etree.SubElement(pago, f'{{{PAGO_NS}}}ImpuestosP')
|
||||
traslados_p = etree.SubElement(impuestos_p, f'{{{PAGO_NS}}}TrasladosP')
|
||||
traslado_p = etree.SubElement(traslados_p, f'{{{PAGO_NS}}}TrasladoP')
|
||||
traslado_p.set('BaseP', _format_amount(base_pago))
|
||||
traslado_p.set('ImpuestoP', '002')
|
||||
traslado_p.set('TipoFactorP', 'Tasa')
|
||||
traslado_p.set('TasaOCuotaP', '0.160000')
|
||||
traslado_p.set('ImporteP', _format_amount(iva_pago))
|
||||
|
||||
return etree.tostring(root, xml_declaration=True, encoding='UTF-8',
|
||||
pretty_print=True).decode('utf-8')
|
||||
|
||||
Reference in New Issue
Block a user