#!/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', }, 'nomina12': { 'version': '1.2', 'prefix': 'nomina', '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', }, } 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.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._emisor(datos['emisor']) self._receptor(datos['receptor']) self._conceptos(datos['conceptos']) self._impuestos(datos['impuestos']) 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 'nomina' in datos: return self._validate_nomina(datos) return True def _validate_nomina(self, datos): comprobante = datos['comprobante'] validators = ( ('MetodoDePago', 'NA'), ('TipoCambio', '1'), ('Moneda', 'MXN'), ('TipoDeComprobante', 'egreso'), ) for f, v in validators: if f in comprobante: if v != comprobante[f]: msg = 'El atributo: {}, debe ser: {}'.format(f, v) self.error = msg return False return True def _comprobante(self, datos): attributes = {} attributes['xmlns:{}'.format(self._pre)] = self._sat_cfdi['xmlns'] attributes['xmlns:xsi'] = self._xsi attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] attributes.update(datos) #~ if DEBUG: #~ attributes['Fecha'] = self._now() #~ attributes['NoCertificado'] = CERT_NUM 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 _emisor(self, datos): #~ if DEBUG: #~ datos['Rfc'] = RFC_TEST 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): conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre)) for row in datos: complemento = {} if 'complemento' in row: complemento = row.pop('complemento') 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 'InformacionAduanera' in row: for field in fields: if field in row['InformacionAduanera']: attributes[field] = row['InformacionAduanera'][field] if attributes: node_name = '{}:InformacionAduanera'.format(self._pre) ET.SubElement(concepto, node_name, attributes) if 'CuentaPredial' in row: attributes = {'numero': row['CuentaPredial']} node_name = '{}:CuentaPredial'.format(self._pre) ET.SubElement(concepto, node_name, attributes) if 'autRVOE' in row: fields = ( 'version', 'nombreAlumno', 'CURP', 'nivelEducativo', 'autRVOE', ) for field in fields: if field in row['autRVOE']: attributes[field] = row['autRVOE'][field] node_name = '{}:ComplementoConcepto'.format(self._pre) complemento = ET.SubElement(concepto, node_name) ET.SubElement(complemento, 'iedu:instEducativas', attributes) return def _impuestos(self, datos): 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): sat_nomina = SAT[NOMINA_ACTUAL] pre = sat_nomina['prefix'] 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) attributes = {} attributes['xmlns:{}'.format(pre)] = sat_nomina['xmlns'] attributes['xsi:schemaLocation'] = sat_nomina['schema'] attributes.update(datos) if not 'Version' in attributes: attributes['Version'] = sat_nomina['version'] nomina = ET.SubElement(complemento, '{}:Nomina'.format(pre), attributes) if emisor: ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor) if receptor: ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor) if percepciones: detalle = percepciones.pop('detalle', None) percepciones = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones) for row in detalle: ET.SubElement(percepciones, '{}:Percepcion'.format(pre), row) if deducciones: detalle = deducciones.pop('detalle', None) deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones) for row in detalle: ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row) return def _complementos(self, datos): complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre)) 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