diff --git a/facho/facho.py b/facho/facho.py
index 2f32907..97a564e 100644
--- a/facho/facho.py
+++ b/facho/facho.py
@@ -117,8 +117,12 @@ class LXMLBuilder:
def set_attribute(self, elem, key, value):
elem.attrib[key] = value
- def remove_attributes(self, elem, keys):
+ @classmethod
+ def remove_attributes(cls, elem, keys, exclude = []):
for key in keys:
+ if key in exclude:
+ continue
+
try:
del elem.attrib[key]
except KeyError:
@@ -132,10 +136,8 @@ class LXMLBuilder:
attrs['encoding'] = attrs.pop('encoding', 'UTF-8')
for el in elem.getiterator():
- try:
- del el.attrib['facho_placeholder']
- except KeyError:
- pass
+ keys = filter(lambda key: key.startswith('facho_'), el.keys())
+ self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True'
if is_optional and el.getchildren() == [] and el.keys() == ['facho_optional']:
@@ -364,6 +366,20 @@ class FachoXML:
text = self.builder.get_text(elem)
return format_(text)
+ def exist_element(self, xpath):
+ elem = self.get_element(xpath)
+
+ if elem is None:
+ return False
+
+ if elem.get('facho_placeholder') == 'True':
+ return False
+
+ if elem.get('facho_optional') == 'True':
+ return False
+
+ return True
+
def _remove_facho_attributes(self, elem):
self.builder.remove_attributes(elem, ['facho_optional', 'facho_placeholder'])
diff --git a/facho/fe/nomina/__init__.py b/facho/fe/nomina/__init__.py
new file mode 100644
index 0000000..07708fb
--- /dev/null
+++ b/facho/fe/nomina/__init__.py
@@ -0,0 +1,125 @@
+from .. import fe
+from .. import form
+
+from dataclasses import dataclass
+
+class Amount(form.Amount):
+ pass
+
+
+class Devengado:
+ pass
+
+@dataclass
+class DevengadoBasico(Devengado):
+ dias_trabajados: int
+ sueldo_trabajado: Amount
+
+ def apply(self, fragment):
+ fragment.find_or_create_element('./Basico')
+
+ fragment.set_attributes('/Basico',
+ # NIE069
+ DiasTrabajados = str(self.dias_trabajados),
+ # NIE070
+ SueldoTrabajado = str(self.sueldo_trabajado)
+ )
+
+@dataclass
+class DevengadoTransporte(Devengado):
+ auxilio_transporte: Amount = None
+ viatico_manutencion: Amount = None
+ viatico_manutencion_no_salarial: Amount = None
+
+ def apply(self, fragment):
+ fragment.set_element('./Transporte', None,
+ append_ = True,
+ # NIE071
+ AuxilioTransporte = self.auxilio_transporte,
+ # NIE072
+ ViaticoManuAlojS = self.viatico_manutencion,
+ # NIE073
+ ViaticoManuAlojNS = self.viatico_manutencion_no_salarial
+ )
+
+class Deduccion:
+ pass
+
+@dataclass
+class DeduccionSalud(Deduccion):
+ porcentaje: Amount
+ deduccion: Amount
+
+ def apply(self, fragment):
+ fragment.set_element('./Salud', None,
+ append_ = True,
+ # NIE161
+ Porcentaje = self.porcentaje,
+ # NIE163
+ Deduccion = self.deduccion
+ )
+
+@dataclass
+class DeduccionFondoPension(Deduccion):
+ porcentaje: Amount
+ deduccion: Amount
+
+ def apply(self, fragment):
+ fragment.set_element('./FondoPension', None,
+ append_ = True,
+ # NIE164
+ Porcentaje = self.porcentaje,
+ # NIE166
+ Deduccion = self.deduccion
+ )
+
+class DIANNominaIndividualError(Exception):
+ pass
+
+class DIANNominaIndividual:
+ def __init__(self):
+ self.fexml = fe.FeXML('NominaIndividual', '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('./Devengados/Basico')
+ self.fexml.placeholder_for('./Devengados/Transporte', optional=True)
+
+ self.devengados = self.fexml.fragment('./Devengados')
+ self.deducciones = self.fexml.fragment('./Deducciones')
+
+ def adicionar_devengado(self, devengado):
+ if not isinstance(devengado, Devengado):
+ raise ValueError('se espera tipo Devengado')
+
+ devengado.apply(self.devengados)
+
+ def adicionar_deduccion(self, deduccion):
+ if not isinstance(deduccion, Deduccion):
+ raise ValueError('se espera tipo Devengado')
+
+ deduccion.apply(self.deducciones)
+
+ def validate(self):
+ """
+ Valida requisitos segun anexo tecnico
+ """
+ errors = []
+
+ def add_error(xpath, msg):
+ if not self.fexml.exist_element(xpath):
+ errors.append(DIANNominaIndividualError(msg))
+
+ add_error('/fe:NominaIndividual/Devengados/Basico',
+ 'se requiere DevengadoBasico')
+
+ add_error('/fe:NominaIndividual/Deducciones/Salud',
+ 'se requiere DeduccionSalud')
+
+ add_error('/fe:NominaIndividual/Deducciones/FondoPension',
+ 'se requiere DeduccionFondoPension')
+
+ return errors
+
+ def toFachoXML(self):
+ return self.fexml
diff --git a/tests/test_facho.py b/tests/test_facho.py
index 08ac90e..135b017 100644
--- a/tests/test_facho.py
+++ b/tests/test_facho.py
@@ -351,3 +351,14 @@ def test_facho_xml_placeholder_optional_and_fragment_with_set_element():
assert xml.tostring() == ''
assert xml.get_element_attribute('/root/A/AA', 'prueba') == 'OK'
+
+def test_facho_xml_exist_element():
+ xml = facho.FachoXML('root')
+
+ xml.placeholder_for('./A')
+ assert xml.exist_element('/root/A') == False
+ assert xml.tostring() == ''
+
+ xml.find_or_create_element('./A')
+ assert xml.exist_element('/root/A') == True
+ assert xml.tostring() == ''
diff --git a/tests/test_nomina.py b/tests/test_nomina.py
index f1432b1..007c5fc 100644
--- a/tests/test_nomina.py
+++ b/tests/test_nomina.py
@@ -60,3 +60,19 @@ def test_adicionar_deduccion_salud():
print(xml)
assert str(xml) == """"""
+def test_nomina_obligatorios_segun_anexo_tecnico():
+ nomina = fe.nomina.DIANNominaIndividual()
+
+ errors = nomina.validate()
+
+ assert_error(errors, 'se requiere DevengadoBasico')
+ assert_error(errors, 'se requiere DeduccionSalud')
+ assert_error(errors, 'se requiere DeduccionFondoPension')
+
+def assert_error(errors, msg):
+ for error in errors:
+ if str(error) == msg:
+ return True
+
+ raise "wants error: %s" % (msg)
+