forked from elmau/empresa-libre
471 lines
18 KiB
Python
471 lines
18 KiB
Python
|
#!/usr/bin/env python
|
||
|
|
||
|
import datetime
|
||
|
from xml.etree import ElementTree as ET
|
||
|
from xml.dom.minidom import parseString
|
||
|
|
||
|
from logbook import Logger
|
||
|
|
||
|
#~ from settings import DEBUG
|
||
|
|
||
|
|
||
|
log = Logger('XML')
|
||
|
CFDI_ACTUAL = 'cfdi33'
|
||
|
NOMINA_ACTUAL = 'nomina12'
|
||
|
|
||
|
SAT = {
|
||
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||
|
'cfdi32': {
|
||
|
'version': '3.2',
|
||
|
'prefix': 'cfdi',
|
||
|
'xmlns': 'http://www.sat.gob.mx/cfd/3',
|
||
|
'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd',
|
||
|
},
|
||
|
'cfdi33': {
|
||
|
'version': '3.3',
|
||
|
'prefix': 'cfdi',
|
||
|
'xmlns': 'http://www.sat.gob.mx/cfd/3',
|
||
|
'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd',
|
||
|
},
|
||
|
'nomina11': {
|
||
|
'version': '1.1',
|
||
|
'prefix': 'nomina',
|
||
|
'xmlns': 'http://www.sat.gob.mx/nomina',
|
||
|
'schema': 'http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd',
|
||
|
},
|
||
|
'nomina': {
|
||
|
'version': '1.2',
|
||
|
'prefix': 'nomina12',
|
||
|
'xmlns': 'http://www.sat.gob.mx/nomina12',
|
||
|
'schema': 'http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd',
|
||
|
},
|
||
|
'locales': {
|
||
|
'version': '1.0',
|
||
|
'prefix': 'implocal',
|
||
|
'xmlns': 'http://www.sat.gob.mx/implocal',
|
||
|
'schema': ' http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd',
|
||
|
},
|
||
|
'donativo': {
|
||
|
'version': '1.1',
|
||
|
'prefix': 'donat',
|
||
|
'xmlns': 'http://www.sat.gob.mx/donat',
|
||
|
'schema': ' http://www.sat.gob.mx/donat http://www.sat.gob.mx/sitio_internet/cfd/donat/donat11.xsd',
|
||
|
'leyenda': 'Este comprobante ampara un donativo, el cual serĆ” destinado por la donataria a los fines propios de su objeto social. En el caso de que los bienes donados hayan sido deducidos previamente para los efectos del impuesto sobre la renta, este donativo no es deducible. La reproducciĆ³n no autorizada de este comprobante constituye un delito en los tĆ©rminos de las disposiciones fiscales.',
|
||
|
},
|
||
|
'ine': {
|
||
|
'version': '1.1',
|
||
|
'prefix': 'ine',
|
||
|
'xmlns': 'http://www.sat.gob.mx/ine',
|
||
|
'schema': ' http://www.sat.gob.mx/ine http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd',
|
||
|
},
|
||
|
'edu': {
|
||
|
'version': '1.0',
|
||
|
'prefix': 'iedu',
|
||
|
'xmlns': 'http://www.sat.gob.mx/iedu',
|
||
|
'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/ine/iedu.xsd',
|
||
|
},
|
||
|
}
|
||
|
|
||
|
|
||
|
class CFDI(object):
|
||
|
|
||
|
def __init__(self, version=CFDI_ACTUAL):
|
||
|
self._sat_cfdi = SAT[version]
|
||
|
self._xsi = SAT['xsi']
|
||
|
self._pre = self._sat_cfdi['prefix']
|
||
|
self._cfdi = None
|
||
|
self._complemento = None
|
||
|
self._impuestos_locales = False
|
||
|
self._donativo = False
|
||
|
self._ine = False
|
||
|
<<<<<<< HEAD
|
||
|
self._edu = False
|
||
|
=======
|
||
|
self._is_nomina = False
|
||
|
>>>>>>> nomina
|
||
|
self.error = ''
|
||
|
|
||
|
def _now(self):
|
||
|
return datetime.datetime.now().isoformat()[:19]
|
||
|
|
||
|
def get_xml(self, datos):
|
||
|
if not self._validate(datos):
|
||
|
return ''
|
||
|
|
||
|
self._comprobante(datos['comprobante'])
|
||
|
self._relacionados(datos['relacionados'])
|
||
|
self._emisor(datos['emisor'])
|
||
|
self._receptor(datos['receptor'])
|
||
|
self._conceptos(datos['conceptos'])
|
||
|
self._impuestos(datos['impuestos'])
|
||
|
self._locales(datos['impuestos'])
|
||
|
self._donatarias(datos['donativo'])
|
||
|
self._complementos(datos['complementos'])
|
||
|
|
||
|
if 'nomina' in datos:
|
||
|
self._nomina(datos['nomina'])
|
||
|
|
||
|
return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
|
||
|
|
||
|
def add_sello(self, sello):
|
||
|
self._cfdi.attrib['Sello'] = sello
|
||
|
return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
|
||
|
|
||
|
def _to_pretty_xml(self, source):
|
||
|
tree = parseString(source)
|
||
|
xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
|
||
|
return xml
|
||
|
|
||
|
def _validate(self, datos):
|
||
|
if datos['impuestos']:
|
||
|
if datos['impuestos']['total_locales_trasladados'] or \
|
||
|
datos['impuestos']['total_locales_retenciones']:
|
||
|
self._impuestos_locales = True
|
||
|
|
||
|
if datos['donativo']:
|
||
|
self._donativo = True
|
||
|
|
||
|
if datos['complementos']:
|
||
|
if 'ine' in datos['complementos']:
|
||
|
self._ine = True
|
||
|
|
||
|
self._edu = datos['edu']
|
||
|
|
||
|
if 'nomina' in datos:
|
||
|
self._is_nomina = True
|
||
|
return self._validate_nomina(datos)
|
||
|
|
||
|
return True
|
||
|
|
||
|
def _validate_nomina(self, datos):
|
||
|
return True
|
||
|
|
||
|
def _comprobante(self, datos):
|
||
|
attributes = {}
|
||
|
attributes['xmlns:{}'.format(self._pre)] = self._sat_cfdi['xmlns']
|
||
|
attributes['xmlns:xsi'] = self._xsi
|
||
|
|
||
|
schema_locales = ''
|
||
|
if self._impuestos_locales:
|
||
|
name = 'xmlns:{}'.format(SAT['locales']['prefix'])
|
||
|
attributes[name] = SAT['locales']['xmlns']
|
||
|
schema_locales = SAT['locales']['schema']
|
||
|
|
||
|
schema_donativo = ''
|
||
|
if self._donativo:
|
||
|
name = 'xmlns:{}'.format(SAT['donativo']['prefix'])
|
||
|
attributes[name] = SAT['donativo']['xmlns']
|
||
|
schema_donativo = SAT['donativo']['schema']
|
||
|
|
||
|
schema_ine = ''
|
||
|
if self._ine:
|
||
|
name = 'xmlns:{}'.format(SAT['ine']['prefix'])
|
||
|
attributes[name] = SAT['ine']['xmlns']
|
||
|
schema_ine = SAT['ine']['schema']
|
||
|
|
||
|
<<<<<<< HEAD
|
||
|
schema_edu = ''
|
||
|
if self._edu:
|
||
|
name = 'xmlns:{}'.format(SAT['edu']['prefix'])
|
||
|
attributes[name] = SAT['edu']['xmlns']
|
||
|
schema_edu = SAT['edu']['schema']
|
||
|
|
||
|
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
|
||
|
schema_locales + schema_donativo + schema_ine + schema_edu
|
||
|
=======
|
||
|
schema_nomina = ''
|
||
|
if self._nomina:
|
||
|
name = 'xmlns:{}'.format(SAT['nomina']['prefix'])
|
||
|
attributes[name] = SAT['nomina']['xmlns']
|
||
|
schema_nomina = SAT['nomina']['schema']
|
||
|
|
||
|
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
|
||
|
schema_locales + schema_donativo + schema_ine + schema_nomina
|
||
|
>>>>>>> nomina
|
||
|
attributes.update(datos)
|
||
|
|
||
|
if not 'Version' in attributes:
|
||
|
attributes['Version'] = self._sat_cfdi['version']
|
||
|
if not 'Fecha' in attributes:
|
||
|
attributes['Fecha'] = self._now()
|
||
|
|
||
|
self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes)
|
||
|
return
|
||
|
|
||
|
def _relacionados(self, datos):
|
||
|
if not datos or not datos['tipo'] or not datos['cfdis']:
|
||
|
return
|
||
|
|
||
|
node_name = '{}:CfdiRelacionados'.format(self._pre)
|
||
|
value = {'TipoRelacion': datos['tipo']}
|
||
|
node = ET.SubElement(self._cfdi, node_name, value)
|
||
|
for uuid in datos['cfdis']:
|
||
|
node_name = '{}:CfdiRelacionado'.format(self._pre)
|
||
|
value = {'UUID': uuid}
|
||
|
ET.SubElement(node, node_name, value)
|
||
|
return
|
||
|
|
||
|
def _emisor(self, datos):
|
||
|
node_name = '{}:Emisor'.format(self._pre)
|
||
|
emisor = ET.SubElement(self._cfdi, node_name, datos)
|
||
|
return
|
||
|
|
||
|
def _receptor(self, datos):
|
||
|
node_name = '{}:Receptor'.format(self._pre)
|
||
|
emisor = ET.SubElement(self._cfdi, node_name, datos)
|
||
|
return
|
||
|
|
||
|
def _conceptos(self, datos):
|
||
|
from xml.sax.saxutils import escape, unescape
|
||
|
|
||
|
conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre))
|
||
|
for row in reversed(datos):
|
||
|
# ~ print (row['Descripcion'])
|
||
|
# ~ xml = escape(xml.encode('ascii', 'xmlcharrefreplace').decode('utf-8'), False)
|
||
|
# ~ row['Descripcion'] = escape(row['Descripcion'].replace('\n', ' '), False)
|
||
|
# ~ row['Descripcion'] = row['Descripcion'].replace('\n', '
')
|
||
|
# ~ print (row['Descripcion'])
|
||
|
|
||
|
complemento = {}
|
||
|
if 'complemento' in row:
|
||
|
complemento = row.pop('complemento')
|
||
|
cuenta_predial = row.pop('CuentaPredial', '')
|
||
|
pedimento = row.pop('Pedimento', '')
|
||
|
student = row.pop('student', '')
|
||
|
|
||
|
taxes = {}
|
||
|
if 'impuestos' in row:
|
||
|
taxes = row.pop('impuestos')
|
||
|
node_name = '{}:Concepto'.format(self._pre)
|
||
|
concepto = ET.SubElement(conceptos, node_name, row)
|
||
|
|
||
|
if taxes:
|
||
|
node_name = '{}:Impuestos'.format(self._pre)
|
||
|
impuestos = ET.SubElement(concepto, node_name)
|
||
|
if 'traslados' in taxes and taxes['traslados']:
|
||
|
node_name = '{}:Traslados'.format(self._pre)
|
||
|
traslados = ET.SubElement(impuestos, node_name)
|
||
|
for traslado in taxes['traslados']:
|
||
|
ET.SubElement(
|
||
|
traslados, '{}:Traslado'.format(self._pre), traslado)
|
||
|
if 'retenciones' in taxes and taxes['retenciones']:
|
||
|
node_name = '{}:Retenciones'.format(self._pre)
|
||
|
retenciones = ET.SubElement(impuestos, node_name)
|
||
|
for retencion in taxes['retenciones']:
|
||
|
ET.SubElement(
|
||
|
retenciones, '{}:Retencion'.format(self._pre), retencion)
|
||
|
|
||
|
if pedimento:
|
||
|
attributes = {'NumeroPedimento': pedimento}
|
||
|
node_name = '{}:InformacionAduanera'.format(self._pre)
|
||
|
ET.SubElement(concepto, node_name, attributes)
|
||
|
|
||
|
if cuenta_predial:
|
||
|
attributes = {'Numero': cuenta_predial}
|
||
|
node_name = '{}:CuentaPredial'.format(self._pre)
|
||
|
ET.SubElement(concepto, node_name, attributes)
|
||
|
|
||
|
if student:
|
||
|
node_name = '{}:ComplementoConcepto'.format(self._pre)
|
||
|
complemento = ET.SubElement(concepto, node_name)
|
||
|
ET.SubElement(complemento, 'iedu:instEducativas', student)
|
||
|
return
|
||
|
|
||
|
def _impuestos(self, datos):
|
||
|
if self._is_nomina:
|
||
|
return
|
||
|
|
||
|
if not datos:
|
||
|
node_name = '{}:Impuestos'.format(self._pre)
|
||
|
ET.SubElement(self._cfdi, node_name)
|
||
|
return
|
||
|
|
||
|
attributes = {}
|
||
|
fields = ('TotalImpuestosTrasladados', 'TotalImpuestosRetenidos')
|
||
|
for field in fields:
|
||
|
if field in datos:
|
||
|
attributes[field] = datos[field]
|
||
|
node_name = '{}:Impuestos'.format(self._pre)
|
||
|
impuestos = ET.SubElement(self._cfdi, node_name, attributes)
|
||
|
|
||
|
if 'retenciones' in datos and datos['retenciones']:
|
||
|
retenciones = ET.SubElement(impuestos, '{}:Retenciones'.format(self._pre))
|
||
|
for row in datos['retenciones']:
|
||
|
ET.SubElement(retenciones, '{}:Retencion'.format(self._pre), row)
|
||
|
|
||
|
if 'traslados' in datos and datos['traslados']:
|
||
|
traslados = ET.SubElement(impuestos, '{}:Traslados'.format(self._pre))
|
||
|
for row in datos['traslados']:
|
||
|
ET.SubElement(traslados, '{}:Traslado'.format(self._pre), row)
|
||
|
return
|
||
|
|
||
|
def _nomina(self, datos):
|
||
|
pre = SAT['nomina']['prefix']
|
||
|
|
||
|
if self._complemento is None:
|
||
|
self._complemento = ET.SubElement(
|
||
|
self._cfdi, '{}:Complemento'.format(self._pre))
|
||
|
|
||
|
emisor = datos.pop('emisor', None)
|
||
|
receptor = datos.pop('receptor', None)
|
||
|
percepciones = datos.pop('percepciones', None)
|
||
|
deducciones = datos.pop('deducciones', None)
|
||
|
otros_pagos = datos.pop('otros_pagos', ())
|
||
|
incapacidades = datos.pop('incapacidades', ())
|
||
|
|
||
|
nomina = ET.SubElement(
|
||
|
self._complemento, '{}:Nomina'.format(pre), datos['nomina'])
|
||
|
|
||
|
if emisor:
|
||
|
ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor)
|
||
|
|
||
|
if receptor:
|
||
|
node = ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor)
|
||
|
|
||
|
if percepciones:
|
||
|
details = percepciones.pop('details', None)
|
||
|
hours_extra = percepciones.pop('hours_extra', None)
|
||
|
separacion = percepciones.pop('separacion', None)
|
||
|
if details:
|
||
|
node = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones)
|
||
|
for row in details:
|
||
|
nodep = ET.SubElement(node, '{}:Percepcion'.format(pre), row)
|
||
|
if row['TipoPercepcion'] == '019' and hours_extra:
|
||
|
for he in hours_extra:
|
||
|
ET.SubElement(nodep, '{}:HorasExtra'.format(pre), he)
|
||
|
hours_extra = None
|
||
|
if separacion:
|
||
|
ET.SubElement(node, '{}:SeparacionIndemnizacion'.format(pre), separacion)
|
||
|
|
||
|
if deducciones:
|
||
|
details = deducciones.pop('details', None)
|
||
|
if details:
|
||
|
deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones)
|
||
|
for row in details:
|
||
|
ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row)
|
||
|
|
||
|
if otros_pagos:
|
||
|
node = ET.SubElement(nomina, '{}:OtrosPagos'.format(pre))
|
||
|
for row in otros_pagos:
|
||
|
subsidio = row.pop('subsidio', None)
|
||
|
subnode = ET.SubElement(node, '{}:OtroPago'.format(pre), row)
|
||
|
if subsidio:
|
||
|
ET.SubElement(subnode, '{}:SubsidioAlEmpleo'.format(pre), subsidio)
|
||
|
|
||
|
if incapacidades:
|
||
|
node = ET.SubElement(nomina, '{}:Incapacidades'.format(pre))
|
||
|
for row in incapacidades:
|
||
|
ET.SubElement(node, '{}:Incapacidad'.format(pre), row)
|
||
|
return
|
||
|
|
||
|
def _locales(self, datos):
|
||
|
if not self._impuestos_locales:
|
||
|
return
|
||
|
|
||
|
if self._complemento is None:
|
||
|
self._complemento = ET.SubElement(
|
||
|
self._cfdi, '{}:Complemento'.format(self._pre))
|
||
|
|
||
|
attributes = {}
|
||
|
attributes['version'] = SAT['locales']['version']
|
||
|
if not datos['total_locales_trasladados']:
|
||
|
datos['total_locales_trasladados'] = '0.00'
|
||
|
attributes['TotaldeTraslados'] = datos['total_locales_trasladados']
|
||
|
if not datos['total_locales_retenciones']:
|
||
|
datos['total_locales_retenciones'] = '0.00'
|
||
|
attributes['TotaldeRetenciones'] = datos['total_locales_retenciones']
|
||
|
|
||
|
node = ET.SubElement(
|
||
|
self._complemento, 'implocal:ImpuestosLocales', attributes)
|
||
|
|
||
|
for retencion in datos['locales_retenciones']:
|
||
|
ET.SubElement(node, 'implocal:RetencionesLocales', retencion)
|
||
|
for traslado in datos['locales_trasladados']:
|
||
|
ET.SubElement(node, 'implocal:TrasladosLocales', traslado)
|
||
|
return
|
||
|
|
||
|
def _donatarias(self, datos):
|
||
|
if not datos:
|
||
|
return
|
||
|
|
||
|
if self._complemento is None:
|
||
|
self._complemento = ET.SubElement(
|
||
|
self._cfdi, '{}:Complemento'.format(self._pre))
|
||
|
|
||
|
attributes = {}
|
||
|
attributes['version'] = SAT['donativo']['version']
|
||
|
attributes['leyenda'] = SAT['donativo']['leyenda']
|
||
|
attributes.update(datos)
|
||
|
|
||
|
node = ET.SubElement(self._complemento, 'donat:Donatarias', attributes)
|
||
|
|
||
|
return
|
||
|
|
||
|
def _complementos(self, datos):
|
||
|
if not datos:
|
||
|
return
|
||
|
|
||
|
if self._complemento is None:
|
||
|
self._complemento = ET.SubElement(
|
||
|
self._cfdi, '{}:Complemento'.format(self._pre))
|
||
|
|
||
|
if 'ine' in datos:
|
||
|
atributos = {'Version': SAT['ine']['version']}
|
||
|
atributos.update(datos['ine'])
|
||
|
ET.SubElement(self._complemento, 'ine:INE', atributos)
|
||
|
|
||
|
if 'ce' in datos:
|
||
|
pre = 'cce11'
|
||
|
datos = datos.pop('ce')
|
||
|
emisor = datos.pop('emisor')
|
||
|
propietario = datos.pop('propietario')
|
||
|
receptor = datos.pop('receptor')
|
||
|
destinatario = datos.pop('destinatario')
|
||
|
conceptos = datos.pop('conceptos')
|
||
|
|
||
|
attributes = {}
|
||
|
attributes['xmlns:{}'.format(pre)] = \
|
||
|
'http://www.sat.gob.mx/ComercioExterior11'
|
||
|
attributes['xsi:schemaLocation'] = \
|
||
|
'http://www.sat.gob.mx/ComercioExterior11 ' \
|
||
|
'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd'
|
||
|
attributes.update(datos)
|
||
|
ce = ET.SubElement(
|
||
|
complemento, '{}:ComercioExterior'.format(pre), attributes)
|
||
|
|
||
|
attributes = {}
|
||
|
if 'Curp' in emisor:
|
||
|
attributes = {'Curp': emisor.pop('Curp')}
|
||
|
node = ET.SubElement(ce, '{}:Emisor'.format(pre), attributes)
|
||
|
ET.SubElement(node, '{}:Domicilio'.format(pre), emisor)
|
||
|
|
||
|
if propietario:
|
||
|
ET.SubElement(ce, '{}:Propietario'.format(pre), propietario)
|
||
|
|
||
|
attributes = {}
|
||
|
if 'NumRegIdTrib' in receptor:
|
||
|
attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')}
|
||
|
node = ET.SubElement(ce, '{}:Receptor'.format(pre), attributes)
|
||
|
ET.SubElement(node, '{}:Domicilio'.format(pre), receptor)
|
||
|
|
||
|
attributes = {}
|
||
|
if 'NumRegIdTrib' in destinatario:
|
||
|
attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')}
|
||
|
if 'Nombre' in destinatario:
|
||
|
attributes.update({'Nombre': destinatario.pop('Nombre')})
|
||
|
node = ET.SubElement(ce, '{}:Destinatario'.format(pre), attributes)
|
||
|
ET.SubElement(node, '{}:Domicilio'.format(pre), destinatario)
|
||
|
|
||
|
node = ET.SubElement(ce, '{}:Mercancias'.format(pre))
|
||
|
fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie')
|
||
|
for row in conceptos:
|
||
|
detalle = {}
|
||
|
for f in fields:
|
||
|
if f in row:
|
||
|
detalle[f] = row.pop(f)
|
||
|
concepto = ET.SubElement(node, '{}:Mercancia'.format(pre), row)
|
||
|
if detalle:
|
||
|
ET.SubElement(
|
||
|
concepto, '{}:DescripcionesEspecificas'.format(pre), detalle)
|
||
|
return
|