se adiciona archivo faltante de nomina, y se agregan mas deducciones

FossilOrigin-Name: afb6c19bd3d7f71dd3ceff6e95cc39aaf65d15707eef8535d092e68b34c05c65
This commit is contained in:
bit4bit 2021-11-05 02:09:21 +00:00
parent 3ef002e137
commit 74d98e249d
4 changed files with 173 additions and 5 deletions

View File

@ -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'])

125
facho/fe/nomina/__init__.py Normal file
View File

@ -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

View File

@ -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>'

View File

@ -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)