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() == 'testline 1test'
+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'