cfdi-trimbra/source/api/util/cfdi_xml.py

272 lines
9.9 KiB
Python

#!/usr/bin/env python3
from datetime import datetime
import lxml.etree as ET
class CFDI(object):
_version = '3.3'
_prefix = 'cfdi'
_xmlns = 'http://www.sat.gob.mx/cfd/3'
_schema = f'{_xmlns} http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd'
_pagos = 'http://www.sat.gob.mx/Pagos'
NS = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
PAGOS = {
'version': '1.0',
'prefix': _pagos,
'ns': {'pago10': _pagos},
'schema': f' {_pagos} http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd',
}
_nomina = 'http://www.sat.gob.mx/nomina12'
NOMINA = {
'version': '1.2',
'prefix': _nomina,
'ns': {'nomina12': _nomina},
'schema': f' {_nomina} http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd',
}
_tax_locales = 'http://www.sat.gob.mx/implocal'
TAX_LOCALES = {
'version': '1.0',
'prefix': _tax_locales,
'ns': {'implocal': _tax_locales},
'schema': f' {_tax_locales} http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd',
}
def __init__(self):
self.error = ''
self._root = None
@property
def xml(self):
cfdi = ET.tostring(self._root,
pretty_print=True, xml_declaration=True, encoding='utf-8')
return cfdi.decode()
def make_xml(self, data):
self._validate_data(data)
self._comprobante(data['comprobante'])
self._relacionados(data.get('relacionados', {}))
self._emisor(data['emisor'])
self._receptor(data['receptor'])
self._conceptos(data['conceptos'])
self._impuestos(data.get('impuestos', {}))
self._complementos(data.get('complementos', {}))
return
def stamp(self, cert, path_xslt):
xslt = open(path_xslt, 'rb')
transfor = ET.XSLT(ET.parse(xslt))
self._root.attrib['NoCertificado'] = cert.serial_number
self._root.attrib['Certificado'] = cert.cer_txt
cadena = str(transfor(self._root)).encode()
self._root.attrib['Sello'] = cert.sign(cadena)
xslt.close()
return
def _validate_data(self, data):
self._node_complement = False
self._exists_pagos = False
self._exists_nomina = False
self._exists_tax_locales = False
if not 'complementos' in data:
return
complements = data['complementos']
self._exists_pagos = 'pagos' in complements
self._exists_nomina = 'nomina' in complements
self._exists_tax_locales = 'impuestos_locales' in complements
if self._exists_pagos:
self._node_complement = True
self._schema += self.PAGOS['schema']
if self._exists_nomina:
self._node_complement = True
self._schema += self.NOMINA['schema']
if self._exists_tax_locales:
self._node_complement = True
self._schema += self.TAX_LOCALES['schema']
return
def _comprobante(self, attr):
NSMAP = {
self._prefix: self._xmlns,
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
}
if self._exists_pagos:
NSMAP.update(self.PAGOS['ns'])
if self._exists_nomina:
NSMAP.update(self.NOMINA['ns'])
if self._exists_tax_locales:
NSMAP.update(self.TAX_LOCALES['ns'])
attr_qname = ET.QName(
'http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')
schema = {attr_qname: self._schema}
if not 'Version' in attr:
attr['Version'] = self._version
if not 'Fecha' in attr:
attr['Fecha'] = datetime.now().isoformat()[:19]
node_name = f'{{{self._xmlns}}}Comprobante'
self._root = ET.Element(node_name, schema, **attr, nsmap=NSMAP)
return
def _relacionados(self, attr):
return
def _emisor(self, attr):
node_name = f'{{{self._xmlns}}}Emisor'
emisor = ET.SubElement(self._root, node_name, attr)
return
def _receptor(self, attr):
node_name = f'{{{self._xmlns}}}Receptor'
emisor = ET.SubElement(self._root, node_name, attr)
return
def _conceptos(self, data):
node_name = f'{{{self._xmlns}}}Conceptos'
conceptos = ET.SubElement(self._root, node_name)
for row in data:
complemento = row.pop('complemento', {})
taxes = row.pop('impuestos', {})
node_name = f'{{{self._xmlns}}}Concepto'
concepto = ET.SubElement(conceptos, node_name, row)
if not taxes:
continue
if taxes['traslados'] or taxes['retenciones']:
node_name = f'{{{self._xmlns}}}Impuestos'
impuestos = ET.SubElement(concepto, node_name)
if 'traslados' in taxes and taxes['traslados']:
node_name = f'{{{self._xmlns}}}Traslados'
traslados = ET.SubElement(impuestos, node_name)
node_name = f'{{{self._xmlns}}}Traslado'
for traslado in taxes['traslados']:
ET.SubElement(traslados, node_name, traslado)
if 'retenciones' in taxes and taxes['retenciones']:
node_name = f'{{{self._xmlns}}}Retenciones'
retenciones = ET.SubElement(impuestos, node_name)
node_name = f'{{{self._xmlns}}}Retencion'
for retencion in taxes['retenciones']:
ET.SubElement(retenciones, node_name, retencion)
return
def _impuestos(self, data):
if not data:
return
node_name = f'{{{self._xmlns}}}Impuestos'
retenciones = data.pop('retenciones', ())
traslados = data.pop('traslados', ())
taxes = ET.SubElement(self._root, node_name, data)
if retenciones:
node_name = f'{{{self._xmlns}}}Retenciones'
subnode = ET.SubElement(taxes, node_name)
node_name = f'{{{self._xmlns}}}Retencion'
for row in retenciones:
ET.SubElement(subnode, node_name, row)
if traslados:
node_name = f'{{{self._xmlns}}}Traslados'
subnode = ET.SubElement(taxes, node_name)
node_name = f'{{{self._xmlns}}}Traslado'
for row in traslados:
ET.SubElement(subnode, node_name, row)
return
def _complementos(self, data):
if not self._node_complement:
return
node_name = f'{{{self._xmlns}}}Complemento'
complemento = ET.SubElement(self._root, node_name)
if self._exists_pagos:
self._pagos(complemento, data['pagos'])
if self._exists_nomina:
self._nomina(complemento, data['nomina'])
if self._exists_tax_locales:
self._tax_locales(complemento, data['impuestos_locales'])
return
def _pagos(self, complemento, data):
node_name = f"{{{self.PAGOS['prefix']}}}Pagos"
attr = {'Version': self.PAGOS['version']}
node_pagos = ET.SubElement(complemento, node_name, attr)
for pago in data:
documentos = pago.pop('documentos')
node_name = f"{{{self.PAGOS['prefix']}}}Pago"
node_pago = ET.SubElement(node_pagos, node_name, pago)
node_name = f"{{{self.PAGOS['prefix']}}}DoctoRelacionado"
for doc in documentos:
ET.SubElement(node_pago, node_name, doc)
return
def _nomina(self, complemento, data):
emisor = data.pop('emisor')
receptor = data.pop('receptor')
percepciones = data.pop('percepciones', {})
deducciones = data.pop('deducciones', {})
otros = data.pop('otros', ())
if not 'Version' in data:
data['Version'] = self.NOMINA['version']
node_name = f"{{{self.NOMINA['prefix']}}}Nomina"
node_nomina = ET.SubElement(complemento, node_name, data)
node_name = f"{{{self.NOMINA['prefix']}}}Emisor"
ET.SubElement(node_nomina, node_name, emisor)
node_name = f"{{{self.NOMINA['prefix']}}}Receptor"
ET.SubElement(node_nomina, node_name, receptor)
if percepciones:
attr = percepciones
percepciones = attr.pop('percepciones')
node_name = f"{{{self.NOMINA['prefix']}}}Percepciones"
node = ET.SubElement(node_nomina, node_name, attr)
node_name = f"{{{self.NOMINA['prefix']}}}Percepcion"
for percepcion in percepciones:
ET.SubElement(node, node_name, percepcion)
if deducciones:
attr = deducciones
deducciones = attr.pop('deducciones')
node_name = f"{{{self.NOMINA['prefix']}}}Deducciones"
node = ET.SubElement(node_nomina, node_name, attr)
node_name = f"{{{self.NOMINA['prefix']}}}Deduccion"
for deduccion in deducciones:
ET.SubElement(node, node_name, deduccion)
if otros:
node_name = f"{{{self.NOMINA['prefix']}}}OtrosPagos"
node = ET.SubElement(node_nomina, node_name)
node_name = f"{{{self.NOMINA['prefix']}}}OtroPago"
for otro in otros:
subsidio = otro.pop('subsidio', {})
sub_node = ET.SubElement(node, node_name, otro)
if subsidio:
sub_name = f"{{{self.NOMINA['prefix']}}}SubsidioAlEmpleo"
ET.SubElement(sub_node, sub_name, subsidio)
return
def _tax_locales(self, complemento, data):
traslados = data.pop('traslados', ())
retenciones = data.pop('retenciones', ())
node_name = f"{{{self.TAX_LOCALES['prefix']}}}ImpuestosLocales"
attr = {'version': self.TAX_LOCALES['version']}
attr.update(data)
node_tax = ET.SubElement(complemento, node_name, attr)
for traslado in traslados:
node_name = f"{{{self.TAX_LOCALES['prefix']}}}TrasladosLocales"
ET.SubElement(node_tax, node_name, traslado)
return