From 74d98e249db66f69a8a6740a490e23cd0bb791f5 Mon Sep 17 00:00:00 2001
From: bit4bit <bit4bit@noemail.net>
Date: Fri, 5 Nov 2021 02:09:21 +0000
Subject: [PATCH] se adiciona archivo faltante de nomina, y se agregan mas
 deducciones

FossilOrigin-Name: afb6c19bd3d7f71dd3ceff6e95cc39aaf65d15707eef8535d092e68b34c05c65
---
 facho/facho.py              |  26 ++++++--
 facho/fe/nomina/__init__.py | 125 ++++++++++++++++++++++++++++++++++++
 tests/test_facho.py         |  11 ++++
 tests/test_nomina.py        |  16 +++++
 4 files changed, 173 insertions(+), 5 deletions(-)
 create mode 100644 facho/fe/nomina/__init__.py

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() == '<root><A><AA prueba="OK"/></A></root>'
     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() == '<root><A/></root>'
+    
+    xml.find_or_create_element('./A')
+    assert xml.exist_element('/root/A') == True
+    assert xml.tostring() == '<root><A/></root>'
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) == """<NominaIndividual xmlns:facho="http://git.disroot.org/Etrivial/facho" xmlns="http://www.dian.gov.co/contratos/facturaelectronica/v1" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cdt="urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1" xmlns:clm54217="urn:un:unece:uncefact:codelist:specification:54217:2001" xmlns:clmIANAMIMEMediaType="urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:qdt="urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2" xmlns:sts="dian:gov:co:facturaelectronica:Structures-2-1" xmlns:udt="urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:sig="http://www.w3.org/2000/09/xmldsig#"><Devengados><Basico/></Devengados><Deducciones><Salud Porcentaje="19.0" Deduccion="1000.0"/></Deducciones></NominaIndividual>"""
 
+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)
+