2022-08-09 22:43:56 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import lxml.etree as ET
|
|
|
|
|
|
|
|
from settings import DEBUG, log, PATH_XSLT, DELETE_FILES, PAC_AUTH
|
|
|
|
from helper.comercio import PACComercioDigital as PAC
|
|
|
|
|
|
|
|
|
|
|
|
def _call(args):
|
|
|
|
return subprocess.check_output(args, shell=True).decode()
|
|
|
|
|
|
|
|
|
|
|
|
def join(*paths):
|
|
|
|
return os.path.join(*paths)
|
|
|
|
|
|
|
|
|
|
|
|
def kill(path):
|
|
|
|
try:
|
|
|
|
os.remove(path)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
class Cert():
|
|
|
|
OPENSSL = 'openssl'
|
|
|
|
|
|
|
|
def __init__ (self, path, name):
|
|
|
|
self._get_data(path, name)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def serial_number(self):
|
|
|
|
return self._serial_number
|
|
|
|
|
|
|
|
@property
|
|
|
|
def txt(self):
|
|
|
|
return self._cert_txt
|
|
|
|
|
|
|
|
def _get_data(self, path, name):
|
|
|
|
path_cer = join(path, f'{name}.cer')
|
|
|
|
self._path_key = join(path, f'{name}.pem')
|
|
|
|
self._serial_number = self._get_serial_number(path_cer)
|
|
|
|
self._cert_txt = self._get_cert_txt(path_cer)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _get_serial_number(self, path_cer):
|
|
|
|
args = f'"{self.OPENSSL}" x509 -inform DER -in "{path_cer}" -noout -serial'
|
|
|
|
serial_number = _call(args)
|
|
|
|
serial_number = serial_number.split('=')[1].split('\n')[0][1::2]
|
|
|
|
return serial_number
|
|
|
|
|
|
|
|
def _get_cert_txt(self, path_cer):
|
|
|
|
args = f'"{self.OPENSSL}" enc -base64 -in "{path_cer}"'
|
|
|
|
data = _call(args).replace('\n', '')
|
|
|
|
return data
|
|
|
|
|
|
|
|
def sign(self, data):
|
2023-04-12 21:32:13 -06:00
|
|
|
args = f"echo -n -e '{data.decode()}' | '{self.OPENSSL}' dgst -sha256 -sign '{self._path_key}' | '{self.OPENSSL}' enc -base64"
|
2022-08-09 22:43:56 -05:00
|
|
|
data = _call(args).replace('\n', '')
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
class DictToCfdi():
|
|
|
|
_PREFIX = 'cfdi'
|
|
|
|
_XMLNS = 'http://www.sat.gob.mx/cfd/4'
|
|
|
|
_SCHEMA = f'{_XMLNS} http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd'
|
|
|
|
_LEYENDAS = {
|
|
|
|
'version': '1.0',
|
|
|
|
'prefix': 'leyendasFisc',
|
|
|
|
'xmlns': 'http://www.sat.gob.mx/leyendasFiscales',
|
|
|
|
'schema': ' http://www.sat.gob.mx/leyendasFiscales http://www.sat.gob.mx/sitio_internet/cfd/leyendasFiscales/leyendasFisc.xsd',
|
|
|
|
}
|
2022-11-05 22:48:54 -06:00
|
|
|
_PAGOS = {
|
|
|
|
'version': '2.0',
|
2022-11-08 19:47:44 -06:00
|
|
|
'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',
|
2022-11-05 22:48:54 -06:00
|
|
|
}
|
|
|
|
_COMERCIO = {
|
|
|
|
'version': '1.1',
|
|
|
|
'prefix': 'cce11',
|
|
|
|
'xmlns': 'http://www.sat.gob.mx/ComercioExterior11',
|
|
|
|
'schema': ' http://www.sat.gob.mx/ComercioExterior11 http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd',
|
|
|
|
}
|
2022-08-09 22:43:56 -05:00
|
|
|
|
|
|
|
def __init__ (self, data):
|
|
|
|
self._data = data
|
|
|
|
self._cfdi = None
|
|
|
|
self._root = None
|
|
|
|
self._attr_complementos = {}
|
2022-08-10 20:41:16 -05:00
|
|
|
self._node_addenda = None
|
2022-08-09 22:43:56 -05:00
|
|
|
self._make_cfdi()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def cfdi(self):
|
|
|
|
return self._cfdi
|
|
|
|
|
|
|
|
def _make_cfdi(self):
|
|
|
|
self._validate_data()
|
|
|
|
self._comprobante()
|
|
|
|
self._relacionados()
|
2023-02-10 11:54:38 -06:00
|
|
|
self._global()
|
2022-08-09 22:43:56 -05:00
|
|
|
self._emisor()
|
|
|
|
self._receptor()
|
|
|
|
self._conceptos()
|
|
|
|
self._impuestos()
|
|
|
|
self._complementos()
|
2022-08-10 20:41:16 -05:00
|
|
|
self._addenda()
|
2022-08-09 22:43:56 -05:00
|
|
|
|
|
|
|
xml = ET.tostring(self._root,
|
|
|
|
pretty_print=True, xml_declaration=True, encoding='utf-8')
|
|
|
|
self._cfdi = xml.decode()
|
|
|
|
return
|
|
|
|
|
|
|
|
def _validate_data(self):
|
|
|
|
self._schema = self._SCHEMA
|
|
|
|
|
|
|
|
if 'leyendas' in self._data['complementos']:
|
|
|
|
self._schema += self._LEYENDAS['schema']
|
|
|
|
self._attr_complementos['leyendas'] = {
|
|
|
|
self._LEYENDAS['prefix']: self._LEYENDAS['xmlns']
|
|
|
|
}
|
2022-11-08 19:47:44 -06:00
|
|
|
if 'pagos' in self._data['complementos']:
|
|
|
|
self._schema += self._PAGOS['schema']
|
|
|
|
self._attr_complementos['pagos'] = {
|
|
|
|
self._PAGOS['prefix']: self._PAGOS['xmlns']
|
|
|
|
}
|
2022-11-05 22:48:54 -06:00
|
|
|
if 'comercio' in self._data['complementos']:
|
|
|
|
self._schema += self._COMERCIO['schema']
|
|
|
|
self._attr_complementos['comercio'] = {
|
|
|
|
self._COMERCIO['prefix']: self._COMERCIO['xmlns']
|
|
|
|
}
|
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
def _comprobante(self):
|
|
|
|
attr = self._data['comprobante']
|
|
|
|
NSMAP = {
|
|
|
|
self._PREFIX: self._XMLNS,
|
|
|
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
|
|
}
|
|
|
|
for k, value in self._attr_complementos.items():
|
|
|
|
NSMAP.update(value)
|
|
|
|
|
|
|
|
attr_qname = ET.QName(
|
|
|
|
'http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')
|
|
|
|
schema = {attr_qname: self._schema}
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}Comprobante'
|
|
|
|
self._root = ET.Element(node_name, schema, **attr, nsmap=NSMAP)
|
|
|
|
return
|
|
|
|
|
2023-02-10 11:54:38 -06:00
|
|
|
def _global(self):
|
|
|
|
data = self._data.get('global', {})
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}InformacionGlobal'
|
|
|
|
node = ET.SubElement(self._root, node_name, data)
|
|
|
|
return
|
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
def _relacionados(self):
|
|
|
|
data = self._data['relacionados']
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}CfdiRelacionados'
|
|
|
|
attr = {'TipoRelacion': data['TipoRelacion']}
|
|
|
|
node = ET.SubElement(self._root, node_name, attr)
|
|
|
|
for uuid in data['UUID']:
|
|
|
|
node_name = f'{{{self._XMLNS}}}CfdiRelacionado'
|
|
|
|
attr = {'UUID': uuid}
|
|
|
|
ET.SubElement(node, node_name, attr)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _emisor(self):
|
|
|
|
attr = self._data['emisor']
|
|
|
|
node_name = f'{{{self._XMLNS}}}Emisor'
|
|
|
|
emisor = ET.SubElement(self._root, node_name, attr)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _receptor(self):
|
|
|
|
attr = self._data['receptor']
|
|
|
|
node_name = f'{{{self._XMLNS}}}Receptor'
|
|
|
|
emisor = ET.SubElement(self._root, node_name, attr)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _conceptos(self):
|
|
|
|
products = self._data['conceptos']
|
|
|
|
node_name = f'{{{self._XMLNS}}}Conceptos'
|
|
|
|
node = ET.SubElement(self._root, node_name)
|
|
|
|
for product in products:
|
|
|
|
complemento = product.pop('complemento', {})
|
|
|
|
taxes = product.pop('impuestos', {})
|
|
|
|
node_name = f'{{{self._XMLNS}}}Concepto'
|
|
|
|
node_product = ET.SubElement(node, node_name, product)
|
|
|
|
if not taxes:
|
|
|
|
continue
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}Impuestos'
|
|
|
|
node_taxes = ET.SubElement(node_product, node_name)
|
|
|
|
traslados = taxes.get('traslados', [])
|
|
|
|
retenciones = taxes.get('retenciones', [])
|
|
|
|
|
|
|
|
if traslados:
|
|
|
|
node_name = f'{{{self._XMLNS}}}Traslados'
|
|
|
|
node_tmp = ET.SubElement(node_taxes, node_name)
|
|
|
|
for tax in traslados:
|
|
|
|
node_name = f'{{{self._XMLNS}}}Traslado'
|
|
|
|
ET.SubElement(node_tmp, node_name, tax)
|
|
|
|
|
|
|
|
if retenciones:
|
|
|
|
node_name = f'{{{self._XMLNS}}}Retenciones'
|
|
|
|
node_tmp = ET.SubElement(node_taxes, node_name)
|
|
|
|
for tax in retenciones:
|
|
|
|
node_name = f'{{{self._XMLNS}}}Retencion'
|
|
|
|
ET.SubElement(node_tmp, node_name, tax)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _impuestos(self):
|
|
|
|
taxes = self._data['impuestos']
|
|
|
|
if not taxes:
|
|
|
|
return
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}Impuestos'
|
|
|
|
retenciones = taxes.pop('retenciones', ())
|
|
|
|
traslados = taxes.pop('traslados', ())
|
|
|
|
node = ET.SubElement(self._root, node_name, taxes)
|
|
|
|
|
|
|
|
if retenciones:
|
|
|
|
node_name = f'{{{self._XMLNS}}}Retenciones'
|
|
|
|
sub_node = ET.SubElement(node, node_name)
|
|
|
|
node_name = f'{{{self._XMLNS}}}Retencion'
|
|
|
|
for tax in retenciones:
|
|
|
|
ET.SubElement(sub_node, node_name, tax)
|
|
|
|
|
|
|
|
if traslados:
|
|
|
|
node_name = f'{{{self._XMLNS}}}Traslados'
|
|
|
|
sub_node = ET.SubElement(node, node_name)
|
|
|
|
node_name = f'{{{self._XMLNS}}}Traslado'
|
|
|
|
for tax in traslados:
|
|
|
|
ET.SubElement(sub_node, node_name, tax)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _complementos(self):
|
|
|
|
if not self._data['complementos']:
|
|
|
|
return
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}Complemento'
|
|
|
|
node = ET.SubElement(self._root, node_name)
|
|
|
|
|
|
|
|
if 'leyendas' in self._data['complementos']:
|
|
|
|
self._complemento_leyendas(self._data['complementos']['leyendas'], node)
|
2022-11-05 22:48:54 -06:00
|
|
|
|
|
|
|
if 'pagos' in self._data['complementos']:
|
|
|
|
self._complemento_pagos(self._data['complementos']['pagos'], node)
|
|
|
|
elif 'comercio' in self._data['complementos']:
|
|
|
|
self._complemento_comercio(self._data['complementos']['comercio'], node)
|
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
def _complemento_leyendas(self, data, node):
|
|
|
|
attr = {'version': self._LEYENDAS['version']}
|
|
|
|
node_name = f"{{{self._LEYENDAS['xmlns']}}}LeyendasFiscales"
|
|
|
|
node_leyendas = ET.SubElement(node, node_name, attr)
|
|
|
|
|
|
|
|
for leyenda in data:
|
|
|
|
node_name = f"{{{self._LEYENDAS['xmlns']}}}Leyenda"
|
|
|
|
ET.SubElement(node_leyendas, node_name, leyenda)
|
|
|
|
return
|
|
|
|
|
2022-11-05 22:48:54 -06:00
|
|
|
def _complemento_pagos(self, data, node):
|
2022-11-08 19:47:44 -06:00
|
|
|
pago = data.pop('pago')
|
|
|
|
docs = data.pop('docs')
|
|
|
|
taxesd = data.pop('taxesd')
|
|
|
|
taxes = data.pop('taxes')
|
|
|
|
|
|
|
|
attr = {'Version': data.pop('Version')}
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}Pagos"
|
|
|
|
node_pagos = ET.SubElement(node, node_name, attr)
|
|
|
|
|
|
|
|
attr = data
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}Totales"
|
|
|
|
ET.SubElement(node_pagos, node_name, attr)
|
|
|
|
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}Pago"
|
|
|
|
node_pago = ET.SubElement(node_pagos, node_name, pago)
|
|
|
|
|
|
|
|
for i, doc in enumerate(docs):
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}DoctoRelacionado"
|
|
|
|
node_doc = ET.SubElement(node_pago, node_name, doc)
|
2023-03-27 10:32:03 -06:00
|
|
|
if taxesd[i]:
|
|
|
|
doc_taxes = taxesd[i]
|
2022-11-08 19:47:44 -06:00
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}ImpuestosDR"
|
|
|
|
node_taxes_doc = ET.SubElement(node_doc, node_name)
|
2023-03-27 10:32:03 -06:00
|
|
|
if 'retenciones' in doc_taxes:
|
2022-11-08 19:47:44 -06:00
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}RetencionesDR"
|
|
|
|
node_taxes_dr = ET.SubElement(node_taxes_doc, node_name)
|
2023-03-27 10:32:03 -06:00
|
|
|
for r in doc_taxes['retenciones']:
|
2022-11-08 19:47:44 -06:00
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}RetencionDR"
|
|
|
|
ET.SubElement(node_taxes_dr, node_name, r)
|
2023-03-27 10:32:03 -06:00
|
|
|
if 'traslados' in doc_taxes:
|
2022-11-08 19:47:44 -06:00
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}TrasladosDR"
|
|
|
|
node_taxes_dt = ET.SubElement(node_taxes_doc, node_name)
|
2023-03-27 10:32:03 -06:00
|
|
|
for t in doc_taxes['traslados']:
|
2022-11-08 19:47:44 -06:00
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}TrasladoDR"
|
|
|
|
ET.SubElement(node_taxes_dt, node_name, t)
|
|
|
|
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}ImpuestosP"
|
|
|
|
node_taxes = ET.SubElement(node_pago, node_name)
|
|
|
|
|
|
|
|
if 'retenciones' in taxes:
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}RetencionesP"
|
|
|
|
node_taxes_r = ET.SubElement(node_taxes, node_name)
|
|
|
|
for r in taxes['retenciones']:
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}RetencionP"
|
|
|
|
ET.SubElement(node_taxes_r, node_name, r)
|
|
|
|
if 'traslados' in taxes:
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}TrasladosP"
|
|
|
|
node_taxes_t = ET.SubElement(node_taxes, node_name)
|
|
|
|
for t in taxes['traslados']:
|
|
|
|
node_name = f"{{{self._PAGOS['xmlns']}}}TrasladoP"
|
|
|
|
ET.SubElement(node_taxes_t, node_name, t)
|
|
|
|
|
|
|
|
|
2022-11-05 22:48:54 -06:00
|
|
|
return
|
|
|
|
|
|
|
|
def _complemento_comercio(self, data, node):
|
|
|
|
mercancias = data.pop('mercancias', {})
|
|
|
|
emisor = data.pop('emisor', {})
|
|
|
|
propietario = data.pop('propietario', {})
|
|
|
|
receptor = data.pop('receptor', {})
|
|
|
|
destinatario = data.pop('destinatario', {})
|
|
|
|
attr = data
|
|
|
|
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}ComercioExterior"
|
|
|
|
node_comercio = ET.SubElement(node, node_name, attr)
|
|
|
|
|
|
|
|
if emisor:
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Emisor"
|
|
|
|
attr = {}
|
|
|
|
if 'Curp' in emisor:
|
|
|
|
attr = {'Curp': emisor.pop('Curp')}
|
|
|
|
node_emisor = ET.SubElement(node_comercio, node_name, attr)
|
|
|
|
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Domicilio"
|
|
|
|
ET.SubElement(node_emisor, node_name, emisor)
|
|
|
|
|
|
|
|
if propietario:
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Propietario"
|
|
|
|
ET.SubElement(node_comercio, node_name, propietario)
|
|
|
|
|
|
|
|
if receptor:
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Receptor"
|
|
|
|
attr = {}
|
|
|
|
if 'NumRegIdTrib' in receptor:
|
2023-03-23 21:19:56 -06:00
|
|
|
attr = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')}
|
2022-11-05 22:48:54 -06:00
|
|
|
node_receptor = ET.SubElement(node_comercio, node_name, attr)
|
|
|
|
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Domicilio"
|
|
|
|
ET.SubElement(node_receptor, node_name, receptor)
|
|
|
|
|
|
|
|
if destinatario:
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Destinatario"
|
|
|
|
attr = {}
|
|
|
|
if 'NumRegIdTrib' in destinatario:
|
|
|
|
attr['NumRegIdTrib'] = destinatario.pop('NumRegIdTrib')
|
|
|
|
if 'Nombre' in destinatario:
|
|
|
|
attr['Nombre'] = destinatario.pop('Nombre')
|
|
|
|
node_destinatario = ET.SubElement(node_comercio, node_name, attr)
|
|
|
|
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Domicilio"
|
|
|
|
ET.SubElement(node_destinatario, node_name, destinatario)
|
|
|
|
|
|
|
|
if mercancias:
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Mercancias"
|
|
|
|
node_mercancias = ET.SubElement(node_comercio, node_name)
|
|
|
|
for mercancia in mercancias:
|
|
|
|
description = mercancia.pop('description', {})
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}Mercancia"
|
|
|
|
node_mercancia = ET.SubElement(node_mercancias, node_name, mercancia)
|
|
|
|
if description:
|
|
|
|
node_name = f"{{{self._COMERCIO['xmlns']}}}DescripcionesEspecificas"
|
|
|
|
ET.SubElement(node_mercancia, node_name, description)
|
|
|
|
return
|
|
|
|
|
2022-08-10 20:41:16 -05:00
|
|
|
def _addenda(self):
|
2022-10-24 20:35:26 -05:00
|
|
|
type_boveda = self._data['addenda'].get('type', '')
|
|
|
|
type_client = self._data['addenda'].get('type_client', '')
|
2022-08-10 20:41:16 -05:00
|
|
|
data = self._data['addenda'].get('boveda', False)
|
2022-11-04 21:15:30 -06:00
|
|
|
lotes = self._data['addenda']['lotes']
|
|
|
|
self._boveda(type_boveda, data, lotes)
|
2022-10-24 20:35:26 -05:00
|
|
|
if type_client:
|
2022-08-10 20:41:16 -05:00
|
|
|
data = self._data['addenda'].get('cliente', False)
|
2022-10-24 20:35:26 -05:00
|
|
|
partes = self._data['addenda'].get('partes', ())
|
|
|
|
self._addenda_client(type_client, data, partes)
|
2022-08-10 20:41:16 -05:00
|
|
|
return
|
|
|
|
|
2022-11-04 21:15:30 -06:00
|
|
|
def _boveda(self, type_boveda, data, lotes):
|
2022-08-10 20:41:16 -05:00
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
XMLNS = 'http://kontender.mx/namespace/boveda'
|
|
|
|
NSMAP = {
|
|
|
|
'bovadd': 'http://kontender.mx/namespace/boveda',
|
|
|
|
'kon': 'http://kontender.mx/namespace',
|
|
|
|
}
|
|
|
|
|
|
|
|
node_name = f'{{{self._XMLNS}}}Addenda'
|
|
|
|
self._node_addenda = ET.SubElement(self._root, node_name)
|
|
|
|
|
|
|
|
schema = 'http://kontender.mx/namespace/boveda http://kontender.mx/namespace/boveda/BOVEDAFISCAL.xsd http://kontender.mx/namespace http://kontender.mx/namespace/AddendaK.xsd'
|
|
|
|
attr_qname = ET.QName(
|
|
|
|
'http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')
|
|
|
|
schema = {attr_qname: schema}
|
|
|
|
|
|
|
|
node_name = f'{{{XMLNS}}}BOVEDAFISCAL'
|
|
|
|
node = ET.SubElement(self._node_addenda, node_name, schema, nsmap=NSMAP)
|
|
|
|
|
2022-11-01 21:43:41 -06:00
|
|
|
if type_boveda == '01':
|
|
|
|
for k, v in data.items():
|
|
|
|
node_name = f'{{{XMLNS}}}{k}'
|
|
|
|
ET.SubElement(node, node_name, v)
|
2022-11-04 21:15:30 -06:00
|
|
|
if lotes:
|
|
|
|
node_name = f'{{{XMLNS}}}Lotes'
|
|
|
|
node_lotes = ET.SubElement(node, node_name)
|
|
|
|
for lote in lotes:
|
|
|
|
node_name = f'{{{XMLNS}}}Lote'
|
|
|
|
node_lote = ET.SubElement(node_lotes, node_name)
|
|
|
|
for k2, v2 in lote.items():
|
|
|
|
node_name = f'{{{XMLNS}}}{k2}'
|
|
|
|
sn = ET.SubElement(node_lote, node_name)
|
|
|
|
sn.text = v2
|
2022-11-01 21:43:41 -06:00
|
|
|
elif type_boveda == '02':
|
|
|
|
for k, v in data.items():
|
|
|
|
node_name = f'{{{XMLNS}}}{k}'
|
|
|
|
n = ET.SubElement(node, node_name)
|
|
|
|
n.text = v
|
2022-08-10 20:41:16 -05:00
|
|
|
|
|
|
|
return
|
|
|
|
|
2022-10-24 20:35:26 -05:00
|
|
|
def _addenda_client(self, type_client, data, partes):
|
2022-08-10 20:41:16 -05:00
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
2022-10-24 20:35:26 -05:00
|
|
|
if type_client == '1':
|
|
|
|
XMLNS = 'http://www.vwnovedades.com/volkswagen/kanseilab/shcp/2009/Addenda/PMT'
|
|
|
|
NSMAP = {'PMT': XMLNS}
|
|
|
|
elif type_client == '2':
|
|
|
|
XMLNS = 'http://www.vwnovedades.com/volkswagen/kanseilab/shcp/2009/Addenda/PSV'
|
|
|
|
NSMAP = {'PSV': XMLNS}
|
|
|
|
elif type_client == '4':
|
|
|
|
XMLNS = 'http://www.sas-automative/en/locations/local-offices-and-plants/mexico/plant-puebla.html'
|
|
|
|
NSMAP = {'PMT': XMLNS}
|
|
|
|
|
|
|
|
attr = data.pop('Factura')
|
2022-08-10 20:41:16 -05:00
|
|
|
node_name = f'{{{XMLNS}}}Factura'
|
|
|
|
node = ET.SubElement(self._node_addenda, node_name, **attr, nsmap=NSMAP)
|
|
|
|
|
|
|
|
for key, attr in data.items():
|
|
|
|
node_name = f'{{{XMLNS}}}{key}'
|
|
|
|
ET.SubElement(node, node_name, **attr)
|
|
|
|
|
|
|
|
if not partes:
|
|
|
|
return
|
|
|
|
|
|
|
|
node_name = f'{{{XMLNS}}}Partes'
|
|
|
|
node = ET.SubElement(node, node_name)
|
|
|
|
|
|
|
|
for parte in partes:
|
|
|
|
referencias = parte.pop('referencias')
|
|
|
|
node_name = f'{{{XMLNS}}}Parte'
|
|
|
|
sub_node = ET.SubElement(node, node_name, **parte)
|
|
|
|
node_name = f'{{{XMLNS}}}Referencias'
|
|
|
|
ET.SubElement(sub_node, node_name, **referencias)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
|
|
|
|
class DataToDict():
|
|
|
|
TRASLADO = 'T'
|
|
|
|
RETENCION = 'R'
|
|
|
|
NODES = {
|
|
|
|
'01': '_comprobante',
|
|
|
|
'02': '_relacionados',
|
|
|
|
'03': '_emisor',
|
|
|
|
'04': '_receptor',
|
|
|
|
'05': '_conceptos',
|
|
|
|
'06': '_impuestos',
|
2023-02-10 11:54:38 -06:00
|
|
|
'07': '_global',
|
2022-08-09 22:43:56 -05:00
|
|
|
'10': '_leyendas',
|
2022-11-05 22:48:54 -06:00
|
|
|
'11': '_complemento',
|
|
|
|
'12': '_complemento_12',
|
|
|
|
'13': '_complemento_13',
|
|
|
|
'14': '_complemento_14',
|
|
|
|
'15': '_complemento_15',
|
|
|
|
'16': '_complemento_16',
|
|
|
|
'17': '_complemento_17',
|
2022-11-04 21:15:30 -06:00
|
|
|
'49': '_addenda_lotes',
|
2022-08-10 20:41:16 -05:00
|
|
|
'50': '_boveda',
|
|
|
|
'51': '_addenda',
|
|
|
|
'52': '_addenda_partes',
|
2022-08-09 22:43:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
def __init__ (self, data):
|
|
|
|
self._data = data
|
2022-11-07 23:25:35 -06:00
|
|
|
self._cfdi = {'conceptos': [], 'impuestos': {}, 'complementos': {},
|
|
|
|
'addenda': {}}
|
2022-11-05 22:48:54 -06:00
|
|
|
self._complement = ''
|
2022-08-10 20:41:16 -05:00
|
|
|
self._type_header = ''
|
|
|
|
self._partes = []
|
2022-11-04 21:15:30 -06:00
|
|
|
self._lotes = []
|
2022-11-05 22:48:54 -06:00
|
|
|
self._ce_mercancias = []
|
2022-08-09 22:43:56 -05:00
|
|
|
self._process_data()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def cfdi(self):
|
|
|
|
return self._cfdi
|
|
|
|
|
|
|
|
def _process_data(self):
|
|
|
|
lines = self._data.split('\n')
|
|
|
|
for line in lines:
|
|
|
|
parts = line.split('|')
|
|
|
|
if not parts[0]:
|
|
|
|
continue
|
|
|
|
header = self.NODES.get(parts[0], '')
|
|
|
|
if not header:
|
|
|
|
log.debug(f'No existe: {parts[0]}')
|
|
|
|
continue
|
|
|
|
if hasattr(self, header):
|
|
|
|
getattr(self, header)(parts[2:])
|
2022-08-10 20:41:16 -05:00
|
|
|
|
|
|
|
self._cfdi['addenda']['partes'] = self._partes
|
2022-11-04 21:15:30 -06:00
|
|
|
self._cfdi['addenda']['lotes'] = self._lotes
|
2022-11-05 22:48:54 -06:00
|
|
|
|
|
|
|
if self._ce_mercancias:
|
|
|
|
self._cfdi['complementos']['comercio']['mercancias'] = \
|
|
|
|
self._ce_mercancias
|
2022-08-09 22:43:56 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
def _comprobante(self, data):
|
|
|
|
self._cfdi['comprobante'] = {}
|
|
|
|
fields = (
|
|
|
|
'Version',
|
|
|
|
'Serie',
|
|
|
|
'Folio',
|
|
|
|
'Fecha',
|
|
|
|
'FormaPago',
|
|
|
|
'CondicionesDePago',
|
|
|
|
'SubTotal',
|
|
|
|
'Descuento',
|
|
|
|
'Moneda',
|
|
|
|
'TipoCambio',
|
|
|
|
'Total',
|
|
|
|
'TipoDeComprobante',
|
|
|
|
'MetodoPago',
|
|
|
|
'LugarExpedicion',
|
|
|
|
'Confirmacion',
|
|
|
|
'Exportacion',
|
|
|
|
)
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
if not data[index]:
|
|
|
|
continue
|
|
|
|
self._cfdi['comprobante'][field] = data[index]
|
|
|
|
return
|
|
|
|
|
|
|
|
def _relacionados(self, data):
|
|
|
|
self._cfdi['relacionados'] = {}
|
|
|
|
if data[0]:
|
|
|
|
self._cfdi['relacionados']['TipoRelacion'] = data[0]
|
|
|
|
self._cfdi['relacionados']['UUID'] = data[1:]
|
|
|
|
return
|
|
|
|
|
|
|
|
def _emisor(self, data):
|
|
|
|
self._cfdi['emisor'] = {}
|
|
|
|
fields = (
|
|
|
|
'Rfc',
|
|
|
|
'Nombre',
|
|
|
|
'RegimenFiscal',
|
|
|
|
)
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
self._cfdi['emisor'][field] = data[index]
|
|
|
|
return
|
|
|
|
|
|
|
|
def _receptor(self, data):
|
|
|
|
self._cfdi['receptor'] = {}
|
|
|
|
fields = (
|
|
|
|
'Rfc',
|
|
|
|
'Nombre',
|
|
|
|
'DomicilioFiscalReceptor',
|
|
|
|
'ResidenciaFiscal',
|
|
|
|
'NumRegIdTrib',
|
|
|
|
'RegimenFiscalReceptor',
|
|
|
|
'UsoCFDI',
|
|
|
|
)
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
if not data[index]:
|
|
|
|
continue
|
|
|
|
self._cfdi['receptor'][field] = data[index]
|
|
|
|
return
|
|
|
|
|
|
|
|
def _get_taxes_by_concept(self, data):
|
|
|
|
taxes = {}
|
|
|
|
traslados = []
|
|
|
|
retenciones = []
|
2023-03-23 21:19:56 -06:00
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
for i in range(0, len(data), 6):
|
|
|
|
type_tax = data[i]
|
|
|
|
tax = {
|
|
|
|
'Base': data[i + 1],
|
|
|
|
'Impuesto': data[i + 2],
|
|
|
|
'TipoFactor': data[i + 3],
|
|
|
|
}
|
2023-03-02 21:46:16 -06:00
|
|
|
if data[i + 4]:
|
2023-03-23 21:19:56 -06:00
|
|
|
tax['TasaOCuota'] = data[i + 4]
|
2023-03-02 21:46:16 -06:00
|
|
|
if data[i + 5]:
|
2023-03-23 21:19:56 -06:00
|
|
|
tax['Importe'] = data[i + 5]
|
2023-03-02 21:46:16 -06:00
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
if type_tax == self.TRASLADO:
|
|
|
|
traslados.append(tax)
|
|
|
|
elif type_tax == self.RETENCION:
|
|
|
|
retenciones.append(tax)
|
|
|
|
if traslados:
|
|
|
|
taxes['traslados'] = traslados
|
|
|
|
if retenciones:
|
|
|
|
taxes['retenciones'] = retenciones
|
2023-03-23 21:19:56 -06:00
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
return taxes
|
|
|
|
|
|
|
|
def _conceptos(self, data):
|
|
|
|
concepto = {}
|
|
|
|
fields = (
|
|
|
|
'ClaveProdServ',
|
|
|
|
'NoIdentificacion',
|
|
|
|
'Cantidad',
|
|
|
|
'ClaveUnidad',
|
|
|
|
'Unidad',
|
|
|
|
'Descripcion',
|
|
|
|
'ValorUnitario',
|
|
|
|
'Importe',
|
|
|
|
'Descuento',
|
|
|
|
'ObjetoImp',
|
|
|
|
)
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
if not data[index]:
|
|
|
|
continue
|
|
|
|
concepto[field] = data[index]
|
|
|
|
pedimento = data[index + 1]
|
|
|
|
if pedimento:
|
|
|
|
concepto['pedimento'] = pedimento
|
|
|
|
concepto['impuestos'] = self._get_taxes_by_concept(data[index + 2:])
|
|
|
|
self._cfdi['conceptos'].append(concepto)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _get_taxes(self, data):
|
|
|
|
traslados = []
|
|
|
|
retenciones = []
|
|
|
|
for i in range(0, len(data), 6):
|
|
|
|
type_tax = data[i]
|
|
|
|
if type_tax == self.TRASLADO:
|
|
|
|
tax = {
|
|
|
|
'Base': data[i + 1],
|
|
|
|
'Impuesto': data[i + 2],
|
|
|
|
'TipoFactor': data[i + 3],
|
|
|
|
}
|
2023-07-04 11:59:20 -06:00
|
|
|
if data[i + 4]:
|
|
|
|
tax['TasaOCuota'] = data[i + 4]
|
|
|
|
if data[i + 5]:
|
|
|
|
tax['Importe'] = data[i + 5]
|
2022-08-09 22:43:56 -05:00
|
|
|
traslados.append(tax)
|
|
|
|
elif type_tax == self.RETENCION:
|
|
|
|
tax = {
|
|
|
|
'Impuesto': data[i + 2],
|
|
|
|
'Importe': data[i + 5],
|
|
|
|
}
|
|
|
|
retenciones.append(tax)
|
|
|
|
if traslados:
|
|
|
|
self._cfdi['impuestos']['traslados'] = traslados
|
|
|
|
if retenciones:
|
|
|
|
self._cfdi['impuestos']['retenciones'] = retenciones
|
|
|
|
return
|
|
|
|
|
|
|
|
def _impuestos(self, data):
|
2022-11-07 23:25:35 -06:00
|
|
|
# ~ self._cfdi['impuestos'] = {}
|
2022-08-09 22:43:56 -05:00
|
|
|
fields = (
|
|
|
|
'TotalImpuestosRetenidos',
|
|
|
|
'TotalImpuestosTrasladados',
|
|
|
|
)
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
if not data[index]:
|
|
|
|
continue
|
|
|
|
self._cfdi['impuestos'][field] = data[index]
|
|
|
|
self._get_taxes(data[index + 1:])
|
|
|
|
return
|
|
|
|
|
2023-02-10 11:54:38 -06:00
|
|
|
def _global(self, data):
|
|
|
|
self._cfdi['global'] = {}
|
|
|
|
fields = (
|
|
|
|
'Periodicidad',
|
|
|
|
'Meses',
|
|
|
|
'AƱo',
|
|
|
|
)
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
if not data[index]:
|
|
|
|
continue
|
|
|
|
self._cfdi['global'][field] = data[index]
|
|
|
|
return
|
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
def _leyendas(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
leyendas = []
|
|
|
|
for i in range(0, len(data), 3):
|
|
|
|
leyenda = {
|
|
|
|
'disposicionFiscal': data[i],
|
|
|
|
'norma': data[i+1],
|
|
|
|
'textoLeyenda': data[i+2],
|
|
|
|
}
|
|
|
|
leyendas.append(leyenda)
|
|
|
|
|
|
|
|
self._cfdi['complementos']['leyendas'] = leyendas
|
|
|
|
return
|
|
|
|
|
2022-11-05 22:48:54 -06:00
|
|
|
def _fields_to_dict(self, fields, data):
|
|
|
|
attr = {}
|
|
|
|
for index, field in enumerate(fields):
|
|
|
|
if not data[index]:
|
|
|
|
continue
|
|
|
|
attr[field] = data[index]
|
|
|
|
return attr
|
|
|
|
|
|
|
|
def _complemento(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._complement = data[0]
|
|
|
|
version = {'Version': data[1]}
|
|
|
|
if self._complement == '1':
|
|
|
|
self._cfdi['complementos']['pagos'] = version
|
2022-11-08 19:47:44 -06:00
|
|
|
self._cfdi['complementos']['pagos']['docs'] = []
|
|
|
|
self._cfdi['complementos']['pagos']['taxes'] = {}
|
2023-03-27 10:32:03 -06:00
|
|
|
self._cfdi['complementos']['pagos']['taxesd'] = []
|
2022-11-05 22:48:54 -06:00
|
|
|
elif self._complement == '2':
|
|
|
|
self._cfdi['complementos']['comercio'] = version
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _complemento_12(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._complement == '1':
|
|
|
|
fields = (
|
|
|
|
'TotalRetencionesIVA',
|
|
|
|
'TotalRetencionesISR',
|
|
|
|
'TotalRetencionesIEPS',
|
|
|
|
'TotalTrasladosBaseIVA16',
|
|
|
|
'TotalTrasladosImpuestoIVA16',
|
|
|
|
'TotalTrasladosBaseIVA8',
|
|
|
|
'TotalTrasladosImpuestoIVA8',
|
|
|
|
'TotalTrasladosBaseIVA0',
|
|
|
|
'TotalTrasladosImpuestoIVA0',
|
|
|
|
'TotalTrasladosBaseIVAExento',
|
|
|
|
'MontoTotalPagos',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['pagos'].update(attr)
|
|
|
|
elif self._complement == '2':
|
|
|
|
fields = (
|
|
|
|
'MotivoTraslado',
|
|
|
|
'TipoOperacion',
|
|
|
|
'ClaveDePedimento',
|
|
|
|
'CertificadoOrigen',
|
|
|
|
'NumCertificadoOrigen',
|
|
|
|
'NumeroExportadorConfiable',
|
|
|
|
'Incoterm',
|
|
|
|
'Subdivision',
|
|
|
|
'Observaciones',
|
|
|
|
'TipoCambioUSD',
|
|
|
|
'TotalUSD',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['comercio'].update(attr)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _complemento_13(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._complement == '1':
|
2022-11-08 19:47:44 -06:00
|
|
|
fields = (
|
|
|
|
'FechaPago',
|
|
|
|
'FormaDePagoP',
|
|
|
|
'MonedaP',
|
|
|
|
'TipoCambioP',
|
|
|
|
'Monto',
|
|
|
|
'NumOperacion',
|
|
|
|
'RfcEmisorCtaOrd',
|
|
|
|
'NomBancoOrdExt',
|
|
|
|
'CtaOrdenante',
|
|
|
|
'RfcEmisorCtaBen',
|
|
|
|
'CtaBeneficiario',
|
|
|
|
'TipoCadPago',
|
|
|
|
'CertPago',
|
|
|
|
'CadPago',
|
|
|
|
'SelloPago',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['pagos']['pago'] = attr
|
2022-11-05 22:48:54 -06:00
|
|
|
elif self._complement == '2':
|
|
|
|
fields = (
|
|
|
|
'Curp',
|
|
|
|
'Calle',
|
|
|
|
'NumeroExterior',
|
|
|
|
'NumeroInterior',
|
|
|
|
'Colonia',
|
|
|
|
'Localidad',
|
|
|
|
'Referencia',
|
|
|
|
'Municipio',
|
|
|
|
'Estado',
|
|
|
|
'Pais',
|
|
|
|
'CodigoPostal',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['comercio']['emisor'] = attr
|
|
|
|
return
|
|
|
|
|
2022-11-08 19:47:44 -06:00
|
|
|
def _get_retenciones_by_pay(self, data):
|
|
|
|
retenciones = []
|
|
|
|
for i in range(0, len(data), 2):
|
|
|
|
tax = {
|
|
|
|
'ImpuestoP': data[i],
|
|
|
|
'ImporteP': data[i + 1],
|
|
|
|
}
|
|
|
|
retenciones.append(tax)
|
|
|
|
return retenciones
|
|
|
|
|
2022-11-05 22:48:54 -06:00
|
|
|
def _complemento_14(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._complement == '1':
|
2022-11-08 19:47:44 -06:00
|
|
|
attr = self._get_retenciones_by_pay(data)
|
|
|
|
self._cfdi['complementos']['pagos']['taxes']['retenciones'] = attr
|
2022-11-05 22:48:54 -06:00
|
|
|
elif self._complement == '2':
|
|
|
|
fields = ('NumRegIdTrib', 'ResidenciaFiscal')
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
if attr:
|
|
|
|
self._cfdi['complementos']['comercio']['propietario'] = attr
|
|
|
|
return
|
|
|
|
|
2022-11-08 19:47:44 -06:00
|
|
|
def _get_traslados_by_pay(self, data):
|
|
|
|
traslados = []
|
|
|
|
for i in range(0, len(data), 5):
|
|
|
|
tax = {
|
|
|
|
'BaseP': data[i],
|
|
|
|
'ImpuestoP': data[i + 1],
|
|
|
|
'TipoFactorP': data[i + 2],
|
|
|
|
}
|
2023-08-10 11:02:51 -06:00
|
|
|
if data[i + 3]:
|
|
|
|
tax['TasaOCuotaP'] = data[i + 3]
|
|
|
|
if data[i + 4]:
|
|
|
|
tax['ImporteP'] = data[i + 4]
|
2022-11-08 19:47:44 -06:00
|
|
|
traslados.append(tax)
|
|
|
|
return traslados
|
|
|
|
|
2022-11-05 22:48:54 -06:00
|
|
|
def _complemento_15(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._complement == '1':
|
2022-11-08 19:47:44 -06:00
|
|
|
attr = self._get_traslados_by_pay(data)
|
|
|
|
self._cfdi['complementos']['pagos']['taxes']['traslados'] = attr
|
2022-11-05 22:48:54 -06:00
|
|
|
elif self._complement == '2':
|
|
|
|
fields = (
|
|
|
|
'NumRegIdTrib',
|
|
|
|
'Calle',
|
|
|
|
'NumeroExterior',
|
|
|
|
'NumeroInterior',
|
|
|
|
'Colonia',
|
|
|
|
'Localidad',
|
|
|
|
'Referencia',
|
|
|
|
'Municipio',
|
|
|
|
'Estado',
|
|
|
|
'Pais',
|
|
|
|
'CodigoPostal',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['comercio']['receptor'] = attr
|
|
|
|
return
|
|
|
|
|
|
|
|
def _complemento_16(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._complement == '1':
|
2022-11-08 19:47:44 -06:00
|
|
|
fields = (
|
|
|
|
'IdDocumento',
|
|
|
|
'Serie',
|
|
|
|
'Folio',
|
|
|
|
'MonedaDR',
|
|
|
|
'EquivalenciaDR',
|
|
|
|
'NumParcialidad',
|
|
|
|
'ImpSaldoAnt',
|
|
|
|
'ImpPagado',
|
|
|
|
'ImpSaldoInsoluto',
|
|
|
|
'ObjetoImpDR',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['pagos']['docs'].append(attr)
|
2022-11-05 22:48:54 -06:00
|
|
|
elif self._complement == '2':
|
|
|
|
fields = (
|
|
|
|
'NumRegIdTrib',
|
|
|
|
'Nombre',
|
|
|
|
'Calle',
|
|
|
|
'NumeroExterior',
|
|
|
|
'NumeroInterior',
|
|
|
|
'Colonia',
|
|
|
|
'Localidad',
|
|
|
|
'Referencia',
|
|
|
|
'Municipio',
|
|
|
|
'Estado',
|
|
|
|
'Pais',
|
|
|
|
'CodigoPostal',
|
|
|
|
)
|
|
|
|
attr = self._fields_to_dict(fields, data)
|
|
|
|
self._cfdi['complementos']['comercio']['destinatario'] = attr
|
|
|
|
return
|
|
|
|
|
2022-11-08 19:47:44 -06:00
|
|
|
def _get_taxes_by_doc(self, data):
|
|
|
|
taxes = {}
|
|
|
|
traslados = []
|
|
|
|
retenciones = []
|
|
|
|
for i in range(0, len(data), 6):
|
|
|
|
type_tax = data[i]
|
|
|
|
tax = {
|
|
|
|
'BaseDR': data[i + 1],
|
|
|
|
'ImpuestoDR': data[i + 2],
|
2023-08-10 11:02:51 -06:00
|
|
|
'TipoFactorDR': data[i + 3]
|
2022-11-08 19:47:44 -06:00
|
|
|
}
|
2023-08-10 11:02:51 -06:00
|
|
|
if data[i + 4]:
|
|
|
|
tax['TasaOCuotaDR'] = data[i + 4]
|
|
|
|
if data[i + 5]:
|
|
|
|
tax['ImporteDR'] = data[i + 5]
|
|
|
|
|
2022-11-08 19:47:44 -06:00
|
|
|
if type_tax == self.TRASLADO:
|
|
|
|
traslados.append(tax)
|
|
|
|
elif type_tax == self.RETENCION:
|
|
|
|
retenciones.append(tax)
|
|
|
|
|
|
|
|
if traslados:
|
|
|
|
taxes['traslados'] = traslados
|
|
|
|
if retenciones:
|
|
|
|
taxes['retenciones'] = retenciones
|
|
|
|
|
|
|
|
return taxes
|
|
|
|
|
2022-11-05 22:48:54 -06:00
|
|
|
def _complemento_17(self, data):
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._complement == '1':
|
2022-11-08 19:47:44 -06:00
|
|
|
attr = self._get_taxes_by_doc(data)
|
2023-03-27 10:32:03 -06:00
|
|
|
self._cfdi['complementos']['pagos']['taxesd'].append(attr)
|
2022-11-05 22:48:54 -06:00
|
|
|
elif self._complement == '2':
|
|
|
|
fields = (
|
|
|
|
'NoIdentificacion',
|
|
|
|
'FraccionArancelaria',
|
|
|
|
'CantidadAduana',
|
|
|
|
'UnidadAduana',
|
|
|
|
'ValorUnitarioAduana',
|
|
|
|
'ValorDolares',
|
|
|
|
)
|
|
|
|
mercancia = self._fields_to_dict(fields, data)
|
|
|
|
fields = (
|
|
|
|
'Marca',
|
|
|
|
'Modelo',
|
|
|
|
'SubModelo',
|
|
|
|
'NumeroSerie',
|
|
|
|
)
|
|
|
|
description = self._fields_to_dict(fields, data[6:])
|
|
|
|
if description:
|
|
|
|
mercancia['description'] = description
|
|
|
|
self._ce_mercancias.append(mercancia)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2022-08-10 20:41:16 -05:00
|
|
|
def _boveda(self, data):
|
|
|
|
type_addenda = data[0]
|
2022-11-01 21:43:41 -06:00
|
|
|
if type_addenda == '01':
|
|
|
|
fields = (
|
|
|
|
('ImporteLetra', 'importe'),
|
|
|
|
('UsoCFDI', 'UsoCFDI'),
|
|
|
|
('MetodosPago', 'MetodoPagoSAT'),
|
|
|
|
('FormaPago', 'FormaPagoSAT'),
|
|
|
|
('TipoDoctoElectronico', 'TipoDocumento'),
|
|
|
|
('BovedaFiscal', 'almacen', 'condicion', 'correoEmisor', 'correoReceptor', 'numeroCliente', 'razonSocialCliente', 'tipo'),
|
|
|
|
('DireccionEmisor', 'Calle', 'CodigoPostal', 'Colonia', 'Estado', 'Localidad', 'Municipio', 'NoExterior', 'NoInterior', 'Pais', 'Referencia', 'Telefono'),
|
|
|
|
('DireccionSucursal', 'Calle', 'Ciudad', 'CodigoPostal', 'Colonia', 'Estado', 'Localidad', 'Municipio', 'NoExterior', 'NoInterior', 'Pais', 'Referencia'),
|
|
|
|
('DireccionReceptor', 'Calle', 'Ciudad', 'CodigoPostal', 'Colonia', 'Delegacion', 'Estado', 'Localidad', 'Municipio', 'NoExterior', 'NoInterior', 'Pais', 'Referencia'),
|
|
|
|
('DireccionReceptorSucursal', 'Nombre', 'Calle', 'Ciudad', 'CodigoPostal', 'Estado', 'Pais', 'Comentario', 'Dato01', 'Dato02', 'Dato03', 'Dato04', 'Dato05', 'Dato06', 'Dato07', 'Dato08', 'Dato09', 'Dato10'),
|
|
|
|
('NombreComercial', 'Nombre'),
|
|
|
|
('ClaveTipoFolio', 'clave'),
|
|
|
|
('TR', 'transaccion'),
|
|
|
|
('OrdenCompra', 'folio'),
|
|
|
|
('NotaDeVenta', 'folio'),
|
|
|
|
)
|
|
|
|
boveda = {}
|
|
|
|
i = 1
|
|
|
|
for f in fields:
|
|
|
|
k = f[0]
|
|
|
|
attr = {}
|
|
|
|
for a in f[1:]:
|
|
|
|
try:
|
|
|
|
attr[a] = data[i]
|
|
|
|
except IndexError:
|
|
|
|
log.error('Faltan datos en addenda Boveda')
|
|
|
|
attr[a] = ''
|
|
|
|
i += 1
|
|
|
|
boveda[k] = attr
|
|
|
|
elif type_addenda == '02':
|
2022-10-24 20:35:26 -05:00
|
|
|
fields = (
|
|
|
|
'Razon_Social_destino',
|
|
|
|
'Calle_Destino',
|
|
|
|
'Colonia_Destino',
|
|
|
|
'Ciudad_Destino',
|
|
|
|
'Estado_Destino',
|
|
|
|
'Pais_Destino',
|
|
|
|
'CP_Destino_consigan',
|
|
|
|
'RFC_Destino_consigna',
|
|
|
|
'Telefono_Receptor',
|
|
|
|
'Peso_Bruto',
|
|
|
|
'Peso_Neto',
|
|
|
|
'Incoterm',
|
|
|
|
'leyenda_pie',
|
|
|
|
'R.vto',
|
|
|
|
'TIPO_CAMBIO_FACTURA',
|
|
|
|
'R.cte',
|
|
|
|
'RI_Solicitante',
|
|
|
|
'R.fefa',
|
|
|
|
'Razon_Social_facturado',
|
|
|
|
'Calle_facturado',
|
|
|
|
'Colonia_facturado',
|
|
|
|
'RFC_destino',
|
|
|
|
'Telefono_facturado',
|
|
|
|
'NUMCTAPAGO',
|
|
|
|
)
|
|
|
|
boveda = {}
|
|
|
|
for i, f in enumerate(fields):
|
|
|
|
boveda[f] = data[i+1]
|
2022-08-10 20:41:16 -05:00
|
|
|
|
|
|
|
self._cfdi['addenda']['type'] = type_addenda
|
|
|
|
self._cfdi['addenda']['boveda'] = boveda
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def _addenda(self, header):
|
|
|
|
self._type_header = header[1]
|
2022-10-24 20:35:26 -05:00
|
|
|
self._cfdi['addenda']['type_client'] = self._type_header
|
|
|
|
data = {}
|
|
|
|
if self._type_header == '2':
|
|
|
|
data['Factura'] = {
|
|
|
|
'version': header[3],
|
|
|
|
'tipoDocumentoFiscal': header[4],
|
|
|
|
'tipoDocumentoVWM': header[5],
|
|
|
|
'division': header[6]}
|
|
|
|
data['Moneda'] = {
|
|
|
|
'tipoMoneda': header[7],
|
|
|
|
'tipoCambio': header[8]}
|
|
|
|
data['Proveedor'] = {
|
|
|
|
'codigo': header[10],
|
|
|
|
'nombre': header[11],
|
|
|
|
'correoContacto': header[12]}
|
|
|
|
data['Origen'] = {'codigo': header[13]}
|
|
|
|
data['Destino'] = {
|
|
|
|
'codigo': header[14],
|
|
|
|
'naveReciboMaterial': header[15]}
|
|
|
|
data['Referencias'] = {'referenciaProveedor': header[16]}
|
|
|
|
data['Solicitante'] = {'correo': header[17]}
|
|
|
|
data['Archivo'] = {'datos': header[20], 'tipo': 'ZIP'}
|
|
|
|
elif self._type_header == '4':
|
|
|
|
data['Factura'] = {'version': header[3]}
|
2022-08-10 20:41:16 -05:00
|
|
|
data['Moneda'] = {'tipoMoneda': header[7]}
|
|
|
|
data['Proveedor'] = {'codigo': header[10], 'nombre': header[11]}
|
|
|
|
data['Referencias'] = {'referenciaProveedor': header[16]}
|
|
|
|
|
|
|
|
self._cfdi['addenda']['cliente'] = data
|
|
|
|
return
|
|
|
|
|
2022-11-04 21:15:30 -06:00
|
|
|
def _addenda_lotes(self, lote):
|
|
|
|
attr = {
|
|
|
|
'IVM_InvoiceID': '',
|
|
|
|
'POline': lote[0],
|
|
|
|
'NoIdentificacion': lote[1],
|
|
|
|
'Lote': lote[2],
|
|
|
|
'Cantidad': lote[3],
|
|
|
|
'Fecha': lote[4],
|
|
|
|
}
|
|
|
|
self._lotes.append(attr)
|
|
|
|
return
|
|
|
|
|
2022-08-10 20:41:16 -05:00
|
|
|
def _addenda_partes(self, parte):
|
2022-10-24 20:35:26 -05:00
|
|
|
if self._type_header == '2':
|
|
|
|
attr = {'posicion': parte[0],
|
|
|
|
'numeroMaterial': parte[1],
|
|
|
|
'descripcionMaterial': parte[2],
|
|
|
|
'cantidadMaterial': parte[3],
|
|
|
|
'unidadMedida': parte[4],
|
|
|
|
'precioUnitario': parte[5],
|
|
|
|
'montoLinea': parte[6],
|
|
|
|
'codigoImpuesto': parte[8],
|
|
|
|
'referencias': {
|
|
|
|
'ordenCompra': parte[7],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elif self._type_header == '4':
|
2022-08-10 20:41:16 -05:00
|
|
|
attr = {'posicion': parte[0],
|
|
|
|
'numeroMaterial': parte[1],
|
|
|
|
'descripcionMaterial': parte[2],
|
|
|
|
'referencias': {
|
|
|
|
'ordenCompra': parte[7],
|
|
|
|
'numeroPKN': parte[8],
|
|
|
|
'numeroASN': parte[9],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self._partes.append(attr)
|
|
|
|
return
|
|
|
|
|
2022-08-09 22:43:56 -05:00
|
|
|
|
|
|
|
def stamp_cfdi(cfdi, cert):
|
|
|
|
xslt = open(PATH_XSLT, 'rb')
|
|
|
|
root = ET.fromstring(cfdi.encode())
|
|
|
|
root.attrib['NoCertificado'] = cert.serial_number
|
|
|
|
root.attrib['Certificado'] = cert.txt
|
|
|
|
|
|
|
|
transfor = ET.XSLT(ET.parse(xslt))
|
|
|
|
cadena = str(transfor(root)).encode()
|
|
|
|
root.attrib['Sello'] = cert.sign(cadena)
|
|
|
|
xslt.close()
|
|
|
|
|
|
|
|
xml = ET.tostring(root, pretty_print=True, encoding='utf-8')
|
|
|
|
|
|
|
|
return xml.decode()
|
|
|
|
|
|
|
|
|
|
|
|
def _get_files(path, ext='xml'):
|
|
|
|
paths = []
|
|
|
|
for folder, _, files in os.walk(path):
|
|
|
|
pattern = re.compile('\.{}'.format(ext), re.IGNORECASE)
|
|
|
|
paths += [join(folder, f) for f in files if pattern.search(f)]
|
|
|
|
return paths
|
|
|
|
|
|
|
|
|
|
|
|
def _read_file(path, encoding='utf-8'):
|
|
|
|
# ~ CODEC_WIN = 'ISO-8859-1'
|
|
|
|
with open(path, 'r', encoding=encoding) as f:
|
|
|
|
data = f.read()
|
|
|
|
if DEBUG:
|
|
|
|
msg = f'Archivo leido: {path}'
|
|
|
|
log.debug(msg)
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def _save_file(path, target, data):
|
|
|
|
_, filename = os.path.split(path)
|
|
|
|
name, _ = os.path.splitext(filename)
|
|
|
|
path_new = join(target, f'{name}.xml')
|
|
|
|
data = f'<?xml version="1.0" encoding="utf-8"?>\n{data}'
|
|
|
|
with open(path_new, 'w', encoding='utf-8') as f:
|
|
|
|
f.write(data)
|
|
|
|
if DEBUG:
|
|
|
|
msg = f'Archivo sellado: {path}'
|
|
|
|
log.debug(msg)
|
|
|
|
return path_new
|
|
|
|
|
|
|
|
|
|
|
|
def make_cfdi(source, target, dir_cert, nombre):
|
|
|
|
cert = Cert(dir_cert, nombre)
|
|
|
|
paths = _get_files(source, 'txt')
|
|
|
|
for path in paths:
|
2022-08-10 20:41:16 -05:00
|
|
|
# ~ _version33(path, target)
|
|
|
|
# ~ continue
|
2022-08-09 22:43:56 -05:00
|
|
|
data = _read_file(path)
|
|
|
|
data = DataToDict(data).cfdi
|
|
|
|
cfdi = DictToCfdi(data).cfdi
|
|
|
|
cfdi = stamp_cfdi(cfdi, cert)
|
|
|
|
path_xml = _save_file(path, target, cfdi)
|
|
|
|
msg = f'CFDI: {path_xml}'
|
|
|
|
log.info(msg)
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def stamp_pac(source, target):
|
|
|
|
pac = PAC()
|
|
|
|
paths = _get_files(source)
|
|
|
|
for path in paths:
|
|
|
|
log.info(f'\tEnviar: {path}')
|
|
|
|
_, filename = os.path.split(path)
|
|
|
|
data = open(path, 'r').read()
|
|
|
|
result = pac.stamp(data, PAC_AUTH)
|
|
|
|
|
|
|
|
if pac.error:
|
|
|
|
log.error(pac.error)
|
|
|
|
continue
|
|
|
|
|
|
|
|
new_path = f'{target}/{filename}'
|
|
|
|
|
|
|
|
with open(new_path, 'w') as f:
|
|
|
|
f.write(result['xml'])
|
|
|
|
log.info(f'\tTimbrada: {new_path}')
|
|
|
|
return
|