#!/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 self._edu = False self._is_nomina = False 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']) #~ if 'complementos' in datos: #~ self._complementos(datos['complementos']) 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 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'] schema_edu = '' if self._edu: name = 'xmlns:{}'.format(SAT['edu']['prefix']) attributes[name] = SAT['edu']['xmlns'] schema_edu = SAT['edu']['schema'] 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_edu + schema_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