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: