diff --git a/facho/facho.py b/facho/facho.py index 97a564e..3c7edfc 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -353,7 +353,9 @@ class FachoXML: def get_element_attribute(self, xpath, attribute): elem = self.get_element(xpath) - print(elem.attrib) + if elem is None: + raise ValueError("xpath %s not found" % (xpath)) + return self.builder.get_attribute(elem, attribute) def get_element(self, xpath): diff --git a/facho/fe/nomina/__init__.py b/facho/fe/nomina/__init__.py index 3ff34d9..444b471 100644 --- a/facho/fe/nomina/__init__.py +++ b/facho/fe/nomina/__init__.py @@ -6,6 +6,7 @@ # creando las estructuras minimas necesaras. from dataclasses import dataclass +import hashlib from .. import fe from .. import form @@ -14,10 +15,114 @@ from .devengado import * from .deduccion import * from .amount import Amount +from .exception import * +@dataclass +class NumeroSecuencia: + numero: str -class DIANNominaIndividualError(Exception): - pass + def apply(self, fragment): + fragment.set_attributes('./NumeroSecuenciaXML', + Numero = self.numero) + +@dataclass +class InformacionGeneral: + class TIPO_AMBIENTE: + pass + + # TABLA 5.1.1 + @dataclass + class AMBIENTE_PRODUCCION(TIPO_AMBIENTE): + valor: str = '1' + @dataclass + class AMBIENTE_PRUEBAS(TIPO_AMBIENTE): + valor: str = '2' + + fecha_generacion: str + hora_generacion: str + tipo_ambiente: TIPO_AMBIENTE + software_pin: str + + def apply(self, fragment): + fragment.set_attributes('./InformacionGeneral', + # NIE022 + Version = 'V1.0: Documento Soporte de Pago de Nómina ElectrónicaV1.0', + # NIE023 + Ambiente = self.tipo_ambiente.valor, + # NIE202 + # TABLA 5.5.2 + # TODO(bit4bit) solo NominaIndividual + TipoXML = '102', + # NIE024 + CUNE = None, + # NIE025 + EncripCUNE = 'SHA-384', + # NIE026 + FechaGen = self.fecha_generacion, + # NIE027 + HoraGen = self.hora_generacion, + # TODO(bit4bit) resto... + # ..... + ) + + def post_apply(self, fexml, fragment): + devengados = map(lambda valor: Amount(valor), + [ + fexml.get_element_attribute('/fe:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') + ] + ) + devengados_total = Amount(0.0) + for devengado in devengados: + devengados_total += devengado + fexml.set_element('/fe:NominaIndividual/DevengadosTotal', round(devengados_total,2)) + + # TODO + fexml.set_element('/fe:NominaIndividual/DeduccionesTotal', '1000000.00') + + # TODO + fexml.set_element('/fe:NominaIndividual/ComprobanteTotal', '2500000.00') + + # generar cune + campos = [ + fexml.get_element_attribute('/fe:NominaIndividual/NumeroSecuenciaXML', 'Numero'), + fexml.get_element_attribute('/fe:NominaIndividual/InformacionGeneral', 'FechaGen'), + fexml.get_element_attribute('/fe:NominaIndividual/InformacionGeneral', 'HoraGen'), + fexml.get_element_text('/fe:NominaIndividual/DevengadosTotal'), + fexml.get_element_text('/fe:NominaIndividual/DeduccionesTotal'), + fexml.get_element_text('/fe:NominaIndividual/ComprobanteTotal'), + fexml.get_element_attribute('/fe:NominaIndividual/Empleador', 'NIT'), + fexml.get_element_attribute('/fe:NominaIndividual/Trabajador', 'NumeroDocumento'), + fexml.get_element_attribute('/fe:NominaIndividual/InformacionGeneral', 'TipoXML'), + self.software_pin, + fexml.get_element_attribute('/fe:NominaIndividual/InformacionGeneral', 'Ambiente') + ] + cune = "".join(campos) + print(cune) + h = hashlib.sha384() + h.update(cune.encode('utf-8')) + cune_hash = h.hexdigest() + + fragment.set_attributes( + './InformacionGeneral', + # NIE024 + CUNE = cune_hash + ) + +@dataclass +class Empleador: + nit: str + + def apply(self, fragment): + fragment.set_attributes('./Empleador', + NIT = self.nit) + +@dataclass +class Trabajador: + numero_documento: str + + def apply(self, fragment): + fragment.set_attributes('./Trabajador', + NumeroDocumento = self.numero_documento) class DIANNominaIndividual: def __init__(self): @@ -25,12 +130,44 @@ class DIANNominaIndividual: # layout, la dian requiere que los elementos # esten ordenados segun el anexo tecnico + self.fexml.placeholder_for('./NumeroSecuenciaXML') + self.fexml.placeholder_for('./InformacionGeneral') + self.fexml.placeholder_for('./Empleador') + self.fexml.placeholder_for('./Trabajador') self.fexml.placeholder_for('./Devengados/Basico') self.fexml.placeholder_for('./Devengados/Transporte', optional=True) + + self.informacion_general_xml = self.fexml.fragment('./InformacionGeneral') + self.numero_secuencia_xml = self.fexml.fragment('./NumeroSecuenciaXML') + self.empleador = self.fexml.fragment('./Empleador') + self.trabajador = self.fexml.fragment('./Trabajador') self.devengados = self.fexml.fragment('./Devengados') self.deducciones = self.fexml.fragment('./Deducciones') + self.informacion_general = None + + def asignar_numero_secuencia(self, secuencia): + if not isinstance(secuencia, NumeroSecuencia): + raise ValueError('se espera tipo NumeroSecuencia') + secuencia.apply(self.numero_secuencia_xml) + + def asignar_informacion_general(self, general): + if not isinstance(general, InformacionGeneral): + raise ValueError('se espera tipo InformacionGeneral') + self.informacion_general = general + self.informacion_general.apply(self.informacion_general_xml) + + def asignar_empleador(self, empleador): + if not isinstance(empleador, Empleador): + raise ValueError('se espera tipo Empleador') + empleador.apply(self.empleador) + + def asignar_trabajador(self, trabajador): + if not isinstance(trabajador, Trabajador): + raise ValueError('se espera tipo Trabajador') + trabajador.apply(self.trabajador) + def adicionar_devengado(self, devengado): if not isinstance(devengado, Devengado): raise ValueError('se espera tipo Devengado') @@ -83,4 +220,7 @@ class DIANNominaIndividual: return errors def toFachoXML(self): + if self.informacion_general is not None: + self.informacion_general.post_apply(self.fexml, self.informacion_general_xml) + return self.fexml diff --git a/facho/fe/nomina/devengado/basico.py b/facho/fe/nomina/devengado/basico.py index 30c95be..55f88aa 100644 --- a/facho/fe/nomina/devengado/basico.py +++ b/facho/fe/nomina/devengado/basico.py @@ -16,5 +16,5 @@ class DevengadoBasico(Devengado): # NIE069 DiasTrabajados = str(self.dias_trabajados), # NIE070 - SueldoTrabajado = str(self.sueldo_trabajado) + SueldoTrabajado = round(self.sueldo_trabajado, 2) ) diff --git a/tests/test_nomina.py b/tests/test_nomina.py index 516dba4..a6616a8 100644 --- a/tests/test_nomina.py +++ b/tests/test_nomina.py @@ -19,7 +19,7 @@ def test_adicionar_devengado_Basico(): xml = nomina.toFachoXML() assert xml.get_element_attribute('/fe:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30' - assert xml.get_element_attribute('/fe:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.0' + assert xml.get_element_attribute('/fe:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00' def test_adicionar_devengado_transporte(): nomina = fe.nomina.DIANNominaIndividual() @@ -45,7 +45,7 @@ def test_adicionar_devengado_transporte_muchos(): xml = nomina.toFachoXML() print(xml) - assert str(xml) == """""" + assert str(xml) == """""" def test_adicionar_deduccion_salud(): @@ -58,7 +58,7 @@ def test_adicionar_deduccion_salud(): xml = nomina.toFachoXML() print(xml) - assert str(xml) == """""" + assert str(xml) == """""" def test_nomina_obligatorios_segun_anexo_tecnico(): nomina = fe.nomina.DIANNominaIndividual() @@ -70,6 +70,36 @@ def test_nomina_obligatorios_segun_anexo_tecnico(): assert_error(errors, 'se requiere DeduccionSalud') assert_error(errors, 'se requiere DeduccionFondoPension') +def test_nomina_cune(): + nomina = fe.nomina.DIANNominaIndividual() + + nomina.asignar_numero_secuencia(fe.nomina.NumeroSecuencia( + numero = 'N00001' + )) + + nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( + fecha_generacion = '2020-01-16', + hora_generacion = '1053:10-05:00', + tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, + software_pin = '693' + )) + + nomina.asignar_empleador(fe.nomina.Empleador( + nit = '700085371', + )) + + nomina.asignar_trabajador(fe.nomina.Trabajador( + numero_documento = '800199436' + )) + + nomina.adicionar_devengado(fe.nomina.DevengadoBasico( + dias_trabajados = 60, + sueldo_trabajado = fe.nomina.Amount(3_500_000) + )) + + xml = nomina.toFachoXML() + assert xml.get_element_attribute('/fe:NominaIndividual/InformacionGeneral', 'CUNE') == '16560dc8956122e84ffb743c817fe7d494e058a44d9ca3fa4c234c268b4f766003253fbee7ea4af9682dd57210f3bac2' + def assert_error(errors, msg): for error in errors: if str(error) == msg: