se adiciona archivo faltante de nomina, y se agregan mas deducciones
FossilOrigin-Name: afb6c19bd3d7f71dd3ceff6e95cc39aaf65d15707eef8535d092e68b34c05c65
This commit is contained in:
		| @@ -117,8 +117,12 @@ class LXMLBuilder: | |||||||
|     def set_attribute(self,  elem, key, value): |     def set_attribute(self,  elem, key, value): | ||||||
|         elem.attrib[key] = value |         elem.attrib[key] = value | ||||||
|  |  | ||||||
|     def remove_attributes(self, elem, keys): |     @classmethod | ||||||
|  |     def remove_attributes(cls, elem, keys, exclude = []): | ||||||
|         for key in keys: |         for key in keys: | ||||||
|  |             if key in exclude: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 del elem.attrib[key] |                 del elem.attrib[key] | ||||||
|             except KeyError: |             except KeyError: | ||||||
| @@ -132,10 +136,8 @@ class LXMLBuilder: | |||||||
|         attrs['encoding'] = attrs.pop('encoding', 'UTF-8') |         attrs['encoding'] = attrs.pop('encoding', 'UTF-8') | ||||||
|  |  | ||||||
|         for el in elem.getiterator(): |         for el in elem.getiterator(): | ||||||
|             try: |             keys = filter(lambda key: key.startswith('facho_'), el.keys()) | ||||||
|                 del el.attrib['facho_placeholder'] |             self.remove_attributes(el, keys, exclude=['facho_optional']) | ||||||
|             except KeyError: |  | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|             is_optional = el.get('facho_optional', 'False') == 'True' |             is_optional = el.get('facho_optional', 'False') == 'True' | ||||||
|             if is_optional and el.getchildren() == [] and el.keys() == ['facho_optional']: |             if is_optional and el.getchildren() == [] and el.keys() == ['facho_optional']: | ||||||
| @@ -364,6 +366,20 @@ class FachoXML: | |||||||
|         text = self.builder.get_text(elem) |         text = self.builder.get_text(elem) | ||||||
|         return format_(text) |         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): |     def _remove_facho_attributes(self, elem): | ||||||
|         self.builder.remove_attributes(elem, ['facho_optional', 'facho_placeholder']) |         self.builder.remove_attributes(elem, ['facho_optional', 'facho_placeholder']) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										125
									
								
								facho/fe/nomina/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								facho/fe/nomina/__init__.py
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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.tostring() == '<root><A><AA prueba="OK"/></A></root>' | ||||||
|     assert xml.get_element_attribute('/root/A/AA', 'prueba') == 'OK' |     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>' | ||||||
|   | |||||||
| @@ -60,3 +60,19 @@ def test_adicionar_deduccion_salud(): | |||||||
|     print(xml) |     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>""" |     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) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user