forked from elmau/empresa-libre
434 lines
16 KiB
Python
434 lines
16 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',
|
|
},
|
|
'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',
|
|
},
|
|
'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',
|
|
},
|
|
}
|
|
|
|
|
|
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.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']['total_locales_trasladados'] or \
|
|
datos['impuestos']['total_locales_retenciones']:
|
|
self._impuestos_locales = True
|
|
|
|
if datos['donativo']:
|
|
self._donativo = True
|
|
|
|
if 'ine' in datos['complementos']:
|
|
self._ine = True
|
|
|
|
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
|
|
|
|
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_donativo = SAT['ine']['schema']
|
|
|
|
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
|
|
schema_locales + schema_donativo +schema_ine
|
|
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):
|
|
conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre))
|
|
for row in datos:
|
|
complemento = {}
|
|
if 'complemento' in row:
|
|
complemento = row.pop('complemento')
|
|
cuenta_predial = row.pop('CuentaPredial', '')
|
|
pedimento = row.pop('Pedimento', '')
|
|
|
|
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 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 '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 _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
|