272 lines
9.9 KiB
Python
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
|