From 56c7e2c453713372e559f5332483aea89616de55 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Wed, 17 Nov 2021 00:50:19 +0000 Subject: [PATCH] se adiciona Ajuste Reemplazar y Eliminar FossilOrigin-Name: b488d606e28c44a581d7a387d33f08442d53d613149fd19b264e28a6b94a8951 --- facho/facho.py | 23 +++++++--- facho/fe/nomina/__init__.py | 87 ++++++++++++++++++++++--------------- tests/test_facho.py | 31 +++++++++++++ tests/test_nomina.py | 44 +++++++++++++++++++ 4 files changed, 145 insertions(+), 40 deletions(-) diff --git a/facho/facho.py b/facho/facho.py index caa2a42..0a1a243 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -153,19 +153,20 @@ class FachoXML: """ Decora XML con funciones de consulta XPATH de un solo elemento """ - def __init__(self, root, builder=None, nsmap=None, fragment_prefix=''): + def __init__(self, root, builder=None, nsmap=None, fragment_prefix='',fragment_root_element=None): if builder is None: self.builder = LXMLBuilder(nsmap) else: self.builder = builder self.nsmap = nsmap - + if isinstance(root, str): self.root = self.builder.build_element_from_string(root, nsmap) else: self.root = root + self.fragment_root_element = fragment_root_element self.fragment_prefix = fragment_prefix self.xpath_for = {} self.extensions = [] @@ -195,7 +196,7 @@ class FachoXML: if parent is None: parent = self.find_or_create_element(xpath, append=append) - return FachoXML(parent, nsmap=self.nsmap, fragment_prefix=root_prefix) + return FachoXML(parent, nsmap=self.nsmap, fragment_prefix=root_prefix, fragment_root_element=self.root) def register_alias_xpath(self, alias, xpath): self.xpath_for[alias] = xpath @@ -374,6 +375,9 @@ class FachoXML: def get_element_text(self, xpath, format_=str, multiple=False): xpath = self.fragment_prefix + self._path_xpath_for(xpath) + # MACHETE(bit4bit) al usar ./ queda ../ + xpath = re.sub(r'^\.\.+', '.', xpath) + elem = self.builder.xpath(self.root, xpath, multiple=multiple) if multiple: vals = [] @@ -458,12 +462,19 @@ class FachoXML: def xpath_from_root(self, xpath): nsmap = {} ns = '' - + + root = self.root + if self.fragment_root_element is not None: + root = self.fragment_root_element + if isinstance(self.nsmap, dict): nsmap = dict(map(reversed, self.nsmap.items())) - ns = nsmap[etree.QName(self.root).namespace] + ':' + ns = nsmap[etree.QName(root).namespace] + ':' - new_xpath = '/' + ns + etree.QName(self.root).localname + '/' + xpath.lstrip('/') + if self.fragment_root_element is not None: + new_xpath = '/' + ns + etree.QName(root).localname + '/' + etree.QName(self.root).localname + '/' + xpath.lstrip('/') + else: + new_xpath = '/' + ns + etree.QName(root).localname + '/' + xpath.lstrip('/') return new_xpath def __str__(self): diff --git a/facho/fe/nomina/__init__.py b/facho/fe/nomina/__init__.py index a47c44f..844d91a 100644 --- a/facho/fe/nomina/__init__.py +++ b/facho/fe/nomina/__init__.py @@ -57,6 +57,8 @@ class Proveedor: def post_apply(self, fexml, fragment): cune_xpath = fexml.xpath_from_root('/InformacionGeneral') cune = fexml.get_element_attribute(cune_xpath, 'CUNE') + # TODO(bit4bit) https://catalogo‐vpfe‐hab.dian.gov.co/document/searchqr?documentkey=CUNE para habilitacion + # https://catalogo‐vpfe.dian.gov.co/document/searchqr?documentkey=CUNE codigo_qr = f"https://catalogo‐vpfe.dian.gov.co/document/searchqr?documentkey={cune}" fragment.set_attributes('./ProveedorXML', CodigoQR=codigo_qr) @@ -182,35 +184,39 @@ class DianXMLExtensionSigner(fe.DianXMLExtensionSigner): class DIANNominaXML: - def __init__(self, tag_document): + def __init__(self, tag_document, xpath_ajuste=None): self.tag_document = tag_document self.fexml = fe.FeXML(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1') # layout, la dian requiere que los elementos # esten ordenados segun el anexo tecnico self.fexml.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') - self.fexml.placeholder_for('./Novedad', optional=True) - self.fexml.placeholder_for('./Periodo') - self.fexml.placeholder_for('./NumeroSecuenciaXML') - self.fexml.placeholder_for('./LugarGeneracionXML') - self.fexml.placeholder_for('./ProveedorXML') - self.fexml.placeholder_for('./InformacionGeneral') - self.fexml.placeholder_for('./Empleador') - self.fexml.placeholder_for('./Trabajador') - self.fexml.placeholder_for('./Pago') - self.fexml.placeholder_for('./Devengados/Basico') - self.fexml.placeholder_for('./Devengados/Transporte', optional=True) + self.fexml.placeholder_for('./TipoNota', optional=True) + self.root_fragment = self.fexml + if xpath_ajuste is not None: + self.root_fragment = self.fexml.fragment(xpath_ajuste) + self.root_fragment.placeholder_for('./Novedad', optional=True) + self.root_fragment.placeholder_for('./Periodo') + self.root_fragment.placeholder_for('./NumeroSecuenciaXML') + self.root_fragment.placeholder_for('./LugarGeneracionXML') + self.root_fragment.placeholder_for('./ProveedorXML') + self.root_fragment.placeholder_for('./InformacionGeneral') + self.root_fragment.placeholder_for('./Empleador') + self.root_fragment.placeholder_for('./Trabajador') + self.root_fragment.placeholder_for('./Pago') + self.root_fragment.placeholder_for('./Devengados/Basico') + self.root_fragment.placeholder_for('./Devengados/Transporte', optional=True) - self.informacion_general_xml = self.fexml.fragment('./InformacionGeneral') - self.numero_secuencia_xml = self.fexml.fragment('./NumeroSecuenciaXML') - self.lugar_generacion_xml = self.fexml.fragment('./LugarGeneracionXML') - self.proveedor_xml = self.fexml.fragment('./ProveedorXML') - self.empleador = self.fexml.fragment('./Empleador') - self.trabajador = self.fexml.fragment('./Trabajador') - self.pago_xml = self.fexml.fragment('./Pago') - self.devengados = self.fexml.fragment('./Devengados') - self.deducciones = self.fexml.fragment('./Deducciones') + self.informacion_general_xml = self.root_fragment.fragment('./InformacionGeneral') + self.numero_secuencia_xml = self.root_fragment.fragment('./NumeroSecuenciaXML') + self.lugar_generacion_xml = self.root_fragment.fragment('./LugarGeneracionXML') + self.proveedor_xml = self.root_fragment.fragment('./ProveedorXML') + self.empleador = self.root_fragment.fragment('./Empleador') + self.trabajador = self.root_fragment.fragment('./Trabajador') + self.pago_xml = self.root_fragment.fragment('./Pago') + self.devengados = self.root_fragment.fragment('./Devengados') + self.deducciones = self.root_fragment.fragment('./Deducciones') self.informacion_general = None self.metadata = None @@ -310,25 +316,25 @@ class DIANNominaXML: #TODO(bit4bit) acoplamiento temporal # es importante el orden de ejecucion - self.informacion_general.post_apply(self.fexml, self.informacion_general_xml) + self.informacion_general.post_apply(self.root_fragment, self.informacion_general_xml) if self.metadata is not None: - self.metadata.post_apply(self.fexml, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml) + self.metadata.post_apply(self.root_fragment, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml) return self.fexml def _comprobante_total(self): - devengados_total = self.fexml.get_element_text_or_attribute(self.fexml.xpath_from_root('/DevengadosTotal'), '0.0') - deducciones_total = self.fexml.get_element_text_or_attribute(self.fexml.xpath_from_root('/DeduccionesTotal'), '0.0') + devengados_total = self.root_fragment.get_element_text_or_attribute('./DevengadosTotal', '0.0') + deducciones_total = self.root_fragment.get_element_text_or_attribute('./DeduccionesTotal', '0.0') comprobante_total = Amount(devengados_total) - Amount(deducciones_total) - self.fexml.set_element(self.fexml.xpath_from_root('/ComprobanteTotal'), str(round(comprobante_total, 2))) + self.root_fragment.set_element('./ComprobanteTotal', str(round(comprobante_total, 2))) def _deducciones_total(self): xpaths = [ - self.fexml.xpath_from_root('/Deducciones/Salud/@Deduccion'), - self.fexml.xpath_from_root('/Deducciones/FondoPension/@Deduccion') + self.root_fragment.xpath_from_root('/Deducciones/Salud/@Deduccion'), + self.root_fragment.xpath_from_root('/Deducciones/FondoPension/@Deduccion') ] deducciones = map(lambda valor: Amount(valor), self._values_of_xpaths(xpaths)) @@ -338,14 +344,14 @@ class DIANNominaXML: for deduccion in deducciones: deducciones_total += deduccion - self.fexml.set_element(f'/fe:{self.tag_document}/DeduccionesTotal', str(round(deducciones_total, 2))) + self.root_fragment.set_element('./DeduccionesTotal', str(round(deducciones_total, 2))) def _devengados_total(self): xpaths = [ - self.fexml.xpath_from_root('/Devengados/Basico/@SueldoTrabajado'), - self.fexml.xpath_from_root('/Devengados/Transporte/@AuxilioTransporte'), - self.fexml.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojS'), - self.fexml.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojNS') + self.root_fragment.xpath_from_root('/Devengados/Basico/@SueldoTrabajado'), + self.root_fragment.xpath_from_root('/Devengados/Transporte/@AuxilioTransporte'), + self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojS'), + self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojNS') ] devengados = map(lambda valor: Amount(valor), self._values_of_xpaths(xpaths)) @@ -354,7 +360,7 @@ class DIANNominaXML: for devengado in devengados: devengados_total += devengado - self.fexml.set_element(self.fexml.xpath_from_root('/DevengadosTotal'), str(round(devengados_total,2))) + self.root_fragment.set_element('./DevengadosTotal', str(round(devengados_total,2))) def _values_of_xpaths(self, xpaths): xpaths_values_of_values = map(lambda val: self.fexml.get_element_text_or_attribute(val, multiple=True), xpaths) @@ -378,5 +384,18 @@ class DIANNominaIndividual(DIANNominaXML): # TODO(bit4bit) confirmar que no tienen en comun con NominaIndividual class DIANNominaIndividualDeAjuste(DIANNominaXML): + class Reemplazar(DIANNominaXML): + def __init__(self): + super().__init__('NominaIndividualDeAjuste', './Reemplazar') + # NIAE214 + self.root_fragment.set_element('TipoNota', '1') + + class Eliminar(DIANNominaXML): + def __init__(self): + super().__init__('NominaIndividualDeAjuste', './Eliminar') + # NIAE214 + self.root_fragment.set_element('TipoNota', '2') + def __init__(self): super().__init__('NominaIndividualDeAjuste') + diff --git a/tests/test_facho.py b/tests/test_facho.py index af6eaa0..765b894 100644 --- a/tests/test_facho.py +++ b/tests/test_facho.py @@ -103,6 +103,7 @@ def test_facho_xml_fragment(): invoice.set_element('/Invoice/Id', 1) assert xml.tostring() == '1' + def test_facho_xml_fragments(): xml = facho.FachoXML('Invoice') @@ -129,6 +130,13 @@ def test_facho_xml_nested_fragments(): assert xml.tostring() == 'test
line 1
test
' +def test_facho_xml_get_element_text_of_fragment(): + xml = facho.FachoXML('root') + invoice = xml.fragment('/root/Invoice') + invoice.set_element('/Invoice/Id', 1) + + assert invoice.get_element_text('/Invoice/Id') == '1' + def test_facho_xml_get_element_text(): xml = facho.FachoXML('Invoice') xml.set_element('/Invoice/ID', 'ABC123') @@ -171,6 +179,11 @@ def test_facho_xml_fragment_relative(): invoice.set_element('./Id', 1) assert xml.tostring() == '1' +def test_facho_xml_get_element_fragment_relative(): + xml = facho.FachoXML('root') + invoice = xml.fragment('./Invoice') + invoice.set_element('./Id', 1) + assert invoice.get_element_text('./Id') == '1' def test_facho_xml_replacement_for(): xml = facho.FachoXML('root') @@ -371,6 +384,14 @@ def test_facho_xml_query_element_text_or_attribute(): assert xml.get_element_text_or_attribute('/root/A') == 'contenido' assert xml.get_element_text_or_attribute('/root/A/@clave') == 'valor' +def test_facho_xml_query_element_text_or_attribute_from_fragment(): + xml = facho.FachoXML('root') + + invoice = xml.fragment('/root/Invoice') + invoice.set_element('./A', 'contenido') + + assert invoice.get_element_text_or_attribute('/Invoice/A') == 'contenido' + def test_facho_xml_build_xml_absolute(): xml = facho.FachoXML('root') @@ -384,3 +405,13 @@ def test_facho_xml_build_xml_absolute_namespace(): xpath = xml.xpath_from_root('/A') assert xpath == '/fe:root/A' + + +def test_facho_xml_build_xml_absolute_namespace_from_fragment(): + xml = facho.FachoXML('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'), + nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'}) + invoice = xml.fragment('/root/Invoice') + + xpath = invoice.xpath_from_root('/A') + assert xpath == '/fe:root/Invoice/A' + diff --git a/tests/test_nomina.py b/tests/test_nomina.py index f575d0e..ccc1cb8 100644 --- a/tests/test_nomina.py +++ b/tests/test_nomina.py @@ -239,3 +239,47 @@ def test_nomina_xmlsign(monkeypatch): print(xml.tostring()) elem = xml.get_element('/fe:NominaIndividual/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature') assert elem is not None + + +def atest_nomina_ajuste_reemplazar(): + nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() + + xml = nomina.toFachoXML() + print(xml) + assert False + + +def test_adicionar_reemplazar_devengado_comprobante_total(): + nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() + + nomina.adicionar_devengado(fe.nomina.DevengadoBasico( + dias_trabajados = 60, + sueldo_trabajado = fe.nomina.Amount(2_000_000) + )) + + nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( + porcentaje = fe.nomina.Amount(19), + deduccion = fe.nomina.Amount(1_000_000) + )) + + xml = nomina.toFachoXML() + + assert xml.get_element_text('/fe:NominaIndividualDeAjuste/Reemplazar/ComprobanteTotal') == '1000000.00' + + +def test_adicionar_eliminar_devengado_comprobante_total(): + nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() + + nomina.adicionar_devengado(fe.nomina.DevengadoBasico( + dias_trabajados = 60, + sueldo_trabajado = fe.nomina.Amount(2_000_000) + )) + + nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( + porcentaje = fe.nomina.Amount(19), + deduccion = fe.nomina.Amount(1_000_000) + )) + + xml = nomina.toFachoXML() + + assert xml.get_element_text('/fe:NominaIndividualDeAjuste/Eliminar/ComprobanteTotal') == '1000000.00'