cfdi-admin/source/main/util/cfdi_to_dict.py

202 lines
6.4 KiB
Python

#!/usr/bin/env python3
import datetime
import sys
import lxml.etree as ET
from decimal import Decimal, getcontext
getcontext().prec = 6
TEMPLATE_DATE = '%Y-%m-%dT%H:%M:%S'
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tfd': 'http://www.sat.gob.mx/TimbreFiscalDigital',
'nomina12': 'http://www.sat.gob.mx/nomina12',
}
PRE = '/cfdi:Comprobante'
CADENA = "||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||"
class CfdiToDic(object):
def __init__(self):
self._data = {}
self.complements = True
def parse(self, source):
# ~ recovering_parser = ET.XMLParser(recover=True)
# ~ tree = ET.fromstring(xml, parser=recovering_parser)
self._tree = ET.fromstring(source)
self._data['comprobante'] = dict(self._tree.attrib)
self._data['comprobante']['Total'] = \
self._str_to_decimal(self._data['comprobante']['Total'])
self._data['comprobante']['Fecha'] = \
self._str_to_date(self._data['comprobante']['Fecha'])
attr = self._get_attr(f'{PRE}/cfdi:Complemento/tfd:TimbreFiscalDigital')
self._data['timbre'] = attr
attr = self._get_attr(f'{PRE}/cfdi:Emisor')
self._data['emisor'] = attr
attr = self._get_attr(f'{PRE}/cfdi:Receptor')
self._data['receptor'] = attr
self._parse_details()
self._parse_taxes()
self._parse_nomina()
self._complements()
return
def _parse_details(self):
name = f'{PRE}/cfdi:Conceptos/cfdi:Concepto'
details = self._tree.xpath(name, namespaces=NS_CFDI)
rows = []
for detail in details:
row = dict(detail.attrib)
for k, v in row.items():
if k in ('Cantidad', 'ValorUnitario', 'Descuento', 'Importe'):
row[k] = self._str_to_decimal(v)
row['taxes'] = self._get_taxes(detail)
rows.append(row)
self._data['conceptos'] = rows
return
def _get_taxes(self, node):
data = {'traslados': [], 'retenciones': []}
for n in node.iter():
row = {}
if n.tag.endswith('Traslado') or n.tag.endswith('Retencion'):
row = dict(n.attrib)
if row:
row['Base'] = self._str_to_decimal(row['Base'])
row['TasaOCuota'] = self._str_to_decimal(row['TasaOCuota'])
row['Importe'] = self._str_to_decimal(row['Importe'])
if n.tag.endswith('Traslado'):
data['traslados'].append(row)
else:
data['retenciones'].append(row)
return data
def _parse_taxes(self):
attr = self._get_attr(f'{PRE}/cfdi:Impuestos')
self._data['impuestos'] = attr
fields = ('TotalImpuestosTrasladados', 'TotalImpuestosRetenidos')
k = 'impuestos'
for f in fields:
if f in self._data[k]:
self._data[k][f] = self._str_to_decimal(self._data[k][f])
name = f'{PRE}/cfdi:Impuestos/cfdi:Traslados/cfdi:Traslado'
taxes = self._tree.xpath(name, namespaces=NS_CFDI)
if taxes:
data = []
for t in taxes:
values = dict(t.attrib)
values['TasaOCuota'] = self._str_to_decimal(values['TasaOCuota'])
values['Importe'] = self._str_to_decimal(values['Importe'])
data.append(values)
self._data['impuestos']['traslados'] = data
name = f'{PRE}/cfdi:Impuestos/cfdi:Retenciones/cfdi:Retencion'
taxes = self._tree.xpath(name, namespaces=NS_CFDI)
if taxes:
data = []
for t in taxes:
values = dict(t.attrib)
values['Importe'] = self._str_to_decimal(values['Importe'])
data.append(values)
self._data['impuestos']['retenciones'] = data
return
def _parse_nomina(self):
node_name = '//cfdi:Complemento/nomina12:Nomina'
node = self._tree.xpath(node_name, namespaces=NS_CFDI)
if not node:
return
node = node[0]
attr = dict(node.attrib)
# ~ self._data['version_nomina'] = attr['version']
self._data['nomina'] = attr
node_name = '//nomina12:Emisor'
attr = self._get_attr(node_name, node)
self._data['emisor'].update(attr)
node_name = '//nomina12:Receptor'
attr = self._get_attr(node_name, node)
self._data['receptor'].update(attr)
node_name = '//nomina12:Percepciones'
self._data['percepciones'] = self._get_attr(node_name, node)
node_name = '//nomina12:Percepcion'
percepciones = node.xpath(node_name, namespaces=NS_CFDI)
self._data['percepciones']['percepciones'] = [
dict(n.attrib) for n in percepciones]
node_name = '//nomina12:Deducciones'
self._data['deducciones'] = self._get_attr(node_name, node)
node_name = '//nomina12:Deduccion'
deducciones = node.xpath(node_name, namespaces=NS_CFDI)
self._data['deducciones']['deducciones'] = [
dict(n.attrib) for n in deducciones]
return
def _complements(self):
if not self.complements:
self._data['comprobante']['xml'] = self.xml
return
self._data['comprobante']['Certificado'] = ''
self._data['comprobante']['cadenaoriginal'] = \
CADENA.format(**self._data['timbre'])
return
def _get_attr(self, node_name, node=None):
if node is None:
new_node = self._tree.xpath(node_name, namespaces=NS_CFDI)[0]
else:
new_node = node.xpath(node_name, namespaces=NS_CFDI)[0]
attr = dict(new_node.attrib)
return attr
def _str_to_date(self, str_date, template=TEMPLATE_DATE):
return datetime.datetime.strptime(str_date, template)
def _str_to_decimal(self, value):
d = Decimal(value)
return d
def __str__(self):
return str(self._data)
@property
def xml(self):
data = ET.tostring(self._tree,
pretty_print=True,
xml_declaration=True,
encoding='utf-8').decode()
return data
@property
def data(self):
return self._data
def main(path):
with open(path, 'rb') as f:
data = f.read()
cfdi = CfdiToDic(data)
print(cfdi)
return
if __name__ == '__main__':
main(sys.argv[1])