Compare commits
4 Commits
main
...
feature/py
| Author | SHA1 | Date | |
|---|---|---|---|
| c4a29e4a6f | |||
| 4a7c2e2947 | |||
| 26ce89ca33 | |||
| 93a3d18e32 |
@@ -69,7 +69,7 @@ Ready to contribute? Here's how to set up `facho` for local development.
|
||||
$ python3 -mvenv facho-venv && source facho-venv/bin/activate
|
||||
$ cd facho/
|
||||
$ pre-commit install
|
||||
$ python setup.py develop
|
||||
$ pip install -e .
|
||||
|
||||
4. Create a branch for local development::
|
||||
|
||||
@@ -81,7 +81,7 @@ Ready to contribute? Here's how to set up `facho` for local development.
|
||||
tests, including testing other Python versions with tox::
|
||||
|
||||
$ flake8 facho tests
|
||||
$ python setup.py test or py.test
|
||||
$ py.test
|
||||
$ tox
|
||||
|
||||
To get flake8 and tox, just pip install them into your virtualenv.
|
||||
@@ -97,10 +97,10 @@ Ready to contribute? Here's how to set up `facho` for local development.
|
||||
Using docker
|
||||
------------
|
||||
|
||||
1. make -f Makefile.dev build
|
||||
1. make -f Makefile.dev dev-setup
|
||||
2. make -f Makefile.dev dev-shell
|
||||
3. make -f Makefile.dev python3.8 setup.py develop
|
||||
4. make -f Makefile.dev python3.8 setup.py test
|
||||
3. make -f Makefile.dev test
|
||||
4. make -f Makefile.dev tox
|
||||
|
||||
Pull Request Guidelines
|
||||
-----------------------
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -7,34 +7,45 @@ RUN apt install software-properties-common -y \
|
||||
&& add-apt-repository ppa:deadsnakes/ppa
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
python3.9 python3.9-distutils python3.9-dev \
|
||||
python3.10 python3.10-distutils python3.10-dev \
|
||||
python3.11 python3.11-distutils python3.11-dev \
|
||||
python3.12 python3-setuptools python3.12-dev \
|
||||
python3.13 python3-setuptools python3.13-dev \
|
||||
wget \
|
||||
ca-certificates
|
||||
|
||||
RUN wget https://bootstrap.pypa.io/get-pip.py \
|
||||
&& python3.9 get-pip.py pip==23.2.1 --break-system-packages \
|
||||
&& python3.10 get-pip.py pip==23.2.1 --break-system-packages \
|
||||
&& python3.11 get-pip.py pip==23.2.1 --break-system-packages \
|
||||
&& python3.12 get-pip.py pip==23.2.1 --break-system-packages \
|
||||
&& python3.13 get-pip.py pip==23.2.1 --break-system-packages \
|
||||
&& rm get-pip.py
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
build-essential \
|
||||
gcc \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
xmlsec1 \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
zip
|
||||
|
||||
RUN python3.9 --version
|
||||
RUN python3.10 --version
|
||||
RUN python3.11 --version
|
||||
RUN python3.12 --version
|
||||
RUN python3.13 --version
|
||||
|
||||
|
||||
RUN pip3.9 install setuptools setuptools-rust
|
||||
RUN pip3.10 install setuptools setuptools-rust
|
||||
RUN pip3.11 install setuptools setuptools-rust --break-system-packages
|
||||
RUN pip3.12 install setuptools setuptools-rust --break-system-packages
|
||||
RUN pip3.13 install setuptools setuptools-rust --break-system-packages
|
||||
|
||||
RUN pip3 install tox pytest --break-system-packages
|
||||
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml README.rst ./
|
||||
COPY facho ./facho
|
||||
RUN pip3.13 install -e . --break-system-packages
|
||||
|
||||
@@ -3,4 +3,4 @@ History
|
||||
=======
|
||||
|
||||
|
||||
* 0.2.1 version usada en produccion.
|
||||
* First release on PyPI.
|
||||
|
||||
5
Makefile
5
Makefile
@@ -80,9 +80,8 @@ release: dist ## package and upload a release
|
||||
twine upload dist/*
|
||||
|
||||
dist: clean ## builds source and wheel package
|
||||
python setup.py sdist
|
||||
python setup.py bdist_wheel
|
||||
python -m build
|
||||
ls -l dist
|
||||
|
||||
install: clean ## install the package to the active Python's site-packages
|
||||
python setup.py install
|
||||
pip install .
|
||||
|
||||
@@ -15,7 +15,7 @@ dev-shell:
|
||||
docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash
|
||||
|
||||
test:
|
||||
docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.12 setup.py test'
|
||||
docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.13 -m pytest -vv'
|
||||
|
||||
tox:
|
||||
docker run -it -v $(PWD)/:/app -w /app facho tox
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
# relative to the documentation root, use os.path.abspath to make it
|
||||
# absolute, like shown here.
|
||||
#
|
||||
import facho
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
import facho
|
||||
|
||||
# -- General configuration ---------------------------------------------
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Once you have a copy of the source, you can install it with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python setup.py install
|
||||
$ pip install .
|
||||
|
||||
|
||||
.. _Github repo: https://github.com/bit4bit/facho
|
||||
|
||||
@@ -11,13 +11,18 @@ from facho.fe import form_xml
|
||||
from facho.fe import fe
|
||||
|
||||
# importar otras necesarias
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime
|
||||
|
||||
# Datos del fomulario del SET de pruebas
|
||||
INVOICE_AUTHORIZATION = '181360000001' # Número suministrado por la Dian en el momento de la creación del SET de Pruebas
|
||||
ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas
|
||||
PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas
|
||||
CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por la Dian en el momento de la creación del SET de Pruebas
|
||||
# Número suministrado por la Dian en el momento de la creación del SET de
|
||||
# Pruebas
|
||||
INVOICE_AUTHORIZATION = '181360000001'
|
||||
# Id suministrado por la Dian en el momento de la creación del SET de Pruebas
|
||||
ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440'
|
||||
# Número creado por la empresa para poder crear el SET de pruebas
|
||||
PIN = '19642'
|
||||
# Id suministrado por la Dian en el momento de la creación del SET de Pruebas
|
||||
CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d'
|
||||
|
||||
|
||||
# callback que retonar las extensiones XML necesarias
|
||||
@@ -26,15 +31,24 @@ CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por
|
||||
# muchos de los valores usados son obtenidos
|
||||
# del servicio web de la DIAN.
|
||||
def extensions(inv):
|
||||
security_code = fe.DianXMLExtensionSoftwareSecurityCode(ID_SOFTWARE, PIN, inv.invoice_ident)
|
||||
security_code = fe.DianXMLExtensionSoftwareSecurityCode(
|
||||
ID_SOFTWARE, PIN, inv.invoice_ident)
|
||||
authorization_provider = fe.DianXMLExtensionAuthorizationProvider()
|
||||
cufe = fe.DianXMLExtensionCUFE(inv, CLAVE_TECNICA, fe.AMBIENTE_PRUEBAS)
|
||||
software_provider = fe.DianXMLExtensionSoftwareProvider('nit_empresa', 'dígito_verificación', ID_SOFTWARE)
|
||||
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(INVOICE_AUTHORIZATION,
|
||||
datetime(2019, 1, 19),#Datos toamdos de
|
||||
datetime(2030, 1, 19),#la configuración
|
||||
'SETP', 990000000, 995000000)#del SET de pruebas
|
||||
return [security_code, authorization_provider, cufe, software_provider, inv_authorization]
|
||||
software_provider = fe.DianXMLExtensionSoftwareProvider(
|
||||
'nit_empresa', 'dígito_verificación', ID_SOFTWARE)
|
||||
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(
|
||||
INVOICE_AUTHORIZATION,
|
||||
# Datos tomados de la configuración
|
||||
datetime(2019, 1, 19),
|
||||
datetime(2030, 1, 19),
|
||||
'SETP', 990000000, 995000000) # del SET de pruebas
|
||||
return [
|
||||
security_code,
|
||||
authorization_provider,
|
||||
cufe,
|
||||
software_provider,
|
||||
inv_authorization]
|
||||
|
||||
|
||||
def invoice():
|
||||
@@ -49,7 +63,7 @@ def invoice():
|
||||
# asignar tipo de operacion ver DIAN:6.1.5
|
||||
inv.set_operation_type('10')
|
||||
inv.set_supplier(form.Party(
|
||||
legal_name = 'Nombre registrado de la empresa',
|
||||
legal_name='Nombre registrado de la empresa',
|
||||
name='Nombre comercial o él mismo nombre registrado',
|
||||
ident=form.PartyIdentification(
|
||||
'nit_empresa', 'digito_verificación', '31'),
|
||||
@@ -65,22 +79,22 @@ def invoice():
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia')),
|
||||
))
|
||||
#Tercero a quien se le factura
|
||||
# Tercero a quien se le factura
|
||||
inv.set_customer(form.Party(
|
||||
legal_name = 'consumidor final',
|
||||
name = 'consumidor final',
|
||||
ident = form.PartyIdentification('222222222222', '', '13'),
|
||||
responsability_code = form.Responsability(['R-99-PN']),
|
||||
responsability_regime_code = '49',
|
||||
organization_code = '2',
|
||||
email = "consumidor_final0final.final",
|
||||
address = form.Address(
|
||||
legal_name='consumidor final',
|
||||
name='consumidor final',
|
||||
ident=form.PartyIdentification('222222222222', '', '13'),
|
||||
responsability_code=form.Responsability(['R-99-PN']),
|
||||
responsability_regime_code='49',
|
||||
organization_code='2',
|
||||
email="consumidor_final0final.final",
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia')),
|
||||
# tax_scheme = form.TaxScheme('01', 'IVA')
|
||||
))
|
||||
# asignar metodo de pago
|
||||
# asignar metodo de pago
|
||||
inv.set_payment_mean(form.PaymentMean(
|
||||
# metodo de pago ver DIAN:3.4.1
|
||||
id='1',
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
|
||||
from facho import fe
|
||||
|
||||
|
||||
def extensions(nomina):
|
||||
return []
|
||||
|
||||
|
||||
def nomina():
|
||||
nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
nomina.asignar_metadata(fe.nomina.Metadata(
|
||||
secuencia=fe.nomina.NumeroSecuencia(
|
||||
numero = 'N00001',
|
||||
numero='N00001',
|
||||
consecutivo=232
|
||||
),
|
||||
lugar_generacion=fe.nomina.Lugar(
|
||||
pais = fe.nomina.Pais(
|
||||
code = 'CO'
|
||||
pais=fe.nomina.Pais(
|
||||
code='CO'
|
||||
),
|
||||
departamento = fe.nomina.Departamento(
|
||||
code = '05'
|
||||
departamento=fe.nomina.Departamento(
|
||||
code='05'
|
||||
),
|
||||
municipio = fe.nomina.Municipio(
|
||||
code = '05001'
|
||||
municipio=fe.nomina.Municipio(
|
||||
code='05001'
|
||||
),
|
||||
),
|
||||
proveedor=fe.nomina.Proveedor(
|
||||
@@ -32,62 +34,62 @@ def nomina():
|
||||
))
|
||||
|
||||
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',
|
||||
periodo_nomina = fe.nomina.PeriodoNomina(code='1'),
|
||||
tipo_moneda = fe.nomina.TipoMoneda(code='COP')
|
||||
fecha_generacion='2020-01-16',
|
||||
hora_generacion='1053:10-05:00',
|
||||
tipo_ambiente=fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION,
|
||||
software_pin='693',
|
||||
periodo_nomina=fe.nomina.PeriodoNomina(code='1'),
|
||||
tipo_moneda=fe.nomina.TipoMoneda(code='COP')
|
||||
))
|
||||
|
||||
nomina.asignar_empleador(fe.nomina.Empleador(
|
||||
nit = '700085371',
|
||||
dv = '1',
|
||||
pais = fe.nomina.Pais(
|
||||
code = 'CO'
|
||||
nit='700085371',
|
||||
dv='1',
|
||||
pais=fe.nomina.Pais(
|
||||
code='CO'
|
||||
),
|
||||
departamento = fe.nomina.Departamento(
|
||||
code = '05'
|
||||
departamento=fe.nomina.Departamento(
|
||||
code='05'
|
||||
),
|
||||
municipio = fe.nomina.Municipio(
|
||||
code = '05001'
|
||||
municipio=fe.nomina.Municipio(
|
||||
code='05001'
|
||||
),
|
||||
direccion = 'calle etrivial'
|
||||
direccion='calle etrivial'
|
||||
))
|
||||
|
||||
nomina.asignar_trabajador(fe.nomina.Trabajador(
|
||||
tipo_contrato = fe.nomina.TipoContrato(
|
||||
code = '1'
|
||||
tipo_contrato=fe.nomina.TipoContrato(
|
||||
code='1'
|
||||
),
|
||||
alto_riesgo = False,
|
||||
tipo_documento = fe.nomina.TipoDocumento(
|
||||
code = '11'
|
||||
alto_riesgo=False,
|
||||
tipo_documento=fe.nomina.TipoDocumento(
|
||||
code='11'
|
||||
),
|
||||
primer_apellido = 'gnu',
|
||||
segundo_apellido = 'emacs',
|
||||
primer_nombre = 'facho',
|
||||
lugar_trabajo = fe.nomina.LugarTrabajo(
|
||||
pais = fe.nomina.Pais(code='CO'),
|
||||
departamento = fe.nomina.Departamento(code='05'),
|
||||
municipio = fe.nomina.Municipio(code='05001'),
|
||||
direccion = 'calle facho'
|
||||
primer_apellido='gnu',
|
||||
segundo_apellido='emacs',
|
||||
primer_nombre='facho',
|
||||
lugar_trabajo=fe.nomina.LugarTrabajo(
|
||||
pais=fe.nomina.Pais(code='CO'),
|
||||
departamento=fe.nomina.Departamento(code='05'),
|
||||
municipio=fe.nomina.Municipio(code='05001'),
|
||||
direccion='calle facho'
|
||||
),
|
||||
numero_documento = '800199436',
|
||||
tipo = fe.nomina.TipoTrabajador(
|
||||
code = '01'
|
||||
numero_documento='800199436',
|
||||
tipo=fe.nomina.TipoTrabajador(
|
||||
code='01'
|
||||
),
|
||||
salario_integral = True,
|
||||
sueldo = fe.nomina.Amount(1_500_000)
|
||||
salario_integral=True,
|
||||
sueldo=fe.nomina.Amount(1_500_000)
|
||||
))
|
||||
|
||||
nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
|
||||
dias_trabajados = 60,
|
||||
sueldo_trabajado = fe.nomina.Amount(3_500_000)
|
||||
dias_trabajados=60,
|
||||
sueldo_trabajado=fe.nomina.Amount(3_500_000)
|
||||
))
|
||||
|
||||
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
|
||||
porcentaje = fe.nomina.Amount(19),
|
||||
deduccion = fe.nomina.Amount(1_000_000)
|
||||
porcentaje=fe.nomina.Amount(19),
|
||||
deduccion=fe.nomina.Amount(1_000_000)
|
||||
))
|
||||
|
||||
return nomina
|
||||
|
||||
446
facho/cli.py
446
facho/cli.py
@@ -1,39 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import base64
|
||||
import warnings
|
||||
|
||||
import click
|
||||
|
||||
import logging.config
|
||||
|
||||
logging.config.dictConfig({
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '%(name)s: %(message)s'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
logging.config.dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"formatters": {"verbose": {"format": "%(name)s: %(message)s"}},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'zeep.transports': {
|
||||
'level': 'DEBUG',
|
||||
'propagate': True,
|
||||
'handlers': ['console'],
|
||||
"loggers": {
|
||||
"zeep.transports": {
|
||||
"level": "DEBUG",
|
||||
"propagate": True,
|
||||
"handlers": ["console"],
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
def disable_ssl():
|
||||
# MACHETE
|
||||
import ssl
|
||||
if getattr(ssl, '_create_unverified_context', None):
|
||||
|
||||
if getattr(ssl, "_create_unverified_context", None):
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
warnings.warn("be sure!! ssl disable")
|
||||
else:
|
||||
@@ -41,99 +39,122 @@ def disable_ssl():
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--nit', required=True)
|
||||
@click.option('--nit-proveedor', required=True)
|
||||
@click.option('--id-software', required=True)
|
||||
@click.option('--username', required=True)
|
||||
@click.option('--password', required=True)
|
||||
def consultaResolucionesFacturacion(nit, nit_proveedor, id_software, username, password):
|
||||
@click.option("--nit", required=True)
|
||||
@click.option("--nit-proveedor", required=True)
|
||||
@click.option("--id-software", required=True)
|
||||
@click.option("--username", required=True)
|
||||
@click.option("--password", required=True)
|
||||
def consultaResolucionesFacturacion(
|
||||
nit, nit_proveedor, id_software, username, password
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
client_dian = dian.DianClient(username,
|
||||
password)
|
||||
resp = client_dian.request(dian.ConsultaResolucionesFacturacionPeticion(
|
||||
nit, nit_proveedor, id_software
|
||||
))
|
||||
|
||||
client_dian = dian.DianClient(username, password)
|
||||
resp = client_dian.request(
|
||||
dian.ConsultaResolucionesFacturacionPeticion(
|
||||
nit, nit_proveedor, id_software
|
||||
)
|
||||
)
|
||||
print(str(resp))
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.option('--test-setid', required=True)
|
||||
@click.argument('filename', required=True)
|
||||
@click.argument('zipfile', type=click.Path(exists=True))
|
||||
def soap_send_test_set_async(private_key, public_key, habilitacion, password, test_setid, filename, zipfile):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.option("--test-setid", required=True)
|
||||
@click.argument("filename", required=True)
|
||||
@click.argument("zipfile", type=click.Path(exists=True))
|
||||
def soap_send_test_set_async(
|
||||
private_key,
|
||||
public_key,
|
||||
habilitacion,
|
||||
password,
|
||||
test_setid,
|
||||
filename,
|
||||
zipfile,
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.SendTestSetAsync
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.SendTestSetAsync
|
||||
resp = client.request(req(
|
||||
filename,
|
||||
open(zipfile, 'rb').read(),
|
||||
test_setid,
|
||||
))
|
||||
resp = client.request(
|
||||
req(
|
||||
filename,
|
||||
open(zipfile, "rb").read(),
|
||||
test_setid,
|
||||
)
|
||||
)
|
||||
print(resp)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.argument('filename', required=True)
|
||||
@click.argument('zipfile', type=click.Path(exists=True))
|
||||
def soap_send_bill_async(private_key, public_key, habilitacion, password, filename, zipfile):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.argument("filename", required=True)
|
||||
@click.argument("zipfile", type=click.Path(exists=True))
|
||||
def soap_send_bill_async(
|
||||
private_key, public_key, habilitacion, password, filename, zipfile
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.SendBillAsync
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.SendBillAsync
|
||||
resp = client.request(req(
|
||||
filename,
|
||||
open(zipfile, 'rb').read()
|
||||
))
|
||||
resp = client.request(req(filename, open(zipfile, "rb").read()))
|
||||
print(resp)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.argument('filename', required=True)
|
||||
@click.argument('zipfile', type=click.Path(exists=True))
|
||||
def soap_send_bill_sync(private_key, public_key, habilitacion, password, filename, zipfile):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.argument("filename", required=True)
|
||||
@click.argument("zipfile", type=click.Path(exists=True))
|
||||
def soap_send_bill_sync(
|
||||
private_key, public_key, habilitacion, password, filename, zipfile
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.SendBillSync
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.SendBillSync
|
||||
resp = client.request(req(
|
||||
filename,
|
||||
open(zipfile, 'rb').read()
|
||||
))
|
||||
resp = client.request(req(filename, open(zipfile, "rb").read()))
|
||||
print(resp)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.option('--track-id', required=True)
|
||||
def soap_get_status_zip(private_key, public_key, habilitacion, password, track_id):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.option("--track-id", required=True)
|
||||
def soap_get_status_zip(
|
||||
private_key, public_key, habilitacion, password, track_id
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.GetStatusZip
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.GetStatusZip
|
||||
resp = client.request(req(
|
||||
trackId = track_id
|
||||
))
|
||||
resp = client.request(req(trackId=track_id))
|
||||
|
||||
print("StatusCode:", resp.StatusCode)
|
||||
print("StatusDescription:", resp.StatusDescription)
|
||||
@@ -143,68 +164,78 @@ def soap_get_status_zip(private_key, public_key, habilitacion, password, track_i
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.option('--track-id', required=True)
|
||||
def soap_get_status(private_key, public_key, habilitacion, password, track_id):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.option("--track-id", required=True)
|
||||
def soap_get_status(
|
||||
private_key, public_key, habilitacion, password, track_id
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.GetStatus
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.GetStatus
|
||||
resp = client.request(req(
|
||||
trackId = track_id
|
||||
))
|
||||
resp = client.request(req(trackId=track_id))
|
||||
print(resp)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.option('--nit', required=True)
|
||||
@click.option('--nit-proveedor', required=True)
|
||||
@click.option('--id-software', required=True)
|
||||
def soap_get_numbering_range(private_key,
|
||||
public_key,
|
||||
habilitacion,
|
||||
password,
|
||||
nit, nit_proveedor, id_software):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.option("--nit", required=True)
|
||||
@click.option("--nit-proveedor", required=True)
|
||||
@click.option("--id-software", required=True)
|
||||
def soap_get_numbering_range(
|
||||
private_key,
|
||||
public_key,
|
||||
habilitacion,
|
||||
password,
|
||||
nit,
|
||||
nit_proveedor,
|
||||
id_software,
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.GetNumberingRange
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.GetNumberingRange
|
||||
resp = client.request(req(
|
||||
nit, nit_proveedor, id_software
|
||||
))
|
||||
resp = client.request(req(nit, nit_proveedor, id_software))
|
||||
print(resp)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument('invoice_path')
|
||||
@click.argument("invoice_path")
|
||||
def validate_invoice(invoice_path):
|
||||
warnings.warn("!! NO APROBADO FUNCIONAMIENTO")
|
||||
|
||||
from facho.fe.data.dian import XSD
|
||||
content = open(invoice_path, 'r').read()
|
||||
|
||||
content = open(invoice_path, "r").read()
|
||||
# TODO donde ubicar esta responsabilidad?
|
||||
# esto es requerido por el XSD de la DIAN
|
||||
content = content.replace(
|
||||
'xmlns:fe="http://www.dian.gov.co/contratos/facturaelectronica/v1"',
|
||||
'xmlns:fe="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"'
|
||||
'xmlns:fe="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"',
|
||||
)
|
||||
XSD.validate(content, XSD.UBLInvoice)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument('nomina_path')
|
||||
@click.argument("nomina_path")
|
||||
def validate_nominaindividual(nomina_path):
|
||||
from facho.fe.data.dian import XSD
|
||||
content = open(nomina_path, 'r').read()
|
||||
|
||||
content = open(nomina_path, "r").read()
|
||||
content = content.replace(
|
||||
'xmlns="http://www.dian.gov.co/contratos/facturaelectronica/v1"',
|
||||
'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"',
|
||||
@@ -213,35 +244,55 @@ def validate_nominaindividual(nomina_path):
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', type=click.Path(exists=True))
|
||||
@click.option('--passphrase')
|
||||
@click.option('--ssl/--no-ssl', default=False)
|
||||
@click.option('--use-cache-policy/--no-use-cache-policy', default=False)
|
||||
@click.argument('xmlfile', type=click.Path(exists=True), required=True)
|
||||
@click.argument('output', required=True)
|
||||
def sign_xml(private_key, passphrase, xmlfile, ssl=True, use_cache_policy=False, output=None):
|
||||
@click.option("--private-key", type=click.Path(exists=True))
|
||||
@click.option("--passphrase")
|
||||
@click.option("--ssl/--no-ssl", default=False)
|
||||
@click.option("--use-cache-policy/--no-use-cache-policy", default=False)
|
||||
@click.argument("xmlfile", type=click.Path(exists=True), required=True)
|
||||
@click.argument("output", required=True)
|
||||
def sign_xml(
|
||||
private_key,
|
||||
passphrase,
|
||||
xmlfile,
|
||||
ssl=True,
|
||||
use_cache_policy=False,
|
||||
output=None,
|
||||
):
|
||||
if not ssl:
|
||||
disable_ssl()
|
||||
|
||||
from facho import fe
|
||||
|
||||
if use_cache_policy:
|
||||
warnings.warn("xades using cache policy")
|
||||
|
||||
signer = fe.DianXMLExtensionSigner(private_key, passphrase=passphrase, localpolicy=use_cache_policy)
|
||||
document = open(xmlfile, 'r').read().encode('utf-8')
|
||||
with open(output, 'w') as f:
|
||||
signer = fe.DianXMLExtensionSigner(
|
||||
private_key, passphrase=passphrase, localpolicy=use_cache_policy
|
||||
)
|
||||
document = open(xmlfile, "r").read().encode("utf-8")
|
||||
with open(output, "w") as f:
|
||||
f.write(signer.sign_xml_string(document))
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', type=click.Path(exists=True))
|
||||
@click.option('--generate/--validate', default=False)
|
||||
@click.option('--passphrase')
|
||||
@click.option('--ssl/--no-ssl', default=False)
|
||||
@click.option('--sign/--no-sign', default=False)
|
||||
@click.option('--use-cache-policy/--no-use-cache-policy', default=False)
|
||||
@click.argument('scriptname', type=click.Path(exists=True), required=True)
|
||||
@click.argument('output', required=True)
|
||||
def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=True, sign=False, use_cache_policy=False, output=None):
|
||||
@click.option("--private-key", type=click.Path(exists=True))
|
||||
@click.option("--generate/--validate", default=False)
|
||||
@click.option("--passphrase")
|
||||
@click.option("--ssl/--no-ssl", default=False)
|
||||
@click.option("--sign/--no-sign", default=False)
|
||||
@click.option("--use-cache-policy/--no-use-cache-policy", default=False)
|
||||
@click.argument("scriptname", type=click.Path(exists=True), required=True)
|
||||
@click.argument("output", required=True)
|
||||
def generate_invoice(
|
||||
private_key,
|
||||
passphrase,
|
||||
scriptname,
|
||||
generate=False,
|
||||
ssl=True,
|
||||
sign=False,
|
||||
use_cache_policy=False,
|
||||
output=None,
|
||||
):
|
||||
"""
|
||||
imprime xml en pantalla.
|
||||
SCRIPTNAME espera
|
||||
@@ -254,19 +305,21 @@ def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=Tr
|
||||
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location('invoice', scriptname)
|
||||
spec = importlib.util.spec_from_file_location("invoice", scriptname)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
import facho.fe.form as form
|
||||
from facho.fe.form_xml import DIANInvoiceXML, DIANWriteSigned, DIANWrite, DIANSupportDocumentXML
|
||||
from facho import fe
|
||||
from facho.fe.form_xml import (
|
||||
DIANWriteSigned,
|
||||
DIANWrite,
|
||||
DIANSupportDocumentXML,
|
||||
)
|
||||
|
||||
try:
|
||||
invoice_xml = module.document_xml()
|
||||
except AttributeError:
|
||||
#invoice_xml = DIANInvoiceXML
|
||||
invoice_xml = DIANSupportDocumentXML
|
||||
# invoice_xml = DIANInvoiceXML
|
||||
invoice_xml = DIANSupportDocumentXML
|
||||
print("Using document xml:", invoice_xml)
|
||||
invoice = module.invoice()
|
||||
invoice.calculate()
|
||||
@@ -279,24 +332,39 @@ def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=Tr
|
||||
xml.add_extension(extension)
|
||||
|
||||
if sign:
|
||||
DIANWriteSigned(xml, output, private_key, passphrase, use_cache_policy)
|
||||
DIANWriteSigned(
|
||||
xml, output, private_key, passphrase, use_cache_policy
|
||||
)
|
||||
else:
|
||||
DIANWrite(xml, output)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', type=click.Path(exists=True))
|
||||
@click.option('--passphrase')
|
||||
@click.option('--ssl/--no-ssl', default=False)
|
||||
@click.option('--sign/--no-sign', default=False)
|
||||
@click.option('--use-cache-policy/--no-use-cache-policy', default=False)
|
||||
@click.argument('scriptname', type=click.Path(exists=True), required=True)
|
||||
@click.argument('output', required=True)
|
||||
def generate_nomina(private_key, passphrase, scriptname, ssl=True, sign=False, use_cache_policy=False, output=None):
|
||||
@click.option("--private-key", type=click.Path(exists=True))
|
||||
@click.option("--passphrase")
|
||||
@click.option("--ssl/--no-ssl", default=False)
|
||||
@click.option("--sign/--no-sign", default=False)
|
||||
@click.option("--use-cache-policy/--no-use-cache-policy", default=False)
|
||||
@click.argument("scriptname", type=click.Path(exists=True), required=True)
|
||||
@click.argument("output", required=True)
|
||||
def generate_nomina(
|
||||
private_key,
|
||||
passphrase,
|
||||
scriptname,
|
||||
ssl=True,
|
||||
sign=False,
|
||||
use_cache_policy=False,
|
||||
output=None,
|
||||
):
|
||||
"""
|
||||
imprime xml en pantalla.
|
||||
SCRIPTNAME espera
|
||||
def nomina() -> fe.nomina.NominaIndividual
|
||||
def extensions(fe.nomina.NominaIndividual): -> List[facho.FachoXMLExtension]
|
||||
def nomina() -> (
|
||||
fe.nomina.NominaIndividual
|
||||
)
|
||||
def extensions(
|
||||
fe.nomina.NominaIndividual
|
||||
): -> List[facho.FachoXMLExtension]
|
||||
"""
|
||||
|
||||
if not ssl:
|
||||
@@ -304,7 +372,7 @@ def generate_nomina(private_key, passphrase, scriptname, ssl=True, sign=False, u
|
||||
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location('nomina', scriptname)
|
||||
spec = importlib.util.spec_from_file_location("nomina", scriptname)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
@@ -320,59 +388,83 @@ def generate_nomina(private_key, passphrase, scriptname, ssl=True, sign=False, u
|
||||
xml.add_extension(extension)
|
||||
|
||||
if sign:
|
||||
DIANWriteSigned(xml, output, private_key, passphrase, use_cache_policy, dian_signer=facho.fe.nomina.DianXMLExtensionSigner)
|
||||
DIANWriteSigned(
|
||||
xml,
|
||||
output,
|
||||
private_key,
|
||||
passphrase,
|
||||
use_cache_policy,
|
||||
dian_signer=facho.fe.nomina.DianXMLExtensionSigner,
|
||||
)
|
||||
else:
|
||||
DIANWrite(xml, output)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', required=True)
|
||||
@click.option('--public-key', required=True)
|
||||
@click.option('--habilitacion/--produccion', default=False)
|
||||
@click.option('--password')
|
||||
@click.argument('filename', required=True)
|
||||
@click.argument('zipfile', type=click.Path(exists=True))
|
||||
def soap_send_nomina_sync(private_key, public_key, habilitacion, password, filename, zipfile):
|
||||
@click.option("--private-key", required=True)
|
||||
@click.option("--public-key", required=True)
|
||||
@click.option("--habilitacion/--produccion", default=False)
|
||||
@click.option("--password")
|
||||
@click.argument("filename", required=True)
|
||||
@click.argument("zipfile", type=click.Path(exists=True))
|
||||
def soap_send_nomina_sync(
|
||||
private_key, public_key, habilitacion, password, filename, zipfile
|
||||
):
|
||||
from facho.fe.client import dian
|
||||
|
||||
client = dian.DianSignatureClient(private_key, public_key, password=password)
|
||||
client = dian.DianSignatureClient(
|
||||
private_key, public_key, password=password
|
||||
)
|
||||
req = dian.SendNominaSync
|
||||
if habilitacion:
|
||||
req = dian.Habilitacion.SendNominaSync
|
||||
resp = client.request(req(
|
||||
open(zipfile, 'rb').read()
|
||||
))
|
||||
resp = client.request(req(open(zipfile, "rb").read()))
|
||||
print(resp)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--private-key', type=click.Path(exists=True))
|
||||
@click.option('--passphrase')
|
||||
@click.option('--ssl/--no-ssl', default=False)
|
||||
@click.option('--use-cache-policy/--no-use-cache-policy', default=False)
|
||||
@click.argument('xmlfile', type=click.Path(exists=True), required=True)
|
||||
def sign_verify_xml(private_key, passphrase, xmlfile, ssl=True, use_cache_policy=False, output=None):
|
||||
@click.option("--private-key", type=click.Path(exists=True))
|
||||
@click.option("--passphrase")
|
||||
@click.option("--ssl/--no-ssl", default=False)
|
||||
@click.option("--use-cache-policy/--no-use-cache-policy", default=False)
|
||||
@click.argument("xmlfile", type=click.Path(exists=True), required=True)
|
||||
def sign_verify_xml(
|
||||
private_key,
|
||||
passphrase,
|
||||
xmlfile,
|
||||
ssl=True,
|
||||
use_cache_policy=False,
|
||||
output=None,
|
||||
):
|
||||
if not ssl:
|
||||
disable_ssl()
|
||||
|
||||
from facho.fe import fe
|
||||
|
||||
if use_cache_policy:
|
||||
warnings.warn("xades using cache policy")
|
||||
|
||||
print("THIS ONLY WORKS FOR DOCUMENTS GENERATE WITH FACHO")
|
||||
signer = fe.DianXMLExtensionSignerVerifier(private_key, passphrase=passphrase, localpolicy=use_cache_policy)
|
||||
document = open(xmlfile, 'r').read().encode('utf-8')
|
||||
signer = fe.DianXMLExtensionSignerVerifier(
|
||||
private_key, passphrase=passphrase, localpolicy=use_cache_policy
|
||||
)
|
||||
document = open(xmlfile, "r").read().encode("utf-8")
|
||||
|
||||
if signer.verify_string(document):
|
||||
print("+OK")
|
||||
else:
|
||||
print("-INVALID")
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--software-id')
|
||||
@click.option('--software-pin')
|
||||
@click.option('--nit')
|
||||
@click.option('--dv')
|
||||
@click.option('--output-zippath')
|
||||
def generate_nomina_habilitacion(software_id, software_pin, nit, dv, output_zippath):
|
||||
@click.option("--software-id")
|
||||
@click.option("--software-pin")
|
||||
@click.option("--nit")
|
||||
@click.option("--dv")
|
||||
@click.option("--output-zippath")
|
||||
def generate_nomina_habilitacion(
|
||||
software_id, software_pin, nit, dv, output_zippath
|
||||
):
|
||||
from facho import fe
|
||||
|
||||
generador = fe.nomina.habilitacion.Habilitacion(
|
||||
@@ -380,15 +472,17 @@ def generate_nomina_habilitacion(software_id, software_pin, nit, dv, output_zipp
|
||||
software_id=software_id,
|
||||
software_pin=software_pin,
|
||||
nit=nit,
|
||||
dv=dv
|
||||
dv=dv,
|
||||
)
|
||||
)
|
||||
generador.generar(output_zippath)
|
||||
|
||||
|
||||
@click.group()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
main.add_command(consultaResolucionesFacturacion)
|
||||
main.add_command(soap_send_test_set_async)
|
||||
main.add_command(soap_send_bill_async)
|
||||
|
||||
101
facho/facho.py
101
facho/facho.py
@@ -119,7 +119,7 @@ class LXMLBuilder:
|
||||
def is_attribute(self, elem, key, value):
|
||||
return elem.get(key, False) == value
|
||||
|
||||
def set_attribute(self, elem, key, value):
|
||||
def set_attribute(self, elem, key, value):
|
||||
elem.attrib[key] = value
|
||||
|
||||
@classmethod
|
||||
@@ -140,12 +140,12 @@ class LXMLBuilder:
|
||||
attrs['pretty_print'] = attrs.pop('pretty_print', False)
|
||||
attrs['encoding'] = attrs.pop('encoding', 'UTF-8')
|
||||
|
||||
for el in elem.getiterator():
|
||||
for el in elem.iter():
|
||||
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() == [
|
||||
if is_optional and list(el) == [] and el.keys() == [
|
||||
'facho_optional']:
|
||||
el.getparent().remove(el)
|
||||
|
||||
@@ -156,6 +156,7 @@ class FachoXML:
|
||||
"""
|
||||
Decora XML con funciones de consulta XPATH de un solo elemento
|
||||
"""
|
||||
|
||||
def __init__(self, root, builder=None, nsmap=None, fragment_prefix='',
|
||||
fragment_root_element=None):
|
||||
if builder is None:
|
||||
@@ -291,7 +292,7 @@ class FachoXML:
|
||||
# se fuerza la adicion como un nuevo elemento
|
||||
if append:
|
||||
last_slibing = None
|
||||
for child in parent.getchildren():
|
||||
for child in list(parent):
|
||||
if child.tag == node_tag:
|
||||
last_slibing = child
|
||||
|
||||
@@ -389,12 +390,94 @@ class FachoXML:
|
||||
|
||||
def get_element(self, xpath, multiple=False):
|
||||
xpath = self.fragment_prefix + self._path_xpath_for(xpath)
|
||||
elem = self.builder.xpath(self.root, xpath)
|
||||
if elem is None:
|
||||
raise AttributeError('xpath %s invalid' % (xpath))
|
||||
return self.builder.xpath(self.root, xpath, multiple=multiple)
|
||||
|
||||
text = self.builder.get_text(elem)
|
||||
return str(text)
|
||||
def get_element_text(self, xpath, format_=str, multiple=False):
|
||||
xpath = self.fragment_prefix + self._path_xpath_for(xpath)
|
||||
# MACHETE(bit4bit) al usar ./ queda ../
|
||||
xpath = re.sub(r'^\.\.+', '.', xpath)
|
||||
|
||||
elem = self.builder.xpath(self.root, xpath, multiple=multiple)
|
||||
if multiple:
|
||||
vals = []
|
||||
for e in elem:
|
||||
text = self.builder.get_text(e)
|
||||
if text is not None:
|
||||
vals.append(format_(text))
|
||||
return vals
|
||||
else:
|
||||
text = self.builder.get_text(elem)
|
||||
if text is None:
|
||||
return None
|
||||
return format_(text)
|
||||
|
||||
def get_element_text_or_attribute(
|
||||
self, xpath, default=None, multiple=False, raise_on_fail=False):
|
||||
parts = xpath.split('/')
|
||||
is_attribute = parts[-1].startswith('@')
|
||||
if is_attribute:
|
||||
attribute_name = parts.pop(-1).lstrip('@')
|
||||
element_path = "/".join(parts)
|
||||
try:
|
||||
val = self.get_element_attribute(
|
||||
element_path, attribute_name, multiple=multiple)
|
||||
if val is None:
|
||||
return default
|
||||
return val
|
||||
except KeyError as e:
|
||||
if raise_on_fail:
|
||||
raise e
|
||||
return default
|
||||
except ValueError as e:
|
||||
if raise_on_fail:
|
||||
raise e
|
||||
return default
|
||||
else:
|
||||
try:
|
||||
val = self.get_element_text(xpath, multiple=multiple)
|
||||
if val is None:
|
||||
return default
|
||||
return val
|
||||
except ValueError as e:
|
||||
if raise_on_fail:
|
||||
raise e
|
||||
return default
|
||||
|
||||
def get_elements_text_or_attributes(self, xpaths, raise_on_fail=True):
|
||||
"""
|
||||
returna el contenido o attributos de un conjunto de XPATHS
|
||||
si algun XPATH es una tupla se retorna el primer elemento del mismo.
|
||||
"""
|
||||
vals = []
|
||||
for xpath in xpaths:
|
||||
if isinstance(xpath, tuple):
|
||||
val = xpath[0]
|
||||
else:
|
||||
val = self.get_element_text_or_attribute(
|
||||
xpath, raise_on_fail=raise_on_fail)
|
||||
vals.append(val)
|
||||
return vals
|
||||
|
||||
def exist_element(self, xpath):
|
||||
elem = self.get_element(xpath)
|
||||
|
||||
# no se encontro elemento
|
||||
if elem is None:
|
||||
return False
|
||||
|
||||
# el placeholder no ha sido populado
|
||||
if elem.get('facho_placeholder') == 'True':
|
||||
return False
|
||||
|
||||
# el valor opcional no ha sido populado
|
||||
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'])
|
||||
|
||||
def tostring(self, **kw):
|
||||
return self.builder.tostring(self.root, **kw)
|
||||
|
||||
@@ -14,3 +14,22 @@ from .fe import AMBIENTE_PRUEBAS
|
||||
from .fe import AMBIENTE_PRODUCCION
|
||||
from . import form_xml
|
||||
from . import nomina
|
||||
|
||||
__all__ = [
|
||||
'FeXML',
|
||||
'fe_from_string',
|
||||
'NAMESPACES',
|
||||
'DianXMLExtensionSigner',
|
||||
'DianXMLExtensionSoftwareSecurityCode',
|
||||
'DianXMLExtensionCUFE',
|
||||
'DianXMLExtensionCUDE',
|
||||
'DianXMLExtensionCUDS',
|
||||
'DianXMLExtensionInvoiceAuthorization',
|
||||
'DianXMLExtensionSoftwareProvider',
|
||||
'DianXMLExtensionAuthorizationProvider',
|
||||
'DianZIP',
|
||||
'AMBIENTE_PRUEBAS',
|
||||
'AMBIENTE_PRODUCCION',
|
||||
'form_xml',
|
||||
'nomina',
|
||||
]
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
from facho import facho
|
||||
|
||||
import zeep
|
||||
from zeep.wsse.username import UsernameToken
|
||||
from .wsse.signature import Signature, BinarySignature
|
||||
from zeep.wsa import WsAddressingPlugin
|
||||
from .wsse.signature import BinarySignature
|
||||
import xmlsec
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from typing import List
|
||||
import http.client
|
||||
import hashlib
|
||||
import secrets
|
||||
import base64
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
__all__ = ['DianClient',
|
||||
'ConsultaResolucionesFacturacionPeticion',
|
||||
'ConsultaResolucionesFacturacionRespuesta']
|
||||
__all__ = ['DianClient']
|
||||
|
||||
|
||||
class SOAPService:
|
||||
|
||||
@@ -33,6 +23,7 @@ class SOAPService:
|
||||
def todict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetNumberingRangeResponse:
|
||||
|
||||
@@ -47,8 +38,7 @@ class GetNumberingRangeResponse:
|
||||
ValidateDateTo: str
|
||||
TechnicalKey: str
|
||||
|
||||
NumberRangeResponse: List[NumberRangeResponse]
|
||||
|
||||
NumberRangeResponse: list[NumberRangeResponse]
|
||||
|
||||
@classmethod
|
||||
def fromdict(cls, data):
|
||||
@@ -91,7 +81,7 @@ class SendBillAsync(SOAPService):
|
||||
@dataclass
|
||||
class SendTestSetAsyncResponse:
|
||||
ZipKey: str
|
||||
ErrorMessageList: List[str]
|
||||
ErrorMessageList: list[str]
|
||||
|
||||
@classmethod
|
||||
def fromdict(cls, data):
|
||||
@@ -100,6 +90,7 @@ class SendTestSetAsyncResponse:
|
||||
data['ErrorMessageList'] or []
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendTestSetAsync(SOAPService):
|
||||
fileName: str
|
||||
@@ -115,6 +106,7 @@ class SendTestSetAsync(SOAPService):
|
||||
def build_response(self, as_dict):
|
||||
return SendTestSetAsyncResponse.fromdict(as_dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendBillSync(SOAPService):
|
||||
fileName: str
|
||||
@@ -129,12 +121,13 @@ class SendBillSync(SOAPService):
|
||||
def build_response(self, as_dict):
|
||||
return as_dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetStatusResponse:
|
||||
IsValid: bool
|
||||
StatusDescription: str
|
||||
StatusCode: int
|
||||
ErrorMessage: List[str]
|
||||
ErrorMessage: list[str]
|
||||
|
||||
@classmethod
|
||||
def fromdict(cls, data):
|
||||
@@ -142,12 +135,12 @@ class GetStatusResponse:
|
||||
error_message = data['ErrorMessage']['string']
|
||||
else:
|
||||
error_message = None
|
||||
|
||||
|
||||
return cls(data['IsValid'],
|
||||
data['StatusDescription'],
|
||||
data['StatusCode'],
|
||||
error_message)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetStatus(SOAPService):
|
||||
@@ -162,6 +155,7 @@ class GetStatus(SOAPService):
|
||||
def build_response(self, as_dict):
|
||||
return GetStatusResponse.fromdict(as_dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetStatusZip(SOAPService):
|
||||
trackId: bytes
|
||||
@@ -175,6 +169,7 @@ class GetStatusZip(SOAPService):
|
||||
def build_response(self, as_dict):
|
||||
return GetStatusResponse.fromdict(as_dict[0])
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendNominaSync(SOAPService):
|
||||
contentFile: bytes
|
||||
@@ -188,7 +183,7 @@ class SendNominaSync(SOAPService):
|
||||
def build_response(self, as_dict):
|
||||
return as_dict
|
||||
|
||||
|
||||
|
||||
class Habilitacion:
|
||||
WSDL = 'https://vpfe-hab.dian.gov.co/WcfDianCustomerServices.svc?wsdl'
|
||||
|
||||
@@ -220,6 +215,7 @@ class Habilitacion:
|
||||
def get_wsdl(self):
|
||||
return Habilitacion.WSDL
|
||||
|
||||
|
||||
class DianGateway:
|
||||
|
||||
def _open(self, service):
|
||||
@@ -250,7 +246,11 @@ class DianClient(DianGateway):
|
||||
self._password = password
|
||||
|
||||
def _open(self, service):
|
||||
return zeep.Client(service.get_wsdl(), wsse=UsernameToken(self._username, self._password))
|
||||
return zeep.Client(
|
||||
service.get_wsdl(),
|
||||
wsse=UsernameToken(
|
||||
self._username,
|
||||
self._password))
|
||||
|
||||
|
||||
class DianSignatureClient(DianGateway):
|
||||
@@ -262,13 +262,10 @@ class DianSignatureClient(DianGateway):
|
||||
|
||||
def _open(self, service):
|
||||
# RESOLUCCION 0004: pagina 756
|
||||
from zeep.wsse import utils
|
||||
|
||||
client = zeep.Client(service.get_wsdl(), wsse=
|
||||
BinarySignature(
|
||||
self.private_key_path, self.public_key_path, self.password,
|
||||
signature_method=xmlsec.Transform.RSA_SHA256,
|
||||
digest_method=xmlsec.Transform.SHA256)
|
||||
,
|
||||
client = zeep.Client(service.get_wsdl(), wsse=BinarySignature(
|
||||
self.private_key_path, self.public_key_path, self.password,
|
||||
signature_method=xmlsec.Transform.RSA_SHA256,
|
||||
digest_method=xmlsec.Transform.SHA256),
|
||||
)
|
||||
return client
|
||||
|
||||
@@ -9,7 +9,7 @@ module.
|
||||
|
||||
"""
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from lxml import etree
|
||||
from lxml.etree import QName
|
||||
|
||||
@@ -70,8 +70,11 @@ class MemorySignature(object):
|
||||
def apply(self, envelope, headers):
|
||||
key = _make_sign_key(self.key_data, self.cert_data, self.password)
|
||||
_sign_envelope_with_key(
|
||||
envelope, key, self.signature_method, self.digest_method, expires_dt=self.expires_dt
|
||||
)
|
||||
envelope,
|
||||
key,
|
||||
self.signature_method,
|
||||
self.digest_method,
|
||||
expires_dt=self.expires_dt)
|
||||
return envelope, headers
|
||||
|
||||
def verify(self, envelope):
|
||||
@@ -81,7 +84,7 @@ class MemorySignature(object):
|
||||
|
||||
|
||||
class Signature(MemorySignature):
|
||||
"""Sign given SOAP envelope with WSSE sig using given key file and cert file."""
|
||||
"""Sign given SOAP envelope with WSSE sig using given key and cert file."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -101,15 +104,18 @@ class Signature(MemorySignature):
|
||||
|
||||
|
||||
class BinarySignature(Signature):
|
||||
"""Sign given SOAP envelope with WSSE sig using given key file and cert file.
|
||||
"""Sign given SOAP envelope with WSSE sig using given key and cert file.
|
||||
|
||||
Place the key information into BinarySecurityElement."""
|
||||
|
||||
def apply(self, envelope, headers):
|
||||
key = _make_sign_key(self.key_data, self.cert_data, self.password)
|
||||
_sign_envelope_with_key_binary(
|
||||
envelope, key, self.signature_method, self.digest_method, expires_dt = self.expires_dt
|
||||
)
|
||||
envelope,
|
||||
key,
|
||||
self.signature_method,
|
||||
self.digest_method,
|
||||
expires_dt=self.expires_dt)
|
||||
return envelope, headers
|
||||
|
||||
|
||||
@@ -219,10 +225,12 @@ def sign_envelope(
|
||||
"""
|
||||
# Load the signing key and certificate.
|
||||
key = _make_sign_key(_read_file(keyfile), _read_file(certfile), password)
|
||||
return _sign_envelope_with_key(envelope, key, signature_method, digest_method)
|
||||
return _sign_envelope_with_key(
|
||||
envelope, key, signature_method, digest_method)
|
||||
|
||||
def get_timestamp(timestamp = None, delta=None):
|
||||
timestamp = timestamp or datetime.utcnow()
|
||||
|
||||
def get_timestamp(timestamp=None, delta=None):
|
||||
timestamp = timestamp or datetime.now(timezone.utc)
|
||||
if delta:
|
||||
timestamp += delta
|
||||
|
||||
@@ -230,25 +238,33 @@ def get_timestamp(timestamp = None, delta=None):
|
||||
timestamp = timestamp.replace(tzinfo=pytz.utc, microsecond=0)
|
||||
return timestamp.strftime(format_)
|
||||
|
||||
|
||||
def _append_timestamp(security, expires_dt=None):
|
||||
if expires_dt is None:
|
||||
expires_dt = timedelta(seconds=6000)
|
||||
|
||||
timestamp = datetime.now()
|
||||
etimestamp = utils.WSU.Timestamp({'{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Id': utils.get_unique_id()})
|
||||
etimestamp.append(utils.WSU.Created(get_timestamp(timestamp=timestamp)))
|
||||
etimestamp.append(utils.WSU.Expires(get_timestamp(timestamp=timestamp, delta=expires_dt)))
|
||||
etimestamp = utils.WSU.Timestamp({
|
||||
'{http://docs.oasis-open.org/wss/2004/01/'
|
||||
'oasis-200401-wss-wssecurity-utility-1.0.xsd}Id':
|
||||
utils.get_unique_id()})
|
||||
etimestamp.append(utils.WSU.Created(get_timestamp()))
|
||||
etimestamp.append(utils.WSU.Expires(get_timestamp(delta=expires_dt)))
|
||||
security.insert(0, etimestamp)
|
||||
if etree.LXML_VERSION[:2] >= (3, 5):
|
||||
etree.cleanup_namespaces(security,
|
||||
keep_ns_prefixes = security.nsmap,
|
||||
keep_ns_prefixes=security.nsmap,
|
||||
top_nsmap=utils.NSMAP)
|
||||
else:
|
||||
etree.cleanup_namespaces(header)
|
||||
etree.cleanup_namespaces(security)
|
||||
|
||||
def _signature_prepare(envelope, key, signature_method, digest_method, expires_dt=None):
|
||||
|
||||
def _signature_prepare(
|
||||
envelope,
|
||||
key,
|
||||
signature_method,
|
||||
digest_method,
|
||||
expires_dt=None):
|
||||
"""Prepare envelope and sign."""
|
||||
soap_env = detect_soap_env(envelope)
|
||||
|
||||
# Create the Signature node.
|
||||
signature = xmlsec.template.create(
|
||||
@@ -279,7 +295,7 @@ def _signature_prepare(envelope, key, signature_method, digest_method, expires_d
|
||||
_append_timestamp(security, expires_dt=expires_dt)
|
||||
|
||||
timestamp = security.find(QName(ns.WSU, "Timestamp"))
|
||||
if timestamp != None:
|
||||
if timestamp is not None:
|
||||
_sign_node(ctx, signature, timestamp, digest_method)
|
||||
ctx.sign(signature)
|
||||
|
||||
@@ -287,18 +303,30 @@ def _signature_prepare(envelope, key, signature_method, digest_method, expires_d
|
||||
# KeyInfo. The recipient expects this structure, but we can't rearrange
|
||||
# like this until after signing, because otherwise xmlsec won't populate
|
||||
# the X509 data (because it doesn't understand WSSE).
|
||||
sec_token_ref = etree.SubElement(key_info, QName(ns.WSSE, "SecurityTokenReference"))
|
||||
sec_token_ref = etree.SubElement(
|
||||
key_info, QName(
|
||||
ns.WSSE, "SecurityTokenReference"))
|
||||
return security, sec_token_ref, x509_data
|
||||
|
||||
|
||||
def _sign_envelope_with_key(envelope, key, signature_method, digest_method, expires_dt=None):
|
||||
def _sign_envelope_with_key(
|
||||
envelope,
|
||||
key,
|
||||
signature_method,
|
||||
digest_method,
|
||||
expires_dt=None):
|
||||
_, sec_token_ref, x509_data = _signature_prepare(
|
||||
envelope, key, signature_method, digest_method, expires_dt=expires_dt
|
||||
)
|
||||
sec_token_ref.append(x509_data)
|
||||
|
||||
|
||||
def _sign_envelope_with_key_binary(envelope, key, signature_method, digest_method, expires_dt=None):
|
||||
def _sign_envelope_with_key_binary(
|
||||
envelope,
|
||||
key,
|
||||
signature_method,
|
||||
digest_method,
|
||||
expires_dt=None):
|
||||
security, sec_token_ref, x509_data = _signature_prepare(
|
||||
envelope, key, signature_method, digest_method, expires_dt=expires_dt
|
||||
)
|
||||
@@ -354,7 +382,10 @@ def _verify_envelope_with_key(envelope, key):
|
||||
ctx = xmlsec.SignatureContext()
|
||||
|
||||
# Find each signed element and register its ID with the signing context.
|
||||
refs = signature.xpath("ds:SignedInfo/ds:Reference", namespaces={"ds": ns.DS})
|
||||
refs = signature.xpath(
|
||||
"ds:SignedInfo/ds:Reference",
|
||||
namespaces={
|
||||
"ds": ns.DS})
|
||||
for ref in refs:
|
||||
# Get the reference URI and cut off the initial '#'
|
||||
referenced_id = ref.get("URI")[1:]
|
||||
|
||||
@@ -7,10 +7,17 @@ def path_for_xsd(dirname, xsdname):
|
||||
data_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(data_dir, dirname, xsdname)
|
||||
|
||||
UBLInvoice= xmlschema.XMLSchema(path_for_xsd('maindoc', 'UBL-Invoice-2.1.xsd'))
|
||||
|
||||
NominaIndividual = xmlschema.XMLSchema(path_for_xsd('nomina', 'NominaIndividualElectronicaXSDV1.0.6.xsd'))
|
||||
NominaIndividualDeAjuste = xmlschema.XMLSchema(path_for_xsd('nomina', 'NominaIndividualDeAjusteElectronicaXSDV1.0.6.xsd'))
|
||||
UBLInvoice = xmlschema.XMLSchema(
|
||||
path_for_xsd(
|
||||
'maindoc',
|
||||
'UBL-Invoice-2.1.xsd'))
|
||||
|
||||
NominaIndividual = xmlschema.XMLSchema(path_for_xsd(
|
||||
'nomina', 'NominaIndividualElectronicaXSDV1.0.6.xsd'))
|
||||
NominaIndividualDeAjuste = xmlschema.XMLSchema(path_for_xsd(
|
||||
'nomina', 'NominaIndividualDeAjusteElectronicaXSDV1.0.6.xsd'))
|
||||
|
||||
|
||||
def validate(xml, schema):
|
||||
schema.validate(xml)
|
||||
|
||||
@@ -20,12 +20,12 @@ class CodeList:
|
||||
def _load(self, filename, primary_column):
|
||||
tree = etree.parse(filename)
|
||||
|
||||
#obtener identificadores...
|
||||
# obtener identificadores...
|
||||
self.short_name = tree.find('./Identification/ShortName').text
|
||||
self.long_name = tree.find('./Identification/LongName').text
|
||||
self.version = tree.find('./Identification/Version').text
|
||||
|
||||
#obtener registros...
|
||||
# obtener registros...
|
||||
for row in tree.findall('./SimpleCodeList/Row'):
|
||||
new_row = self.xmlrow_to_dict(row)
|
||||
primary_key = new_row[primary_column]
|
||||
@@ -34,9 +34,9 @@ class CodeList:
|
||||
def xmlrow_to_dict(self, xmlrow):
|
||||
row = {}
|
||||
|
||||
#construir registro...
|
||||
for value in xmlrow.getchildren():
|
||||
row[value.attrib['ColumnRef']] = value.getchildren()[0].text
|
||||
# construir registro...
|
||||
for value in list(xmlrow):
|
||||
row[value.attrib['ColumnRef']] = list(value)[0].text
|
||||
|
||||
return row
|
||||
|
||||
@@ -60,6 +60,7 @@ class CodeList:
|
||||
# nombres de variables igual a ./Identification/ShortName
|
||||
# TODO: garantizar unica carga en python
|
||||
|
||||
|
||||
__all__ = ['TipoOrganizacion',
|
||||
'TipoResponsabilidad',
|
||||
'TipoAmbiente',
|
||||
@@ -72,38 +73,88 @@ __all__ = ['TipoOrganizacion',
|
||||
'Municipio',
|
||||
'Departamento']
|
||||
|
||||
|
||||
def path_for_codelist(name):
|
||||
return os.path.join(DATA_DIR, name)
|
||||
|
||||
TipoOrganizacion = CodeList(path_for_codelist('TipoOrganizacion-2.1.gc'), 'code', 'name')
|
||||
TipoResponsabilidad = CodeList(path_for_codelist('TipoResponsabilidad-2.1.gc'), 'code', 'name')\
|
||||
.update(CodeList(path_for_codelist('TipoResponsabilidad-2.1.custom.gc'), 'code', 'name'))
|
||||
TipoAmbiente = CodeList(path_for_codelist('TipoAmbiente-2.1.gc'), 'code', 'name')
|
||||
TipoDocumento = CodeList(path_for_codelist('TipoDocumento-2.1.gc'), 'code', 'name')
|
||||
TipoImpuesto = CodeList(path_for_codelist('TipoImpuesto-2.1.gc'), 'code', 'name')\
|
||||
.update(CodeList(path_for_codelist('TipoImpuesto-2.1.custom.gc'), 'code', 'name'))
|
||||
TarifaImpuesto = CodeList(path_for_codelist('TarifaImpuestoINC-2.1.gc'), 'code', 'name')\
|
||||
.update(CodeList(path_for_codelist('TarifaImpuestoIVA-2.1.gc'), 'code', 'name'))\
|
||||
.update(CodeList(path_for_codelist('TarifaImpuestoReteIVA-2.1.gc'), 'code', 'name'))\
|
||||
.update(CodeList(path_for_codelist('TarifaImpuestoReteRenta-2.1.gc'), 'code', 'name'))
|
||||
CodigoPrecioReferencia = CodeList(path_for_codelist('CodigoPrecioReferencia-2.1.gc'), 'code', 'name')
|
||||
|
||||
TipoOrganizacion = CodeList(path_for_codelist(
|
||||
'TipoOrganizacion-2.1.gc'), 'code', 'name')
|
||||
TipoResponsabilidad = CodeList(
|
||||
path_for_codelist('TipoResponsabilidad-2.1.gc'),
|
||||
'code',
|
||||
'name') .update(
|
||||
CodeList(
|
||||
path_for_codelist('TipoResponsabilidad-2.1.custom.gc'),
|
||||
'code',
|
||||
'name'))
|
||||
TipoAmbiente = CodeList(path_for_codelist(
|
||||
'TipoAmbiente-2.1.gc'), 'code', 'name')
|
||||
TipoDocumento = CodeList(path_for_codelist(
|
||||
'TipoDocumento-2.1.gc'), 'code', 'name')
|
||||
TipoImpuesto = CodeList(
|
||||
path_for_codelist('TipoImpuesto-2.1.gc'),
|
||||
'code',
|
||||
'name') .update(
|
||||
CodeList(
|
||||
path_for_codelist('TipoImpuesto-2.1.custom.gc'),
|
||||
'code',
|
||||
'name'))
|
||||
TarifaImpuesto = CodeList(
|
||||
path_for_codelist('TarifaImpuestoINC-2.1.gc'),
|
||||
'code',
|
||||
'name') .update(
|
||||
CodeList(
|
||||
path_for_codelist('TarifaImpuestoIVA-2.1.gc'),
|
||||
'code',
|
||||
'name')) .update(
|
||||
CodeList(
|
||||
path_for_codelist('TarifaImpuestoReteIVA-2.1.gc'),
|
||||
'code',
|
||||
'name')) .update(
|
||||
CodeList(
|
||||
path_for_codelist(
|
||||
'TarifaImpuestoReteRenta-2.1.gc'),
|
||||
'code',
|
||||
'name'))
|
||||
CodigoPrecioReferencia = CodeList(path_for_codelist(
|
||||
'CodigoPrecioReferencia-2.1.gc'), 'code', 'name')
|
||||
MediosPago = CodeList(path_for_codelist('MediosPago-2.1.gc'), 'code', 'name')
|
||||
FormasPago = CodeList(path_for_codelist('FormasPago-2.1.gc'), 'code', 'name')
|
||||
RegimenFiscal = CodeList(path_for_codelist('RegimenFiscal-2.1.custom.gc'), 'code', 'name')
|
||||
TipoOperacionNC = CodeList(path_for_codelist('TipoOperacionNC-2.1.gc'), 'code', 'name')
|
||||
TipoOperacionNCDS = CodeList(path_for_codelist('TipoOperacionNCDS-2.1.gc'), 'code', 'name')
|
||||
TipoOperacionND = CodeList(path_for_codelist('TipoOperacionND-2.1 - copia.gc'), 'code', 'name')
|
||||
TipoOperacionF = CodeList(path_for_codelist('TipoOperacionF-2.1.gc'), 'code', 'name')\
|
||||
.update(CodeList(path_for_codelist('TipoOperacionF-2.1.custom.gc'), 'code', 'name'))
|
||||
RegimenFiscal = CodeList(path_for_codelist(
|
||||
'RegimenFiscal-2.1.custom.gc'), 'code', 'name')
|
||||
TipoOperacionNC = CodeList(path_for_codelist(
|
||||
'TipoOperacionNC-2.1.gc'), 'code', 'name')
|
||||
TipoOperacionNCDS = CodeList(path_for_codelist(
|
||||
'TipoOperacionNCDS-2.1.gc'), 'code', 'name')
|
||||
TipoOperacionND = CodeList(path_for_codelist(
|
||||
'TipoOperacionND-2.1 - copia.gc'), 'code', 'name')
|
||||
TipoOperacionF = CodeList(
|
||||
path_for_codelist('TipoOperacionF-2.1.gc'),
|
||||
'code',
|
||||
'name') .update(
|
||||
CodeList(
|
||||
path_for_codelist('TipoOperacionF-2.1.custom.gc'),
|
||||
'code',
|
||||
'name'))
|
||||
Municipio = CodeList(path_for_codelist('Municipio-2.1.gc'), 'code', 'name')
|
||||
Departamento = CodeList(path_for_codelist('Departamentos-2.1.gc'), 'code', 'name')
|
||||
Departamento = CodeList(path_for_codelist(
|
||||
'Departamentos-2.1.gc'), 'code', 'name')
|
||||
Paises = CodeList(path_for_codelist('Paises-2.1.gc'), 'code', 'name')
|
||||
TipoIdFiscal = CodeList(path_for_codelist('TipoIdFiscal-2.1.gc'), 'code', 'name')
|
||||
CodigoDescuento = CodeList(path_for_codelist('CodigoDescuento-2.1.gc'), 'code', 'name')
|
||||
UnidadesMedida = CodeList(path_for_codelist('UnidadesMedida-2.1.gc'), 'code', 'name')
|
||||
TipoTrabajador = CodeList(path_for_codelist('TipoTrabajador-2.1.gc'), 'code', 'name')
|
||||
SubTipoTrabajador = CodeList(path_for_codelist('SubTipoTrabajador-2.1.gc'), 'code', 'name')
|
||||
TipoContrato = CodeList(path_for_codelist('TipoContrato-2.1.gc'), 'code', 'name')
|
||||
PeriodoNomina = CodeList(path_for_codelist('PeriodoNomina-2.1.gc'), 'code', 'name')
|
||||
TipoIdFiscal = CodeList(path_for_codelist(
|
||||
'TipoIdFiscal-2.1.gc'), 'code', 'name')
|
||||
CodigoDescuento = CodeList(path_for_codelist(
|
||||
'CodigoDescuento-2.1.gc'), 'code', 'name')
|
||||
UnidadesMedida = CodeList(path_for_codelist(
|
||||
'UnidadesMedida-2.1.gc'), 'code', 'name')
|
||||
TipoTrabajador = CodeList(path_for_codelist(
|
||||
'TipoTrabajador-2.1.gc'), 'code', 'name')
|
||||
SubTipoTrabajador = CodeList(path_for_codelist(
|
||||
'SubTipoTrabajador-2.1.gc'), 'code', 'name')
|
||||
TipoContrato = CodeList(path_for_codelist(
|
||||
'TipoContrato-2.1.gc'), 'code', 'name')
|
||||
PeriodoNomina = CodeList(path_for_codelist(
|
||||
'PeriodoNomina-2.1.gc'), 'code', 'name')
|
||||
TipoMoneda = CodeList(path_for_codelist('TipoMoneda-2.1.gc'), 'code', 'name')
|
||||
IdiomaISO6391 = CodeList(path_for_codelist('Idioma-2.1.gc'), 'iso-639-1', 'name')
|
||||
IdiomaISO6391 = CodeList(path_for_codelist(
|
||||
'Idioma-2.1.gc'), 'iso-639-1', 'name')
|
||||
|
||||
363
facho/fe/fe.py
363
facho/fe/fe.py
@@ -5,7 +5,6 @@ import uuid
|
||||
import xmlsig
|
||||
import xades
|
||||
from datetime import datetime
|
||||
import OpenSSL
|
||||
import zipfile
|
||||
# import warnings
|
||||
import hashlib
|
||||
@@ -18,54 +17,88 @@ from dateutil import tz
|
||||
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
|
||||
# Monkey-patch xades/xmlsig para compatibilidad con pyOpenSSL >= 24
|
||||
# (PKCS12 fue eliminado de OpenSSL.crypto, pero ambas librerías lo referencian)
|
||||
import OpenSSL
|
||||
import xades.xades_context
|
||||
|
||||
if not hasattr(OpenSSL.crypto, 'PKCS12'):
|
||||
def _patched_load_pkcs12(self, key):
|
||||
if isinstance(key, tuple):
|
||||
self.x509 = key[1]
|
||||
self.public_key = key[1].public_key()
|
||||
self.private_key = key[0]
|
||||
else:
|
||||
raise NotImplementedError("unsupported key type")
|
||||
xades.xades_context.XAdESContext.load_pkcs12 = _patched_load_pkcs12
|
||||
|
||||
AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
|
||||
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
|
||||
|
||||
|
||||
SCHEME_AGENCY_ATTRS = {
|
||||
'schemeAgencyName': 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)',
|
||||
'schemeAgencyID': '195'
|
||||
}
|
||||
'schemeAgencyName': 'CO, DIAN (Dirección de Impuestos y Aduanas'
|
||||
' Nacionales)',
|
||||
'schemeAgencyID': '195'}
|
||||
|
||||
|
||||
# RESOLUCION 0001: pagina 516
|
||||
POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf'
|
||||
POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.'
|
||||
POLICY_ID = (
|
||||
'https://facturaelectronica.dian.gov.co/politicadefirma/v2'
|
||||
'/politicadefirmav2.pdf'
|
||||
)
|
||||
POLICY_NAME = (
|
||||
u'Política de firma para facturas electrónicas de la República de '
|
||||
u'Colombia.'
|
||||
)
|
||||
|
||||
Bogota = tz.gettz('America/Bogota')
|
||||
# NAMESPACES = {
|
||||
# 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
|
||||
# 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual',
|
||||
# 'nominaajuste': 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste',
|
||||
# 'nominaajuste': 'dian:gov:co:facturaelectronica:'
|
||||
# 'NominaIndividualDeAjuste',
|
||||
# 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1',
|
||||
# 'xs': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
# 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
|
||||
# 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
||||
# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1',
|
||||
# 'xs': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
# 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:'
|
||||
# 'CommonAggregateComponents-2',
|
||||
# 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:'
|
||||
# 'CommonBasicComponents-2',
|
||||
# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:'
|
||||
# 'schema:xsd:DocumentInformationAggregateComponents-1',
|
||||
# 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001',
|
||||
# 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003',
|
||||
# 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2',
|
||||
# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2',
|
||||
# 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:'
|
||||
# 'IANAMIMEMediaType:2003',
|
||||
# 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:'
|
||||
# 'CommonExtensionComponents-2',
|
||||
# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:'
|
||||
# 'QualifiedDatatypes-2',
|
||||
# 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1',
|
||||
# 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
|
||||
# 'udt': 'urn:un:unece:uncefact:data:specification:'
|
||||
# 'UnqualifiedDataTypesSchemaModule:2',
|
||||
# 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
# 'xades': 'http://uri.etsi.org/01903/v1.3.2#',
|
||||
# 'xades141': 'http://uri.etsi.org/01903/v1.4.1#',
|
||||
# 'xades141': 'http://uri.etsi.org/01903/v1.4.1#',
|
||||
# 'ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
# 'sig': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
# }
|
||||
|
||||
|
||||
NAMESPACES = {
|
||||
'apr': 'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2',
|
||||
'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
|
||||
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1',
|
||||
'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
|
||||
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
||||
'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2',
|
||||
'cac': (
|
||||
'urn:oasis:names:specification:ubl:schema:xsd'
|
||||
':CommonAggregateComponents-2'),
|
||||
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd'
|
||||
':CommonBasicComponents-2',
|
||||
'ext': (
|
||||
'urn:oasis:names:specification:ubl:schema:xsd'
|
||||
':CommonExtensionComponents-2'),
|
||||
'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2',
|
||||
'sts': 'dian:gov:co:facturaelectronica:Structures-2-1',
|
||||
'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
|
||||
'udt': (
|
||||
'urn:un:unece:uncefact:data:specification'
|
||||
':UnqualifiedDataTypesSchemaModule:2'),
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'xades': 'http://uri.etsi.org/01903/v1.3.2#',
|
||||
@@ -107,22 +140,21 @@ class FeXML(FachoXML):
|
||||
return super().from_string(document, namespaces=NAMESPACES)
|
||||
|
||||
def tostring(self, **kw):
|
||||
# MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un namespace
|
||||
urn_oasis = {
|
||||
'AttachedDocument': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
|
||||
'Invoice': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
|
||||
'CreditNote': 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2',
|
||||
'ApplicationResponse': 'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2'
|
||||
}
|
||||
|
||||
# MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un
|
||||
# namespace
|
||||
root_namespace = self.root_namespace()
|
||||
root_localname = self.root_localname()
|
||||
xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace]
|
||||
|
||||
if root_localname == 'Invoice':
|
||||
urn_oasis = (
|
||||
'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2')
|
||||
if root_localname == 'CreditNote':
|
||||
urn_oasis = (
|
||||
'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2')
|
||||
return super().tostring(**kw)\
|
||||
.replace(xmlns_name + ':', '')\
|
||||
.replace('xmlns:'+xmlns_name, 'xmlns')\
|
||||
.replace(root_namespace, urn_oasis[root_localname])
|
||||
.replace(xmlns_name + ':', '')\
|
||||
.replace('xmlns:' + xmlns_name, 'xmlns')\
|
||||
.replace(root_namespace, urn_oasis)
|
||||
|
||||
|
||||
class DianXMLExtensionCUDFE(FachoXMLExtension):
|
||||
@@ -141,9 +173,12 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
|
||||
|
||||
def _get_qrcode(self, cufe):
|
||||
url_for = {
|
||||
AMBIENTE_PRUEBAS: 'https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey=',
|
||||
AMBIENTE_PRODUCCION: 'https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey='
|
||||
}
|
||||
AMBIENTE_PRUEBAS: (
|
||||
'https://catalogo-vpfe-hab.dian.gov.co/document/'
|
||||
'searchqr?documentkey='),
|
||||
AMBIENTE_PRODUCCION: (
|
||||
'https://catalogo-vpfe.dian.gov.co/document/'
|
||||
'searchqr?documentkey=')}
|
||||
return url_for[self.tipo_ambiente] + cufe
|
||||
|
||||
def build(self, fachoxml):
|
||||
@@ -154,21 +189,29 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
|
||||
|
||||
if self.schemeName() == "CUDS-SHA384":
|
||||
if fachoxml.tag_document() == 'Invoice':
|
||||
fachoxml.set_element('./cbc:ProfileID',
|
||||
'DIAN 2.1: documento soporte en adquisiciones efectuadas a no obligados a facturar.')
|
||||
fachoxml.set_element(
|
||||
'./cbc:ProfileID',
|
||||
'DIAN 2.1: documento soporte en adquisiciones '
|
||||
'efectuadas a no obligados a facturar.')
|
||||
else:
|
||||
fachoxml.set_element('./cbc:ProfileID',
|
||||
'DIAN 2.1: Nota de ajuste al documento soporte en adquisiciones efectuadas a sujetos no obligados a expedir factura o documento equivalente')
|
||||
fachoxml.set_element(
|
||||
'./cbc:ProfileID',
|
||||
'DIAN 2.1: Nota de ajuste al documento soporte en '
|
||||
'adquisiciones efectuadas a sujetos no obligados a '
|
||||
'expedir factura o documento equivalente')
|
||||
else:
|
||||
fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta')
|
||||
fachoxml.set_element(
|
||||
'./cbc:ProfileID',
|
||||
'DIAN 2.1: Factura Electrónica de Venta')
|
||||
|
||||
# #DIAN 1.8.-2021: FAD03
|
||||
# fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta')
|
||||
# fachoxml.set_element('./cbc:ProfileID',
|
||||
# 'DIAN 2.1: Factura Electrónica de Venta')
|
||||
fachoxml.set_element(
|
||||
'./cbc:ProfileExecutionID', self._tipo_ambiente_int())
|
||||
#DIAN 1.7.-2020: FAB36
|
||||
# DIAN 1.7.-2020: FAB36
|
||||
fachoxml.set_element(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode',
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:QRCode',
|
||||
self._get_qrcode(cufe))
|
||||
|
||||
def issue_time(self, datetime_):
|
||||
@@ -184,7 +227,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
|
||||
build_vars['FecFac'] = self.issue_date(invoice.invoice_issue)
|
||||
build_vars['HoraFac'] = self.issue_time(invoice.invoice_issue)
|
||||
# PAG 601
|
||||
build_vars['ValorBruto'] = invoice.invoice_legal_monetary_total.line_extension_amount
|
||||
build_vars['ValorBruto'] = (
|
||||
invoice.invoice_legal_monetary_total.line_extension_amount)
|
||||
build_vars['ValorTotalPagar'
|
||||
] = invoice.invoice_legal_monetary_total.payable_amount
|
||||
ValorImpuestoPara = defaultdict(lambda: form.Amount(0.0))
|
||||
@@ -196,7 +240,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
|
||||
if subtotal.scheme is not None:
|
||||
# TODO cual es la naturaleza de tax_scheme_ident?
|
||||
codigo_impuesto = subtotal.scheme.code
|
||||
ValorImpuestoPara.setdefault(codigo_impuesto, form.Amount(0.0))
|
||||
ValorImpuestoPara.setdefault(
|
||||
codigo_impuesto, form.Amount(0.0))
|
||||
ValorImpuestoPara[codigo_impuesto] += subtotal.tax_amount
|
||||
|
||||
build_vars['ValorImpuestoPara'] = ValorImpuestoPara
|
||||
@@ -217,7 +262,7 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
|
||||
|
||||
class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
|
||||
def __init__(
|
||||
self, invoice, clave_tecnica='', tipo_ambiente=AMBIENTE_PRUEBAS):
|
||||
self, invoice, clave_tecnica='', tipo_ambiente=AMBIENTE_PRUEBAS):
|
||||
self.tipo_ambiente = tipo_ambiente
|
||||
self.clave_tecnica = clave_tecnica
|
||||
self.invoice = invoice
|
||||
@@ -236,26 +281,40 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
|
||||
CodImpuesto2 = build_vars['CodImpuesto2']
|
||||
CodImpuesto3 = build_vars['CodImpuesto3']
|
||||
return [
|
||||
'%s' % build_vars['NumFac'],
|
||||
'%s' % build_vars['FecFac'],
|
||||
'%s' % build_vars['HoraFac'],
|
||||
form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
|
||||
'%s' %
|
||||
build_vars['NumFac'],
|
||||
'%s' %
|
||||
build_vars['FecFac'],
|
||||
'%s' %
|
||||
build_vars['HoraFac'],
|
||||
form.Amount(
|
||||
build_vars['ValorBruto']).truncate_as_string(2),
|
||||
CodImpuesto1,
|
||||
build_vars['ValorImpuestoPara'].get(CodImpuesto1, form.Amount(0.0)).truncate_as_string(2),
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto1,
|
||||
form.Amount(0.0)).truncate_as_string(2),
|
||||
CodImpuesto2,
|
||||
build_vars['ValorImpuestoPara'].get(CodImpuesto2, form.Amount(0.0)).truncate_as_string(2),
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto2,
|
||||
form.Amount(0.0)).truncate_as_string(2),
|
||||
CodImpuesto3,
|
||||
build_vars['ValorImpuestoPara'].get(CodImpuesto3, form.Amount(0.0)).truncate_as_string(2),
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto3,
|
||||
form.Amount(0.0)).truncate_as_string(2),
|
||||
build_vars['ValorTotalPagar'].truncate_as_string(2),
|
||||
'%s' % build_vars['NitOFE'],
|
||||
'%s' % build_vars['NumAdq'],
|
||||
'%s' % build_vars['ClTec'],
|
||||
'%d' % build_vars['TipoAmb'],
|
||||
'%s' %
|
||||
build_vars['NitOFE'],
|
||||
'%s' %
|
||||
build_vars['NumAdq'],
|
||||
'%s' %
|
||||
build_vars['ClTec'],
|
||||
'%d' %
|
||||
build_vars['TipoAmb'],
|
||||
]
|
||||
|
||||
|
||||
class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
|
||||
def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS):
|
||||
def __init__(self, invoice, software_pin, tipo_ambiente=AMBIENTE_PRUEBAS):
|
||||
self.tipo_ambiente = tipo_ambiente
|
||||
self.software_pin = software_pin
|
||||
self.invoice = invoice
|
||||
@@ -274,26 +333,44 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
|
||||
CodImpuesto2 = build_vars['CodImpuesto2']
|
||||
CodImpuesto3 = build_vars['CodImpuesto3']
|
||||
return [
|
||||
'%s' % build_vars['NumFac'],
|
||||
'%s' % build_vars['FecFac'],
|
||||
'%s' % build_vars['HoraFac'],
|
||||
form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
|
||||
'%s' %
|
||||
build_vars['NumFac'],
|
||||
'%s' %
|
||||
build_vars['FecFac'],
|
||||
'%s' %
|
||||
build_vars['HoraFac'],
|
||||
form.Amount(
|
||||
build_vars['ValorBruto']).truncate_as_string(2),
|
||||
CodImpuesto1,
|
||||
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2),
|
||||
form.Amount(
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto1,
|
||||
0.0)).truncate_as_string(2),
|
||||
CodImpuesto2,
|
||||
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).truncate_as_string(2),
|
||||
form.Amount(
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto2,
|
||||
0.0)).truncate_as_string(2),
|
||||
CodImpuesto3,
|
||||
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).truncate_as_string(2),
|
||||
form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2),
|
||||
'%s' % build_vars['NitOFE'],
|
||||
'%s' % build_vars['NumAdq'],
|
||||
'%s' % build_vars['Software-PIN'],
|
||||
'%d' % build_vars['TipoAmb'],
|
||||
form.Amount(
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto3,
|
||||
0.0)).truncate_as_string(2),
|
||||
form.Amount(
|
||||
build_vars['ValorTotalPagar']).truncate_as_string(2),
|
||||
'%s' %
|
||||
build_vars['NitOFE'],
|
||||
'%s' %
|
||||
build_vars['NumAdq'],
|
||||
'%s' %
|
||||
build_vars['Software-PIN'],
|
||||
'%d' %
|
||||
build_vars['TipoAmb'],
|
||||
]
|
||||
|
||||
|
||||
class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
|
||||
def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS):
|
||||
def __init__(self, invoice, software_pin, tipo_ambiente=AMBIENTE_PRUEBAS):
|
||||
self.tipo_ambiente = tipo_ambiente
|
||||
self.software_pin = software_pin
|
||||
self.invoice = invoice
|
||||
@@ -309,20 +386,30 @@ class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
|
||||
def formatVars(self):
|
||||
build_vars = self.buildVars()
|
||||
CodImpuesto1 = build_vars['CodImpuesto1']
|
||||
CodImpuesto2 = build_vars['CodImpuesto2']
|
||||
CodImpuesto3 = build_vars['CodImpuesto3']
|
||||
return [
|
||||
'%s' % build_vars['NumFac'],
|
||||
'%s' % build_vars['FecFac'],
|
||||
'%s' % build_vars['HoraFac'],
|
||||
form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
|
||||
'%s' %
|
||||
build_vars['NumFac'],
|
||||
'%s' %
|
||||
build_vars['FecFac'],
|
||||
'%s' %
|
||||
build_vars['HoraFac'],
|
||||
form.Amount(
|
||||
build_vars['ValorBruto']).truncate_as_string(2),
|
||||
CodImpuesto1,
|
||||
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2),
|
||||
form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2),
|
||||
'%s' % build_vars['NitOFE'],
|
||||
'%s' % build_vars['NumAdq'],
|
||||
'%s' % build_vars['Software-PIN'],
|
||||
'%d' % build_vars['TipoAmb'],
|
||||
form.Amount(
|
||||
build_vars['ValorImpuestoPara'].get(
|
||||
CodImpuesto1,
|
||||
0.0)).truncate_as_string(2),
|
||||
form.Amount(
|
||||
build_vars['ValorTotalPagar']).truncate_as_string(2),
|
||||
'%s' %
|
||||
build_vars['NitOFE'],
|
||||
'%s' %
|
||||
build_vars['NumAdq'],
|
||||
'%s' %
|
||||
build_vars['Software-PIN'],
|
||||
'%d' %
|
||||
build_vars['TipoAmb'],
|
||||
]
|
||||
|
||||
|
||||
@@ -336,15 +423,20 @@ class DianXMLExtensionSoftwareProvider(FachoXMLExtension):
|
||||
|
||||
def build(self, fexml):
|
||||
software_provider = fexml.fragment(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider')
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:SoftwareProvider')
|
||||
provider_id_attrs = SCHEME_AGENCY_ATTRS.copy()
|
||||
provider_id_attrs.update({'schemeID': self.dv})
|
||||
#DIAN 1.7.-2020: FAB23
|
||||
# DIAN 1.7.-2020: FAB23
|
||||
provider_id_attrs.update({'schemeName': '31'})
|
||||
software_provider.set_element('/sts:SoftwareProvider/sts:ProviderID', self.nit,
|
||||
**provider_id_attrs)
|
||||
software_provider.set_element('/sts:SoftwareProvider/sts:SoftwareID', self.id_software,
|
||||
**SCHEME_AGENCY_ATTRS)
|
||||
software_provider.set_element(
|
||||
'/sts:SoftwareProvider/sts:ProviderID',
|
||||
self.nit,
|
||||
**provider_id_attrs)
|
||||
software_provider.set_element(
|
||||
'/sts:SoftwareProvider/sts:SoftwareID',
|
||||
self.id_software,
|
||||
**SCHEME_AGENCY_ATTRS)
|
||||
|
||||
|
||||
class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
|
||||
@@ -356,7 +448,10 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
|
||||
self.invoice_ident = invoice_ident
|
||||
|
||||
def build(self, fexml):
|
||||
dian_path = './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode'
|
||||
dian_path = (
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:SoftwareSecurityCode'
|
||||
)
|
||||
code = str(self.id_software) + str(self.pin) + str(self.invoice_ident)
|
||||
m = hashlib.sha384()
|
||||
m.update(code.encode('utf-8'))
|
||||
@@ -393,8 +488,8 @@ class DianXMLExtensionSigner:
|
||||
xml = LXMLBuilder.from_string(document)
|
||||
signature = self.sign_xml_element(xml)
|
||||
|
||||
fachoxml = FachoXML(xml,nsmap=NAMESPACES)
|
||||
#DIAN 1.7.-2020: FAB01
|
||||
fachoxml = FachoXML(xml, nsmap=NAMESPACES)
|
||||
# DIAN 1.7.-2020: FAB01
|
||||
extcontent = self._element_extension_content(fachoxml)
|
||||
fachoxml.append_element(extcontent, signature)
|
||||
|
||||
@@ -410,8 +505,11 @@ class DianXMLExtensionSigner:
|
||||
xml.append(signature)
|
||||
|
||||
ref = xmlsig.template.add_reference(
|
||||
signature, xmlsig.constants.TransformSha256, uri="", name="xmldsig-%s-ref0" % (id_uuid)
|
||||
)
|
||||
signature,
|
||||
xmlsig.constants.TransformSha256,
|
||||
uri="",
|
||||
name="xmldsig-%s-ref0" %
|
||||
(id_uuid))
|
||||
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
|
||||
|
||||
id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid)
|
||||
@@ -430,9 +528,8 @@ class DianXMLExtensionSigner:
|
||||
|
||||
id_props = "xmldsig-%s-signedprops" % (id_uuid)
|
||||
props_ref = xmlsig.template.add_reference(
|
||||
signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_props),
|
||||
uri_type="http://uri.etsi.org/01903#SignedProperties"
|
||||
)
|
||||
signature, xmlsig.constants.TransformSha256, uri="#%s" %
|
||||
(id_props), uri_type="http://uri.etsi.org/01903#SignedProperties")
|
||||
xmlsig.template.add_transform(
|
||||
props_ref, xmlsig.constants.TransformInclC14N)
|
||||
|
||||
@@ -450,9 +547,6 @@ class DianXMLExtensionSigner:
|
||||
self._pkcs12_data,
|
||||
self._passphrase))
|
||||
|
||||
# ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(
|
||||
# self._pkcs12_data,
|
||||
# self._passphrase))
|
||||
if self._localpolicy:
|
||||
with mock_xades_policy():
|
||||
ctx.sign(signature)
|
||||
@@ -476,7 +570,9 @@ class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
|
||||
def build(self, fexml):
|
||||
attrs = {'schemeID': '4', 'schemeName': '31'}
|
||||
attrs.update(SCHEME_AGENCY_ATTRS)
|
||||
authorization_provider = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider')
|
||||
authorization_provider = fexml.fragment(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:AuthorizationProvider')
|
||||
authorization_provider.set_element('./sts:AuthorizationProviderID',
|
||||
'800197268',
|
||||
**attrs)
|
||||
@@ -485,12 +581,17 @@ class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
|
||||
class DianXMLExtensionInvoiceSource(FachoXMLExtension):
|
||||
# CAB13
|
||||
def build(self, fexml):
|
||||
dian_path = '/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode'
|
||||
dian_path = (
|
||||
'/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionCo'
|
||||
'ntent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode'
|
||||
)
|
||||
fexml.set_element(
|
||||
dian_path, 'CO',
|
||||
listAgencyID="6",
|
||||
listAgencyName="United Nations Economic Commission for Europe",
|
||||
listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1")
|
||||
listSchemeURI=(
|
||||
"urn:oasis:names:specification:ubl:codelist:gc:"
|
||||
"CountryIdentificationCode-2.1"))
|
||||
|
||||
|
||||
class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
|
||||
@@ -507,28 +608,38 @@ class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
|
||||
self.to = to
|
||||
|
||||
def build(self, fexml):
|
||||
invoice_control = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl')
|
||||
invoice_control.set_element('/sts:InvoiceControl/sts:InvoiceAuthorization', self.authorization)
|
||||
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:StartDate',
|
||||
self.period_startdate.strftime('%Y-%m-%d'))
|
||||
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:EndDate',
|
||||
self.period_enddate.strftime('%Y-%m-%d'))
|
||||
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:Prefix',
|
||||
self.prefix)
|
||||
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:From',
|
||||
self.from_)
|
||||
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To',
|
||||
self.to)
|
||||
invoice_control = fexml.fragment(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:InvoiceControl')
|
||||
invoice_control.set_element(
|
||||
'/sts:InvoiceControl/sts:InvoiceAuthorization',
|
||||
self.authorization)
|
||||
invoice_control.set_element(
|
||||
'/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:StartDate',
|
||||
self.period_startdate.strftime('%Y-%m-%d'))
|
||||
invoice_control.set_element(
|
||||
'/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:EndDate',
|
||||
self.period_enddate.strftime('%Y-%m-%d'))
|
||||
invoice_control.set_element(
|
||||
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:Prefix',
|
||||
self.prefix)
|
||||
invoice_control.set_element(
|
||||
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:From', self.from_)
|
||||
invoice_control.set_element(
|
||||
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To', self.to)
|
||||
|
||||
fexml.set_element(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
|
||||
'CO',
|
||||
# DIAN 1.7.-2020: FAB15
|
||||
listAgencyID="6",
|
||||
# DIAN 1.7.-2020: FAB16
|
||||
listAgencyName="United Nations Economic Commission for Europe",
|
||||
# DIAN 1.7.-2020: FAB17
|
||||
listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1")
|
||||
listSchemeURI=(
|
||||
"urn:oasis:names:specification:ubl:codelist:gc:"
|
||||
"CountryIdentificationCode-2.1"))
|
||||
|
||||
|
||||
class DianZIP:
|
||||
@@ -571,7 +682,11 @@ class DianZIP:
|
||||
|
||||
class DianXMLExtensionSignerVerifier:
|
||||
|
||||
def __init__(self, pkcs12_path_or_bytes, passphrase=None, localpolicy=True):
|
||||
def __init__(
|
||||
self,
|
||||
pkcs12_path_or_bytes,
|
||||
passphrase=None,
|
||||
localpolicy=True):
|
||||
self._pkcs12_path_or_bytes = pkcs12_path_or_bytes
|
||||
self._passphrase = None
|
||||
self._localpolicy = localpolicy
|
||||
@@ -596,8 +711,8 @@ class DianXMLExtensionSignerVerifier:
|
||||
if isinstance(self._pkcs12_path_or_bytes, str):
|
||||
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
|
||||
ctx = xades.XAdESContext()
|
||||
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data,
|
||||
self._passphrase))
|
||||
ctx.load_pkcs12(pkcs12.load_key_and_certificates(
|
||||
pkcs12_data, self._passphrase))
|
||||
try:
|
||||
if self._localpolicy:
|
||||
with mock_xades_policy():
|
||||
@@ -605,5 +720,5 @@ class DianXMLExtensionSignerVerifier:
|
||||
else:
|
||||
ctx.verify(signature)
|
||||
return True
|
||||
except:
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
# from functools import reduce
|
||||
# import copy
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime
|
||||
# from collections import defaultdict
|
||||
import decimal
|
||||
from decimal import Decimal
|
||||
import typing
|
||||
from ..data.dian import codelist
|
||||
|
||||
DECIMAL_PRECISION = 6
|
||||
@@ -60,10 +61,9 @@ class AmountCollection(Collection):
|
||||
|
||||
class Amount:
|
||||
def __init__(
|
||||
self, amount: int or float or str,
|
||||
currency: Currency = Currency('COP'),
|
||||
precision=DECIMAL_PRECISION):
|
||||
self.precision = precision
|
||||
self, amount: int | float | str | "Amount",
|
||||
currency: Currency = Currency('COP')):
|
||||
|
||||
# DIAN 1.7.-2020: 1.2.3.1
|
||||
if isinstance(amount, Amount):
|
||||
if amount < Amount(0.0):
|
||||
@@ -77,10 +77,9 @@ class Amount:
|
||||
|
||||
self.amount = Decimal(
|
||||
amount, decimal.Context(
|
||||
prec=self.precision,
|
||||
prec=DECIMAL_PRECISION,
|
||||
# DIAN 1.7.-2020: 1.2.1.1
|
||||
rounding=decimal.ROUND_HALF_EVEN)
|
||||
)
|
||||
rounding=decimal.ROUND_HALF_EVEN))
|
||||
self.currency = currency
|
||||
|
||||
def fromNumber(self, val):
|
||||
@@ -98,24 +97,20 @@ class Amount:
|
||||
def __lt__(self, other):
|
||||
if not self.is_same_currency(other):
|
||||
raise AmountCurrencyError()
|
||||
return round(self.amount, self.precision) < round(other, 2)
|
||||
return round(self.amount, DECIMAL_PRECISION) < round(other, 2)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not self.is_same_currency(other):
|
||||
raise AmountCurrencyError()
|
||||
return round(self.amount, self.precision) == round(
|
||||
other.amount, self.precision)
|
||||
return round(self.amount, DECIMAL_PRECISION) == round(
|
||||
other.amount, DECIMAL_PRECISION)
|
||||
|
||||
def _cast(self, val):
|
||||
if type(val) in [int, float]:
|
||||
return self.fromNumber(val)
|
||||
if isinstance(val, Amount):
|
||||
return val
|
||||
|
||||
if isinstance(val, Decimal):
|
||||
return self.fromNumber(float(val))
|
||||
|
||||
raise TypeError("cant cast %s to amount" % (type(val)))
|
||||
raise TypeError("cant cast to amount")
|
||||
|
||||
def __add__(self, rother):
|
||||
other = self._cast(rother)
|
||||
@@ -285,22 +280,11 @@ class Responsability:
|
||||
raise ValueError("code %s not found" % (code))
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaxScheme:
|
||||
code: str
|
||||
name: str = ''
|
||||
|
||||
def __post_init__(self):
|
||||
if self.code not in codelist.TipoImpuesto:
|
||||
raise ValueError("code not found")
|
||||
self.name = codelist.TipoImpuesto[self.code]['name']
|
||||
|
||||
|
||||
@dataclass
|
||||
class Party:
|
||||
name: str
|
||||
ident: str
|
||||
responsability_code: typing.List[Responsability]
|
||||
responsability_code: list[Responsability]
|
||||
responsability_regime_code: str
|
||||
organization_code: str
|
||||
tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01'))
|
||||
@@ -333,7 +317,7 @@ class TaxScheme:
|
||||
@dataclass
|
||||
class TaxSubTotal:
|
||||
percent: float
|
||||
scheme: typing.Optional[TaxScheme] = None
|
||||
scheme: TaxScheme | None = None
|
||||
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
|
||||
|
||||
def calculate(self, invline):
|
||||
@@ -365,7 +349,7 @@ class TaxTotalOmit(TaxTotal):
|
||||
@dataclass
|
||||
class WithholdingTaxSubTotal:
|
||||
percent: float
|
||||
scheme: typing.Optional[TaxScheme] = None
|
||||
scheme: TaxScheme | None = None
|
||||
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
|
||||
|
||||
def calculate(self, invline):
|
||||
@@ -497,7 +481,7 @@ class AllowanceCharge:
|
||||
reason: AllowanceChargeReason = None
|
||||
|
||||
# Valor Base para calcular el descuento o el cargo
|
||||
base_amount: typing.Optional[Amount] = field(
|
||||
base_amount: Amount | None = field(
|
||||
default_factory=lambda: Amount(0.0))
|
||||
|
||||
# Porcentaje: Porcentaje que aplicar.
|
||||
@@ -542,9 +526,9 @@ class InvoiceLine:
|
||||
# ya que al reportar los totales es sobre
|
||||
# la factura y el percent es unico por type_code
|
||||
# de subtotal
|
||||
tax: typing.Optional[TaxTotal]
|
||||
withholding: typing.Optional[WithholdingTaxTotal]
|
||||
allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(
|
||||
tax: TaxTotal | None
|
||||
withholding: WithholdingTaxTotal | None
|
||||
allowance_charge: list[AllowanceCharge] = dataclasses.field(
|
||||
default_factory=list)
|
||||
|
||||
def add_allowance_charge(self, charge):
|
||||
|
||||
@@ -6,16 +6,19 @@ from .. import form
|
||||
from ..fe import fe_from_string
|
||||
from datetime import datetime
|
||||
|
||||
def billing_reference(xmldocument: str, klass: form.BillingReference) -> form.BillingReference:
|
||||
|
||||
def billing_reference(
|
||||
xmldocument: str,
|
||||
klass: form.BillingReference) -> form.BillingReference:
|
||||
"""
|
||||
construye BillingReference desde XMLDOCUMENT
|
||||
usando KLASS como clase.
|
||||
"""
|
||||
if not issubclass(klass, form.BillingReference):
|
||||
raise TypeError('klass expected subclass of BillingReference')
|
||||
|
||||
|
||||
fachoxml = fe_from_string(xmldocument)
|
||||
|
||||
|
||||
uid = fachoxml.get_element_text('./cbc:ID')
|
||||
uuid = fachoxml.get_element_text('./cbc:UUID')
|
||||
issue_date = fachoxml.get_element_text('./cbc:IssueDate')
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
from .invoice import *
|
||||
from .credit_note import *
|
||||
from .debit_note import *
|
||||
from .utils import *
|
||||
from .support_document import *
|
||||
from .support_document_credit_note import *
|
||||
from .attached_document import *
|
||||
from .application_response import *
|
||||
from .invoice import DIANInvoiceXML
|
||||
from .credit_note import DIANCreditNoteXML
|
||||
from .debit_note import DIANDebitNoteXML
|
||||
from .utils import DIANWrite, DIANWriteSigned
|
||||
from .attached_document import AttachedDocument
|
||||
from .support_document import DIANSupportDocumentXML
|
||||
from .support_document_credit_note import DIANSupportDocumentCreditNoteXML
|
||||
|
||||
__all__ = [
|
||||
'DIANInvoiceXML',
|
||||
'DIANCreditNoteXML',
|
||||
'DIANDebitNoteXML',
|
||||
'DIANWrite',
|
||||
'DIANWriteSigned',
|
||||
'AttachedDocument',
|
||||
'DIANSupportDocumentXML',
|
||||
'DIANSupportDocumentCreditNoteXML',
|
||||
]
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
from .. import fe
|
||||
|
||||
__all__ = ['ApplicationResponse']
|
||||
|
||||
|
||||
class ApplicationResponse:
|
||||
|
||||
def __init__(self, invoice, tag_document='ApplicationResponse'):
|
||||
self.schema =\
|
||||
'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2'
|
||||
self.tag_document = tag_document
|
||||
self.invoice = invoice
|
||||
self.fexml = fe.FeXML(
|
||||
self.tag_document, self.schema)
|
||||
self.application_response = self.application_response()
|
||||
|
||||
def application_response(self):
|
||||
# DIAN 1.9.-2023: AE02
|
||||
self.fexml.set_element(
|
||||
'./cbc:UBLVersionID', 'UBL 2.1')
|
||||
|
||||
# DIAN 1.9.-2023: AE03
|
||||
self.fexml.set_element(
|
||||
'./cbc:CustomizationID', 'Documentos adjuntos')
|
||||
|
||||
# DIAN 1.9.-2023: AE04
|
||||
self.fexml.set_element(
|
||||
'./cbc:ProfileID', 'DIAN 2.1')
|
||||
|
||||
# DIAN 1.9.-2023: AE04a
|
||||
self.fexml.set_element(
|
||||
'./cbc:ProfileExecutionID', '1')
|
||||
|
||||
self.fexml.set_element(
|
||||
'./cbc:ID', '1')
|
||||
|
||||
self.fexml.set_element(
|
||||
'./cbc:UUID', '1', schemeName="CUDE-SHA384")
|
||||
|
||||
self.fexml.set_element(
|
||||
'./cbc:IssueDate',
|
||||
self.invoice.invoice_issue.strftime('%Y-%m-%d'))
|
||||
|
||||
# DIAN 1.9.-2023: AE06
|
||||
self.fexml.set_element(
|
||||
'./cbc:IssueTime', self.invoice.invoice_issue.strftime(
|
||||
'%H:%M:%S-05:00'))
|
||||
|
||||
self.set_sender_party()
|
||||
self.set_receiver_party()
|
||||
self.set_document_response()
|
||||
|
||||
def set_sender_party(self):
|
||||
# DIAN 1.9.-2023: AE09
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:SenderParty')
|
||||
# DIAN 1.9.-2023: AE10
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme')
|
||||
# DIAN 1.9.-2023: AE11
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cbc:RegistrationName',
|
||||
self.invoice.invoice_supplier.name)
|
||||
# DIAN 1.9.-2023: AE12
|
||||
# DIAN 1.9.-2023: AE13
|
||||
# DIAN 1.9.-2023: AE14
|
||||
# DIAN 1.9.-2023: AE15
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cbc:CompanyID',
|
||||
self.invoice.invoice_supplier.ident,
|
||||
schemeAgencyID='195',
|
||||
schemeID=self.invoice.invoice_supplier.ident.dv,
|
||||
schemeName=self.invoice.invoice_supplier.ident.type_fiscal)
|
||||
|
||||
# DIAN 1.9.-2023: AE16
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
|
||||
self.invoice.invoice_supplier.responsability_code)
|
||||
|
||||
# DIAN 1.9.-2023: AE18
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme')
|
||||
|
||||
# DIAN 1.9.-2023: AE19
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
|
||||
self.invoice.invoice_supplier.tax_scheme.code)
|
||||
|
||||
# DIAN 1.9.-2023: AE20
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
|
||||
self.invoice.invoice_supplier.tax_scheme.name)
|
||||
|
||||
def set_receiver_party(self):
|
||||
# DIAN 1.9.-2023: AE21
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ReceiverParty')
|
||||
# DIAN 1.9.-2023: AE22
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme')
|
||||
# DIAN 1.9.-2023: AE23
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:RegistrationName',
|
||||
self.invoice.invoice_customer.name)
|
||||
# DIAN 1.9.-2023: AE24
|
||||
# DIAN 1.9.-2023: AE25
|
||||
# DIAN 1.9.-2023: AE26
|
||||
# DIAN 1.9.-2023: AE27
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:CompanyID',
|
||||
self.invoice.invoice_customer.ident,
|
||||
schemeAgencyID='195',
|
||||
schemeID=self.invoice.invoice_customer.ident.dv,
|
||||
schemeName=self.invoice.invoice_customer.ident.type_fiscal)
|
||||
# DIAN 1.9.-2023: AE28
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
|
||||
self.invoice.invoice_customer.responsability_code)
|
||||
# DIAN 1.9.-2023: AE30
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme')
|
||||
# DIAN 1.9.-2023: AE31
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
|
||||
self.invoice.invoice_customer.tax_scheme.code)
|
||||
# DIAN 1.9.-2023: AE32
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
|
||||
self.invoice.invoice_customer.tax_scheme.name)
|
||||
|
||||
def set_document_response(self):
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:DocumentResponse')
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:DocumentResponse/cac:Response')
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:Response/cbc:ResponseCode',
|
||||
'02')
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:Response/cbc:Description',
|
||||
'Documento validado por la DIAN')
|
||||
self.set_documnent_reference()
|
||||
|
||||
def set_documnent_reference(self):
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:DocumentResponse/cac:DocumentReference')
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:DocumentReference/cbc:ID',
|
||||
'FESS19566058')
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:DocumentReference/cbc:UUID',
|
||||
'f51ee529aabd19d10e39444f2f593b94d56d5885fbf433faf718d53a7e968f64bf54a6ee43c6a2df842771b54a6aae1a',
|
||||
schemeName="CUFE-SHA384")
|
||||
self.set_response_lines()
|
||||
|
||||
def set_response_lines(self):
|
||||
lines = [{
|
||||
'LineID': '1',
|
||||
'ResponseCode': '0000',
|
||||
'Description': '0',
|
||||
}]
|
||||
|
||||
for line in lines:
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:LineResponse/cac:LineReference/cbc:LineID', line[
|
||||
'LineID'])
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:LineResponse/cac:Response/cbc:ResponseCode', line[
|
||||
'ResponseCode'])
|
||||
self.fexml.set_element(
|
||||
'./cac:DocumentResponse/cac:LineResponse/cac:Response/cbc:Description', line[
|
||||
'Description'])
|
||||
|
||||
|
||||
|
||||
def toFachoXML(self):
|
||||
return self.fexml
|
||||
@@ -1,227 +1,15 @@
|
||||
from .. import fe
|
||||
from .application_response import ApplicationResponse
|
||||
|
||||
__all__ = ['AttachedDocument']
|
||||
|
||||
|
||||
class AttachedDocument():
|
||||
|
||||
def __init__(self, invoice, DIANInvoiceXML, id):
|
||||
self.schema =\
|
||||
def __init__(self, id):
|
||||
schema =\
|
||||
'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2'
|
||||
self.id = id
|
||||
self.invoice = invoice
|
||||
self.DIANInvoiceXML = DIANInvoiceXML
|
||||
self.attached_document_invoice = self.attached_document_invoice()
|
||||
|
||||
def attached_document_invoice(self):
|
||||
self.fexml = fe.FeXML(
|
||||
'AttachedDocument', self.schema)
|
||||
|
||||
# DIAN 1.9.-2023: AE02
|
||||
self.fexml.set_element(
|
||||
'./cbc:UBLVersionID', 'UBL 2.1')
|
||||
|
||||
# DIAN 1.9.-2023: AE03
|
||||
self.fexml.set_element(
|
||||
'./cbc:CustomizationID', 'Documentos adjuntos')
|
||||
|
||||
# DIAN 1.9.-2023: AE04
|
||||
self.fexml.set_element(
|
||||
'./cbc:ProfileID', 'Factura Electrónica de Venta')
|
||||
|
||||
# DIAN 1.9.-2023: AE04a
|
||||
self.fexml.set_element(
|
||||
'./cbc:ProfileExecutionID', '1')
|
||||
|
||||
# DIAN 1.9.-2023: AE04b
|
||||
self.fexml.set_element(
|
||||
'./cbc:ID', self.id)
|
||||
|
||||
# DIAN 1.9.-2023: AE05
|
||||
self.fexml.set_element(
|
||||
'./cbc:IssueDate',
|
||||
self.invoice.invoice_issue.strftime('%Y-%m-%d'))
|
||||
|
||||
# DIAN 1.9.-2023: AE06
|
||||
self.fexml.set_element(
|
||||
'./cbc:IssueTime', self.invoice.invoice_issue.strftime(
|
||||
'%H:%M:%S-05:00'))
|
||||
|
||||
# DIAN 1.9.-2023: AE08
|
||||
self.fexml.set_element(
|
||||
'./cbc:DocumentType', 'Contenedor de Factura Electrónica')
|
||||
|
||||
# DIAN 1.9.-2023: AE08a
|
||||
self.fexml.set_element(
|
||||
'./cbc:ParentDocumentID', self.invoice.invoice_ident)
|
||||
|
||||
# DIAN 1.9.-2023: AE09
|
||||
self.set_sender_party()
|
||||
|
||||
# DIAN 1.9.-2023: AE20
|
||||
self.set_receiver_party()
|
||||
# DIAN 1.9.-2023: AE33
|
||||
self.set_attachment()
|
||||
self.set_parent_document_line_reference()
|
||||
|
||||
def set_sender_party(self):
|
||||
# DIAN 1.9.-2023: AE09
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:SenderParty')
|
||||
# DIAN 1.9.-2023: AE10
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme')
|
||||
# DIAN 1.9.-2023: AE11
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cbc:RegistrationName',
|
||||
self.invoice.invoice_supplier.name)
|
||||
# DIAN 1.9.-2023: AE12
|
||||
# DIAN 1.9.-2023: AE13
|
||||
# DIAN 1.9.-2023: AE14
|
||||
# DIAN 1.9.-2023: AE15
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cbc:CompanyID',
|
||||
self.invoice.invoice_supplier.ident,
|
||||
schemeAgencyID='195',
|
||||
schemeID=self.invoice.invoice_supplier.ident.dv,
|
||||
schemeName=self.invoice.invoice_supplier.ident.type_fiscal)
|
||||
|
||||
# DIAN 1.9.-2023: AE16
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
|
||||
self.invoice.invoice_supplier.responsability_code)
|
||||
|
||||
# DIAN 1.9.-2023: AE18
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme')
|
||||
|
||||
# DIAN 1.9.-2023: AE19
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
|
||||
self.invoice.invoice_supplier.tax_scheme.code)
|
||||
|
||||
# DIAN 1.9.-2023: AE20
|
||||
self.fexml.set_element(
|
||||
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
|
||||
self.invoice.invoice_supplier.tax_scheme.name)
|
||||
|
||||
def set_receiver_party(self):
|
||||
# DIAN 1.9.-2023: AE21
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ReceiverParty')
|
||||
# DIAN 1.9.-2023: AE22
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme')
|
||||
# DIAN 1.9.-2023: AE23
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:RegistrationName',
|
||||
self.invoice.invoice_customer.name)
|
||||
# DIAN 1.9.-2023: AE24
|
||||
# DIAN 1.9.-2023: AE25
|
||||
# DIAN 1.9.-2023: AE26
|
||||
# DIAN 1.9.-2023: AE27
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:CompanyID',
|
||||
self.invoice.invoice_customer.ident,
|
||||
schemeAgencyID='195',
|
||||
schemeID=self.invoice.invoice_customer.ident.dv,
|
||||
schemeName=self.invoice.invoice_customer.ident.type_fiscal)
|
||||
# DIAN 1.9.-2023: AE28
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
|
||||
self.invoice.invoice_customer.responsability_code)
|
||||
# DIAN 1.9.-2023: AE30
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme')
|
||||
# DIAN 1.9.-2023: AE31
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
|
||||
self.invoice.invoice_customer.tax_scheme.code)
|
||||
# DIAN 1.9.-2023: AE32
|
||||
self.fexml.set_element(
|
||||
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
|
||||
self.invoice.invoice_customer.tax_scheme.name)
|
||||
|
||||
def set_attachment(self):
|
||||
# DIAN 1.9.-2023: AE33
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:Attachment')
|
||||
# DIAN 1.9.-2023: AE34
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:Attachment/cac:ExternalReference')
|
||||
# DIAN 1.9.-2023: AE35
|
||||
self.fexml.set_element(
|
||||
'./cac:Attachment/cac:ExternalReference/cbc:MimeCode',
|
||||
'text/xml')
|
||||
# DIAN 1.9.-2023: AE36
|
||||
self.fexml.set_element(
|
||||
'./cac:Attachment/cac:ExternalReference/cbc:EncodingCode',
|
||||
'UTF-8')
|
||||
# DIAN 1.9.-2023: AE37
|
||||
self.fexml.set_element(
|
||||
'./cac:Attachment/cac:ExternalReference/cbc:Description',
|
||||
self._build_attachment(self.DIANInvoiceXML)
|
||||
)
|
||||
|
||||
def set_parent_document_line_reference(self):
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ParentDocumentLineReference')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cbc:LineID', 1)
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:ID',
|
||||
'1234')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:UUID',
|
||||
'1234',
|
||||
schemeName="CUFE-SHA384")
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:IssueDate',
|
||||
'2024-11-28')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:DocumentType',
|
||||
'ApplicationResponse')
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:MimeCode',
|
||||
'text/xml')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:EncodingCode',
|
||||
'UTF-8')
|
||||
|
||||
application_response = ApplicationResponse(
|
||||
self.invoice).toFachoXML()
|
||||
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:Description',
|
||||
self._build_attachment(application_response))
|
||||
self.fexml.placeholder_for(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidatorID',
|
||||
'Unidad Especial Dirección de Impuestos y Aduanas Nacionales')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidationResultCode',
|
||||
'02')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidationDate',
|
||||
'2024-11-28')
|
||||
self.fexml.set_element(
|
||||
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidationTime',
|
||||
'10:35:11-05:00')
|
||||
|
||||
def _build_attachment(self, DIANInvoiceXML):
|
||||
document = (
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
|
||||
) + DIANInvoiceXML.tostring()
|
||||
attachment = "<![CDATA[{}]]>".format(
|
||||
document)
|
||||
|
||||
return attachment
|
||||
self.fexml = fe.FeXML('AttachedDocument', schema)
|
||||
self.fexml.set_element('./cbc:ID', id)
|
||||
|
||||
def toFachoXML(self):
|
||||
return self.fexml
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,36 +14,45 @@ __all__ = ['DIANSupportDocumentXML']
|
||||
class DIANSupportDocumentXML(fe.FeXML):
|
||||
"""
|
||||
DianSupportDocumentXML mapea objeto form.Invoice a XML segun
|
||||
lo indicado para él Documento soporte en adquisiciones efectuadas con sujetos no obligados a expedir factura de venta o documento equivalente.
|
||||
lo indicado para él Documento soporte en adquisiciones efectuadas con
|
||||
sujetos no obligados a expedir factura de venta o documento equivalente.
|
||||
"""
|
||||
|
||||
def __init__(self, invoice, tag_document='Invoice'):
|
||||
super().__init__(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
super().__init__(
|
||||
tag_document,
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
|
||||
# DIAN 1.1.-2021: DSAB03
|
||||
# DIAN 1.1.-2021: NSAB03
|
||||
self.placeholder_for(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl')
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:InvoiceControl')
|
||||
|
||||
# DIAN 1.1.-2021: DSAB13
|
||||
# DIAN 1.1.-2021: NSAB13
|
||||
self.placeholder_for(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource')
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:InvoiceSource')
|
||||
|
||||
# DIAN 1.1.-2021: DSAB18
|
||||
# DIAN 1.1.-2021: NSAB18
|
||||
self.placeholder_for(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider')
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:SoftwareProvider')
|
||||
|
||||
# DIAN 1.1.-2021: DSAB27
|
||||
# DIAN 1.1.-2021: NSAB27
|
||||
self.placeholder_for(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode')
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
|
||||
':DianExtensions/sts:SoftwareSecurityCode')
|
||||
|
||||
# DIAN 1.1.-2021: DSAB30 DSAB31
|
||||
# DIAN 1.1.-2021: NSAB30 NSAB31
|
||||
self.placeholder_for(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/'
|
||||
'sts:DianExtensions/sts:AuthorizationProvider/'
|
||||
'sts:AuthorizationProviderID')
|
||||
|
||||
# ZE02 se requiere existencia para firmar
|
||||
# DIAN 1.1.-2021: DSAA02 DSAB01
|
||||
@@ -52,7 +61,7 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
'./ext:UBLExtensions/ext:UBLExtension', append=True)
|
||||
# DIAN 1.1.-2021: DSAB02
|
||||
# DIAN 1.1.-2021: NSAB02
|
||||
extcontent = ublextension.find_or_create_element(
|
||||
ublextension.find_or_create_element(
|
||||
'/ext:UBLExtension/ext:ExtensionContent')
|
||||
self.attach_invoice(invoice)
|
||||
|
||||
@@ -70,55 +79,65 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
# DIAN 1.1.-2021: DSAJ07 DSAJ08
|
||||
# DIAN 1.1.-2021: NSAJ07 NSAJ08
|
||||
fexml.placeholder_for(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address')
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address')
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ09
|
||||
# DIAN 1.1.-2021: NSAJ09
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cbc:ID',
|
||||
invoice.invoice_supplier.address.city.code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ10
|
||||
# DIAN 1.1.-2021: NSAJ10
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cbc:CityName',
|
||||
invoice.invoice_supplier.address.city.name)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ73
|
||||
# DIAN 1.1.-2021: NSAJ73
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:PostalZone',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cbc:PostalZone',
|
||||
invoice.invoice_supplier.address.postalzone.code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ11
|
||||
# DIAN 1.1.-2021: NSAJ11
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cbc:CountrySubentity',
|
||||
invoice.invoice_supplier.address.countrysubentity.name)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ12
|
||||
# DIAN 1.1.-2021: NSAJ12
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cbc:CountrySubentityCode',
|
||||
invoice.invoice_supplier.address.countrysubentity.code)
|
||||
# DIAN 1.1.-2021: NSAJ13 NSAJ14
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cac:AddressLine/cbc:Line',
|
||||
invoice.invoice_supplier.address.street)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ15 DSAJ16
|
||||
# DIAN 1.1.-2021: NSAJ15 NSAJ16
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cac:Country/cbc:IdentificationCode',
|
||||
invoice.invoice_supplier.address.country.code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ17
|
||||
# DIAN 1.1.-2021: NSAJ17
|
||||
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
|
||||
invoice.invoice_supplier.address.country.name,
|
||||
# DIAN 1.1.-2021: DSAJ18
|
||||
# # DIAN 1.1.-2021: NSAJ18
|
||||
languageID='es')
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
|
||||
':Address/cac:Country/cbc:Name',
|
||||
invoice.invoice_supplier.address.country.name,
|
||||
# DIAN 1.1.-2021: DSAJ18
|
||||
# # DIAN 1.1.-2021: NSAJ18
|
||||
languageID='es')
|
||||
|
||||
supplier_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
|
||||
supplier_company_id_attrs.update(
|
||||
@@ -134,13 +153,15 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
# DIAN 1.1.-2021: DSAJ20
|
||||
# DIAN 1.1.-2021: NSAJ20
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
|
||||
':RegistrationName',
|
||||
invoice.invoice_supplier.legal_name)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ21
|
||||
# DIAN 1.1.-2021: NSAJ21
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
|
||||
':CompanyID',
|
||||
invoice.invoice_supplier.ident,
|
||||
# DIAN 1.1.-2021: DSAJ22 DSAJ23 DSAJ24 DSAJ25
|
||||
# DIAN 1.1.-2021: NSAJ22 NSAJ23 NSAJ24 NSAJ25
|
||||
@@ -149,25 +170,29 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
# DIAN 1.1.-2021: DSAJ26
|
||||
# DIAN 1.1.-2021: NSAJ26
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
|
||||
':TaxLevelCode',
|
||||
invoice.invoice_supplier.responsability_code,
|
||||
listName=invoice.invoice_supplier.responsability_regime_code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ39
|
||||
# DIAN 1.1.-2021: NSAJ39
|
||||
fexml.placeholder_for(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
|
||||
':TaxScheme')
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ40
|
||||
# DIAN 1.1.-2021: NSAJ40
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
|
||||
':TaxScheme/cbc:ID',
|
||||
invoice.invoice_customer.tax_scheme.code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAJ41
|
||||
# DIAN 1.1.-2021: NSAJ41
|
||||
fexml.set_element(
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
|
||||
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
|
||||
':TaxScheme/cbc:Name',
|
||||
invoice.invoice_customer.tax_scheme.name)
|
||||
|
||||
def set_customer(fexml, invoice):
|
||||
@@ -193,7 +218,8 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
# DIAN 1.1.-2021: DSAK20
|
||||
# DIAN 1.1.-2021: NSAK20
|
||||
fexml.set_element(
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
|
||||
':RegistrationName',
|
||||
invoice.invoice_customer.legal_name)
|
||||
|
||||
customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
|
||||
@@ -205,7 +231,8 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
# DIAN 1.1.-2021: DSAK21
|
||||
# DIAN 1.1.-2021: NSAK21
|
||||
fexml.set_element(
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
|
||||
':CompanyID',
|
||||
invoice.invoice_customer.ident,
|
||||
# DIAN 1.1.-2021: DSAK22 DSAK23 DSAK24 DSAK25
|
||||
# DIAN 1.1.-2021: NSAK22 NSAK23 NSAK24 NSAK25
|
||||
@@ -214,24 +241,28 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
# DIAN 1.1.-2021: DSAK26
|
||||
# DIAN 1.1.-2021: NSAK26
|
||||
fexml.set_element(
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
|
||||
':TaxLevelCode',
|
||||
invoice.invoice_customer.responsability_code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAK39
|
||||
# DIAN 1.1.-2021: NSAK39
|
||||
fexml.placeholder_for(
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
|
||||
':TaxScheme')
|
||||
|
||||
# DIAN 1.1.-2021: DSAK40
|
||||
# DIAN 1.1.-2021: NSAK40
|
||||
fexml.set_element(
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
|
||||
':TaxScheme/cbc:ID',
|
||||
invoice.invoice_customer.tax_scheme.code)
|
||||
|
||||
# DIAN 1.1.-2021: DSAK41
|
||||
# DIAN 1.1.-2021: NSAK41
|
||||
fexml.set_element(
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
|
||||
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
|
||||
':TaxScheme/cbc:Name',
|
||||
invoice.invoice_customer.tax_scheme.name)
|
||||
|
||||
def set_payment_mean(fexml, invoice):
|
||||
@@ -328,7 +359,8 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
reference.uuid,
|
||||
schemeName=schemeName)
|
||||
fexml.set_element(
|
||||
'./cac:BillingReference/cac:InvoiceDocumentReference/cbc:IssueDate',
|
||||
'./cac:BillingReference/cac:InvoiceDocumentReference/'
|
||||
'cbc:IssueDate',
|
||||
reference.date.strftime("%Y-%m-%d"))
|
||||
|
||||
def set_billing_reference(fexml, invoice):
|
||||
@@ -419,14 +451,17 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
|
||||
if percent_for[cod_impuesto]:
|
||||
line.set_element(
|
||||
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
|
||||
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/'
|
||||
'cbc:Percent',
|
||||
percent_for[cod_impuesto])
|
||||
|
||||
line.set_element(
|
||||
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
|
||||
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
|
||||
'/cbc:ID',
|
||||
cod_impuesto)
|
||||
line.set_element(
|
||||
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', 'IVA')
|
||||
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
|
||||
'/cbc:Name', 'IVA')
|
||||
|
||||
# abstract method
|
||||
|
||||
@@ -455,7 +490,8 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
|
||||
if subtotal.percent is not None:
|
||||
line.set_element(
|
||||
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
|
||||
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
|
||||
':TaxCategory/cbc:Percent',
|
||||
'%0.2f' %
|
||||
round(
|
||||
subtotal.percent,
|
||||
@@ -464,10 +500,12 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
if subtotal.scheme is not None:
|
||||
# DIAN 1.7.-2020: FAX15
|
||||
line.set_element(
|
||||
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
|
||||
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
|
||||
':TaxCategory/cac:TaxScheme/cbc:ID',
|
||||
subtotal.scheme.code)
|
||||
line.set_element(
|
||||
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
|
||||
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
|
||||
':TaxCategory/cac:TaxScheme/cbc:Name',
|
||||
subtotal.scheme.name)
|
||||
|
||||
def set_invoice_line_tax(fexml, line, invoice_line):
|
||||
@@ -488,7 +526,8 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
|
||||
if subtotal.percent is not None:
|
||||
line.set_element(
|
||||
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
|
||||
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc'
|
||||
':Percent',
|
||||
'%0.2f' %
|
||||
round(
|
||||
subtotal.percent,
|
||||
@@ -497,10 +536,12 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
if subtotal.scheme is not None:
|
||||
# DIAN 1.7.-2020: FAX15
|
||||
line.set_element(
|
||||
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
|
||||
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
|
||||
':TaxScheme/cbc:ID',
|
||||
subtotal.scheme.code)
|
||||
line.set_element(
|
||||
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
|
||||
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
|
||||
':TaxScheme/cbc:Name',
|
||||
subtotal.scheme.name)
|
||||
|
||||
def set_invoice_lines(fexml, invoice):
|
||||
@@ -615,7 +656,9 @@ class DIANSupportDocumentXML(fe.FeXML):
|
||||
(fexml.tag_document()),
|
||||
invoice.invoice_type_code,
|
||||
listAgencyID='195',
|
||||
listAgencyName='No matching global declaration available for the validation root',
|
||||
listAgencyName=(
|
||||
'No matching global declaration available for the '
|
||||
'validation root'),
|
||||
listURI='http://www.dian.gov.co')
|
||||
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
|
||||
fexml.set_element(
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
import facho.model as model
|
||||
import facho.model.fields as fields
|
||||
import facho.fe.form as form
|
||||
from facho import fe
|
||||
from .common import *
|
||||
from . import dian
|
||||
|
||||
from datetime import date, datetime
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
import hashlib
|
||||
|
||||
|
||||
|
||||
class PhysicalLocation(model.Model):
|
||||
__name__ = 'PhysicalLocation'
|
||||
|
||||
address = fields.Many2One(Address, namespace='cac')
|
||||
|
||||
class PartyTaxScheme(model.Model):
|
||||
__name__ = 'PartyTaxScheme'
|
||||
|
||||
registration_name = fields.Many2One(Name, name='RegistrationName', namespace='cbc')
|
||||
company_id = fields.Many2One(ID, name='CompanyID', namespace='cbc')
|
||||
tax_level_code = fields.Many2One(ID, name='TaxLevelCode', namespace='cbc', default='ZZ')
|
||||
|
||||
|
||||
class Party(model.Model):
|
||||
__name__ = 'Party'
|
||||
|
||||
id = fields.Virtual(setter='_on_set_id')
|
||||
name = fields.Many2One(PartyName, namespace='cac')
|
||||
|
||||
tax_scheme = fields.Many2One(PartyTaxScheme, namespace='cac')
|
||||
location = fields.Many2One(PhysicalLocation, namespace='cac')
|
||||
contact = fields.Many2One(Contact, namespace='cac')
|
||||
|
||||
def _on_set_id(self, name, value):
|
||||
self.tax_scheme.company_id = value
|
||||
return value
|
||||
|
||||
class AccountingCustomerParty(model.Model):
|
||||
__name__ = 'AccountingCustomerParty'
|
||||
|
||||
party = fields.Many2One(Party, namespace='cac')
|
||||
|
||||
class AccountingSupplierParty(model.Model):
|
||||
__name__ = 'AccountingSupplierParty'
|
||||
|
||||
party = fields.Many2One(Party, namespace='cac')
|
||||
|
||||
class Quantity(model.Model):
|
||||
__name__ = 'Quantity'
|
||||
|
||||
code = fields.Attribute('unitCode', default='NAR')
|
||||
|
||||
def __setup__(self):
|
||||
self.value = 0
|
||||
|
||||
def __default_set__(self, value):
|
||||
self.value = value
|
||||
return value
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
return self.value
|
||||
|
||||
class Amount(model.Model):
|
||||
__name__ = 'Amount'
|
||||
|
||||
currency = fields.Attribute('currencyID', default='COP')
|
||||
value = fields.Amount(name='amount', default=0.00, precision=2)
|
||||
|
||||
def __default_set__(self, value):
|
||||
self.value = value
|
||||
return value
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
return self.value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
class Price(model.Model):
|
||||
__name__ = 'Price'
|
||||
|
||||
amount = fields.Many2One(Amount, name='PriceAmount', namespace='cbc')
|
||||
|
||||
def __default_set__(self, value):
|
||||
self.amount = value
|
||||
return value
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
return self.amount
|
||||
|
||||
class Percent(model.Model):
|
||||
__name__ = 'Percent'
|
||||
|
||||
class TaxScheme(model.Model):
|
||||
__name__ = 'TaxScheme'
|
||||
|
||||
id = fields.Many2One(ID, namespace='cbc')
|
||||
name= fields.Many2One(Name, namespace='cbc')
|
||||
|
||||
class TaxCategory(model.Model):
|
||||
__name__ = 'TaxCategory'
|
||||
|
||||
percent = fields.Many2One(Percent, namespace='cbc')
|
||||
tax_scheme = fields.Many2One(TaxScheme, namespace='cac')
|
||||
|
||||
class TaxSubTotal(model.Model):
|
||||
__name__ = 'TaxSubTotal'
|
||||
|
||||
taxable_amount = fields.Many2One(Amount, name='TaxableAmount', namespace='cbc', default=0.00)
|
||||
tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00)
|
||||
tax_percent = fields.Many2One(Percent, namespace='cbc')
|
||||
tax_category = fields.Many2One(TaxCategory, namespace='cac')
|
||||
|
||||
percent = fields.Virtual(setter='set_category', getter='get_category')
|
||||
scheme = fields.Virtual(setter='set_category', getter='get_category')
|
||||
|
||||
def set_category(self, name, value):
|
||||
if name == 'percent':
|
||||
self.tax_category.percent = value
|
||||
# TODO(bit4bit) debe variar en conjunto?
|
||||
self.tax_percent = value
|
||||
elif name == 'scheme':
|
||||
self.tax_category.tax_scheme.id = value
|
||||
|
||||
return value
|
||||
|
||||
def get_category(self, name, value):
|
||||
if name == 'percent':
|
||||
return value
|
||||
elif name == 'scheme':
|
||||
return self.tax_category.tax_scheme
|
||||
|
||||
class TaxTotal(model.Model):
|
||||
__name__ = 'TaxTotal'
|
||||
|
||||
tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00)
|
||||
subtotals = fields.One2Many(TaxSubTotal, namespace='cac')
|
||||
|
||||
|
||||
class AllowanceCharge(model.Model):
|
||||
__name__ = 'AllowanceCharge'
|
||||
|
||||
amount = fields.Many2One(Amount, namespace='cbc')
|
||||
is_discount = fields.Virtual(default=False)
|
||||
|
||||
def isCharge(self):
|
||||
return self.is_discount == False
|
||||
|
||||
def isDiscount(self):
|
||||
return self.is_discount == True
|
||||
|
||||
class Taxes:
|
||||
class Scheme:
|
||||
def __init__(self, scheme):
|
||||
self.scheme = scheme
|
||||
|
||||
class Iva(Scheme):
|
||||
def __init__(self, percent):
|
||||
super().__init__('01')
|
||||
self.percent = percent
|
||||
|
||||
def calculate(self, amount):
|
||||
return form.Amount(amount) * form.Amount(self.percent / 100)
|
||||
|
||||
class InvoiceLine(model.Model):
|
||||
__name__ = 'InvoiceLine'
|
||||
|
||||
id = fields.Many2One(ID, namespace='cbc')
|
||||
quantity = fields.Many2One(Quantity, name='InvoicedQuantity', namespace='cbc')
|
||||
taxtotal = fields.Many2One(TaxTotal, namespace='cac')
|
||||
price = fields.Many2One(Price, namespace='cac')
|
||||
amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc')
|
||||
allowance_charge = fields.One2Many(AllowanceCharge, 'cac')
|
||||
tax_amount = fields.Virtual(getter='get_tax_amount')
|
||||
|
||||
def __setup__(self):
|
||||
self._taxs = defaultdict(list)
|
||||
self._subtotals = {}
|
||||
|
||||
def add_tax(self, tax):
|
||||
if not isinstance(tax, Taxes.Scheme):
|
||||
raise ValueError('tax expected TaxIva')
|
||||
|
||||
# inicialiamos subtotal para impuesto
|
||||
if not tax.scheme in self._subtotals:
|
||||
subtotal = self.taxtotal.subtotals.create()
|
||||
subtotal.scheme = tax.scheme
|
||||
|
||||
self._subtotals[tax.scheme] = subtotal
|
||||
|
||||
self._taxs[tax.scheme].append(tax)
|
||||
|
||||
def get_tax_amount(self, name, value):
|
||||
total = form.Amount(0)
|
||||
for (scheme, subtotal) in self._subtotals.items():
|
||||
total += subtotal.tax_amount
|
||||
|
||||
return total
|
||||
|
||||
@fields.on_change(['price', 'quantity'])
|
||||
def update_amount(self, name, value):
|
||||
charge = form.AmountCollection(self.allowance_charge)\
|
||||
.filter(lambda charge: charge.isCharge())\
|
||||
.map(lambda charge: charge.amount)\
|
||||
.sum()
|
||||
|
||||
discount = form.AmountCollection(self.allowance_charge)\
|
||||
.filter(lambda charge: charge.isDiscount())\
|
||||
.map(lambda charge: charge.amount)\
|
||||
.sum()
|
||||
|
||||
total = form.Amount(self.quantity) * form.Amount(self.price)
|
||||
self.amount = total + charge - discount
|
||||
|
||||
for (scheme, subtotal) in self._subtotals.items():
|
||||
subtotal.tax_amount = 0
|
||||
|
||||
for (scheme, taxes) in self._taxs.items():
|
||||
for tax in taxes:
|
||||
self._subtotals[scheme].tax_amount += tax.calculate(self.amount)
|
||||
|
||||
class LegalMonetaryTotal(model.Model):
|
||||
__name__ = 'LegalMonetaryTotal'
|
||||
|
||||
line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc', default=0)
|
||||
|
||||
tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount', namespace='cbc', default=form.Amount(0))
|
||||
tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount', namespace='cbc', default=form.Amount(0))
|
||||
charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount', namespace='cbc', default=form.Amount(0))
|
||||
payable_amount = fields.Many2One(Amount, name='PayableAmount', namespace='cbc', default=form.Amount(0))
|
||||
|
||||
@fields.on_change(['tax_inclusive_amount', 'charge_total'])
|
||||
def update_payable_amount(self, name, value):
|
||||
self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount
|
||||
|
||||
|
||||
class DIANExtensionContent(model.Model):
|
||||
__name__ = 'ExtensionContent'
|
||||
|
||||
dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts')
|
||||
|
||||
class DIANExtension(model.Model):
|
||||
__name__ = 'UBLExtension'
|
||||
|
||||
content = fields.Many2One(DIANExtensionContent, namespace='ext')
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
return self.content.dian
|
||||
|
||||
class UBLExtension(model.Model):
|
||||
__name__ = 'UBLExtension'
|
||||
|
||||
content = fields.Many2One(Element, name='ExtensionContent', namespace='ext', default='')
|
||||
|
||||
class UBLExtensions(model.Model):
|
||||
__name__ = 'UBLExtensions'
|
||||
|
||||
dian = fields.Many2One(DIANExtension, namespace='ext', create=True)
|
||||
extension = fields.Many2One(UBLExtension, namespace='ext', create=True)
|
||||
|
||||
class Invoice(model.Model):
|
||||
__name__ = 'Invoice'
|
||||
__namespace__ = fe.NAMESPACES
|
||||
|
||||
_ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext')
|
||||
# nos interesa el acceso solo los atributos de la DIAN
|
||||
dian = fields.Virtual(getter='get_dian_extension')
|
||||
|
||||
profile_id = fields.Many2One(Element, name='ProfileID', namespace='cbc', default='DIAN 2.1')
|
||||
profile_execute_id = fields.Many2One(Element, name='ProfileExecuteID', namespace='cbc', default='2')
|
||||
|
||||
id = fields.Many2One(ID, namespace='cbc')
|
||||
issue = fields.Virtual(setter='set_issue')
|
||||
issue_date = fields.Many2One(Date, name='IssueDate', namespace='cbc')
|
||||
issue_time = fields.Many2One(Time, name='IssueTime', namespace='cbc')
|
||||
|
||||
period = fields.Many2One(Period, name='InvoicePeriod', namespace='cac')
|
||||
|
||||
supplier = fields.Many2One(AccountingSupplierParty, namespace='cac')
|
||||
customer = fields.Many2One(AccountingCustomerParty, namespace='cac')
|
||||
legal_monetary_total = fields.Many2One(LegalMonetaryTotal, namespace='cac')
|
||||
lines = fields.One2Many(InvoiceLine, namespace='cac')
|
||||
|
||||
taxtotal_01 = fields.Many2One(TaxTotal)
|
||||
taxtotal_04 = fields.Many2One(TaxTotal)
|
||||
taxtotal_03 = fields.Many2One(TaxTotal)
|
||||
|
||||
def __setup__(self):
|
||||
self._namespace_prefix = 'fe'
|
||||
# Se requieren minimo estos impuestos para
|
||||
# validar el cufe
|
||||
self._subtotal_01 = self.taxtotal_01.subtotals.create()
|
||||
self._subtotal_01.scheme = '01'
|
||||
self._subtotal_01.percent = 19.0
|
||||
|
||||
self._subtotal_04 = self.taxtotal_04.subtotals.create()
|
||||
self._subtotal_04.scheme = '04'
|
||||
|
||||
self._subtotal_03 = self.taxtotal_03.subtotals.create()
|
||||
self._subtotal_03.scheme = '03'
|
||||
|
||||
def cufe(self, token, environment):
|
||||
|
||||
valor_bruto = self.legal_monetary_total.line_extension_amount
|
||||
valor_total_pagar = self.legal_monetary_total.payable_amount
|
||||
|
||||
valor_impuesto_01 = form.Amount(0.0)
|
||||
valor_impuesto_04 = form.Amount(0.0)
|
||||
valor_impuesto_03 = form.Amount(0.0)
|
||||
|
||||
for line in self.lines:
|
||||
for subtotal in line.taxtotal.subtotals:
|
||||
if subtotal.scheme.id == '01':
|
||||
valor_impuesto_01 += subtotal.tax_amount
|
||||
elif subtotal.scheme.id == '04':
|
||||
valor_impuesto_04 += subtotal.tax_amount
|
||||
elif subtotal.scheme.id == '03':
|
||||
valor_impuesto_03 += subtotal.tax_amount
|
||||
|
||||
pattern = [
|
||||
'%s' % str(self.id),
|
||||
'%s' % str(self.issue_date),
|
||||
'%s' % str(self.issue_time),
|
||||
valor_bruto.truncate_as_string(2),
|
||||
'01', valor_impuesto_01.truncate_as_string(2),
|
||||
'04', valor_impuesto_04.truncate_as_string(2),
|
||||
'03', valor_impuesto_03.truncate_as_string(2),
|
||||
valor_total_pagar.truncate_as_string(2),
|
||||
str(self.supplier.party.id),
|
||||
str(self.customer.party.id),
|
||||
str(token),
|
||||
str(environment)
|
||||
]
|
||||
|
||||
cufe = "".join(pattern)
|
||||
h = hashlib.sha384()
|
||||
h.update(cufe.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
@fields.on_change(['lines'])
|
||||
def update_legal_monetary_total(self, name, value):
|
||||
self.legal_monetary_total.line_extension_amount = 0
|
||||
self.legal_monetary_total.tax_inclusive_amount = 0
|
||||
|
||||
for line in self.lines:
|
||||
self.legal_monetary_total.line_extension_amount += line.amount
|
||||
self.legal_monetary_total.tax_inclusive_amount += line.amount + line.tax_amount
|
||||
|
||||
def set_issue(self, name, value):
|
||||
if not isinstance(value, datetime):
|
||||
raise ValueError('expected type datetime')
|
||||
self.issue_date = value.date()
|
||||
self.issue_time = value
|
||||
|
||||
def get_dian_extension(self, name, _value):
|
||||
return self._ubl_extensions.dian
|
||||
|
||||
def to_xml(self, **kw):
|
||||
# al generar documento el namespace
|
||||
# se hace respecto a la raiz
|
||||
return super().to_xml(**kw)\
|
||||
.replace("fe:", "")\
|
||||
.replace("xmlns:fe", "xmlns")
|
||||
@@ -1,90 +0,0 @@
|
||||
import facho.model as model
|
||||
import facho.model.fields as fields
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
__all__ = ['Element', 'PartyName', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country', 'Contact']
|
||||
|
||||
class Element(model.Model):
|
||||
"""
|
||||
Lo usuamos para elementos que solo manejan contenido
|
||||
"""
|
||||
__name__ = 'Element'
|
||||
|
||||
class Name(model.Model):
|
||||
__name__ = 'Name'
|
||||
|
||||
class Date(model.Model):
|
||||
__name__ = 'Date'
|
||||
|
||||
def __default_set__(self, value):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, date):
|
||||
return value.isoformat()
|
||||
|
||||
def __str__(self):
|
||||
return str(self._value)
|
||||
|
||||
class Time(model.Model):
|
||||
__name__ = 'Time'
|
||||
|
||||
def __default_set__(self, value):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, date):
|
||||
return value.strftime('%H:%M:%S-05:00')
|
||||
|
||||
def __str__(self):
|
||||
return str(self._value)
|
||||
|
||||
class Period(model.Model):
|
||||
__name__ = 'Period'
|
||||
|
||||
start_date = fields.Many2One(Date, name='StartDate', namespace='cbc')
|
||||
|
||||
end_date = fields.Many2One(Date, name='EndDate', namespace='cbc')
|
||||
|
||||
class ID(model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
return self._value
|
||||
|
||||
def __str__(self):
|
||||
return str(self._value)
|
||||
|
||||
|
||||
class Country(model.Model):
|
||||
__name__ = 'Country'
|
||||
|
||||
name = fields.Many2One(Element, name='Name', namespace='cbc')
|
||||
|
||||
class Address(model.Model):
|
||||
__name__ = 'Address'
|
||||
|
||||
#DIAN 1.7.-2020: FAJ08
|
||||
#DIAN 1.7.-2020: CAJ09
|
||||
id = fields.Many2One(Element, name='ID', namespace='cbc')
|
||||
|
||||
#DIAN 1.7.-2020: FAJ09
|
||||
#DIAN 1.7.-2020: CAJ10
|
||||
city = fields.Many2One(Element, name='CityName', namespace='cbc')
|
||||
|
||||
|
||||
class PartyName(model.Model):
|
||||
__name__ = 'PartyName'
|
||||
|
||||
name = fields.Many2One(Name, namespace='cbc')
|
||||
|
||||
def __default_set__(self, value):
|
||||
self.name = value
|
||||
return value
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
return self.name
|
||||
|
||||
class Contact(model.Model):
|
||||
__name__ = 'Contact'
|
||||
|
||||
email = fields.Many2One(Name, name='ElectronicEmail', namespace='cbc')
|
||||
@@ -1,58 +0,0 @@
|
||||
import facho.model as model
|
||||
import facho.model.fields as fields
|
||||
from .common import *
|
||||
|
||||
class DIANElement(Element):
|
||||
"""
|
||||
Elemento que contiene atributos por defecto.
|
||||
|
||||
Puede extender esta clase y modificar los atributos nuevamente
|
||||
"""
|
||||
__name__ = 'DIANElement'
|
||||
|
||||
scheme_id = fields.Attribute('schemeID', default='4')
|
||||
scheme_name = fields.Attribute('schemeName', default='31')
|
||||
scheme_agency_name = fields.Attribute('schemeAgencyName', default='CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)')
|
||||
scheme_agency_id = fields.Attribute('schemeAgencyID', default='195')
|
||||
|
||||
class SoftwareProvider(model.Model):
|
||||
__name__ = 'SoftwareProvider'
|
||||
|
||||
provider_id = fields.Many2One(Element, name='ProviderID', namespace='sts')
|
||||
software_id = fields.Many2One(Element, name='SoftwareID', namespace='sts')
|
||||
|
||||
class InvoiceSource(model.Model):
|
||||
__name__ = 'InvoiceSource'
|
||||
|
||||
identification_code = fields.Many2One(Element, name='IdentificationCode', namespace='sts', default='CO')
|
||||
|
||||
class AuthorizedInvoices(model.Model):
|
||||
__name__ = 'AuthorizedInvoices'
|
||||
|
||||
prefix = fields.Many2One(Element, name='Prefix', namespace='sts')
|
||||
from_range = fields.Many2One(Element, name='From', namespace='sts')
|
||||
to_range = fields.Many2One(Element, name='To', namespace='sts')
|
||||
|
||||
class InvoiceControl(model.Model):
|
||||
__name__ = 'InvoiceControl'
|
||||
|
||||
authorization = fields.Many2One(Element, name='InvoiceAuthorization', namespace='sts')
|
||||
period = fields.Many2One(Period, name='AuthorizationPeriod', namespace='sts')
|
||||
invoices = fields.Many2One(AuthorizedInvoices, namespace='sts')
|
||||
|
||||
class AuthorizationProvider(model.Model):
|
||||
__name__ = 'AuthorizationProvider'
|
||||
|
||||
|
||||
id = fields.Many2One(DIANElement, name='AuthorizationProviderID', namespace='sts', default='800197268')
|
||||
|
||||
class DianExtensions(model.Model):
|
||||
__name__ = 'DianExtensions'
|
||||
|
||||
authorization_provider = fields.Many2One(AuthorizationProvider, namespace='sts', create=True)
|
||||
|
||||
software_security_code = fields.Many2One(Element, name='SoftwareSecurityCode', namespace='sts')
|
||||
software_provider = fields.Many2One(SoftwareProvider, namespace='sts')
|
||||
source = fields.Many2One(InvoiceSource, namespace='sts')
|
||||
control = fields.Many2One(InvoiceControl, namespace='sts')
|
||||
|
||||
@@ -8,21 +8,91 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import typing
|
||||
|
||||
|
||||
from .. import fe
|
||||
from .. import form
|
||||
from ..data.dian import codelist
|
||||
|
||||
from .devengado import *
|
||||
from .deduccion import *
|
||||
from .trabajador import *
|
||||
from .empleador import *
|
||||
from .pago import *
|
||||
from .lugar import Lugar
|
||||
|
||||
from .amount import Amount
|
||||
from .exception import *
|
||||
from .deduccion import (
|
||||
Deduccion,
|
||||
DeduccionFondoPension,
|
||||
DeduccionSalud,
|
||||
)
|
||||
from .devengado import (
|
||||
Devengado,
|
||||
DevengadoBasico,
|
||||
DevengadoHoraExtra,
|
||||
DevengadoHorasExtrasDiarias,
|
||||
DevengadoHorasExtrasDiariasDominicalesYFestivos,
|
||||
DevengadoHorasExtrasNocturnas,
|
||||
DevengadoHorasExtrasNocturnasDominicalesYFestivos,
|
||||
DevengadoHorasRecargoDiariasDominicalesYFestivos,
|
||||
DevengadoHorasRecargoNocturno,
|
||||
DevengadoHorasRecargoNocturnoDominicalesYFestivos,
|
||||
DevengadoTransporte,
|
||||
)
|
||||
from .empleador import Empleador
|
||||
from .exception import DIANNominaIndividualError
|
||||
from .lugar import Lugar
|
||||
from .pago import (
|
||||
FormaPago,
|
||||
MetodoPago,
|
||||
Pago,
|
||||
)
|
||||
from .trabajador import (
|
||||
LugarTrabajo,
|
||||
SubTipoTrabajador,
|
||||
TipoContrato,
|
||||
TipoDocumento,
|
||||
TipoTrabajador,
|
||||
Trabajador,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Amount',
|
||||
'Deduccion',
|
||||
'DeduccionFondoPension',
|
||||
'DeduccionSalud',
|
||||
'Devengado',
|
||||
'DevengadoBasico',
|
||||
'DevengadoHoraExtra',
|
||||
'DevengadoHorasExtrasDiarias',
|
||||
'DevengadoHorasExtrasDiariasDominicalesYFestivos',
|
||||
'DevengadoHorasExtrasNocturnas',
|
||||
'DevengadoHorasExtrasNocturnasDominicalesYFestivos',
|
||||
'DevengadoHorasRecargoDiariasDominicalesYFestivos',
|
||||
'DevengadoHorasRecargoNocturno',
|
||||
'DevengadoHorasRecargoNocturnoDominicalesYFestivos',
|
||||
'DevengadoTransporte',
|
||||
'DIANNominaIndividual',
|
||||
'DIANNominaIndividualDeAjuste',
|
||||
'DIANNominaXML',
|
||||
'DIANNominaIndividualError',
|
||||
'DianXMLExtensionSigner',
|
||||
'Empleador',
|
||||
'Fecha',
|
||||
'FechaPago',
|
||||
'FormaPago',
|
||||
'InformacionGeneral',
|
||||
'Lugar',
|
||||
'LugarTrabajo',
|
||||
'Metadata',
|
||||
'MetodoPago',
|
||||
'Novedad',
|
||||
'NumeroSecuencia',
|
||||
'Pago',
|
||||
'Periodo',
|
||||
'PeriodoNomina',
|
||||
'Proveedor',
|
||||
'SubTipoTrabajador',
|
||||
'TipoContrato',
|
||||
'TipoDocumento',
|
||||
'TipoMoneda',
|
||||
'TipoTrabajador',
|
||||
'Trabajador',
|
||||
]
|
||||
|
||||
|
||||
class Fecha:
|
||||
def __init__(self, fecha):
|
||||
@@ -46,6 +116,7 @@ class Fecha:
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class FechaPago(Fecha):
|
||||
def apply(self, fragment):
|
||||
fragment.set_element('./FechaPago', self.value)
|
||||
@@ -82,39 +153,43 @@ class NumeroSecuencia:
|
||||
# NIE011
|
||||
Consecutivo=self.consecutivo,
|
||||
# NIE012
|
||||
Numero = numero)
|
||||
Numero=numero)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Periodo:
|
||||
fecha_ingreso: typing.Union[str, Fecha]
|
||||
fecha_liquidacion_inicio: typing.Union[str, Fecha]
|
||||
fecha_liquidacion_fin: typing.Union[str, Fecha]
|
||||
fecha_generacion: typing.Union[str, Fecha]
|
||||
fecha_ingreso: str | Fecha
|
||||
fecha_liquidacion_inicio: str | Fecha
|
||||
fecha_liquidacion_fin: str | Fecha
|
||||
fecha_generacion: str | Fecha
|
||||
|
||||
tiempo_laborado: int = 1
|
||||
fecha_retiro: typing.Union[str, Fecha] = None
|
||||
fecha_retiro: str | Fecha | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.fecha_ingreso = Fecha.cast(self.fecha_ingreso)
|
||||
self.fecha_liquidacion_inicio = Fecha.cast(self.fecha_liquidacion_inicio)
|
||||
self.fecha_liquidacion_inicio = Fecha.cast(
|
||||
self.fecha_liquidacion_inicio)
|
||||
self.fecha_liquidacion_fin = Fecha.cast(self.fecha_liquidacion_fin)
|
||||
self.fecha_retiro = Fecha.cast(self.fecha_retiro, optional=True)
|
||||
|
||||
|
||||
def apply(self, fragment):
|
||||
fragment.set_attributes('./Periodo',
|
||||
#NIE002
|
||||
# NIE002
|
||||
FechaIngreso=self.fecha_ingreso,
|
||||
#NIE003
|
||||
# NIE003
|
||||
FechaRetiro=self.fecha_retiro,
|
||||
#NIE004
|
||||
FechaLiquidacionInicio=self.fecha_liquidacion_inicio,
|
||||
#NIE005
|
||||
# NIE004
|
||||
FechaLiquidacionInicio=(
|
||||
self.fecha_liquidacion_inicio),
|
||||
# NIE005
|
||||
FechaLiquidacionFin=self.fecha_liquidacion_fin,
|
||||
#NIE006
|
||||
# NIE006
|
||||
TiempoLaborado=self.tiempo_laborado,
|
||||
#NIE008
|
||||
# NIE008
|
||||
FechaGen=self.fecha_generacion)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class Proveedor:
|
||||
razon_social: str
|
||||
@@ -140,29 +215,36 @@ class Proveedor:
|
||||
def post_apply(self, fexml, scopexml, fragment):
|
||||
cune_xpath = scopexml.xpath_from_root('/InformacionGeneral')
|
||||
cune = fexml.get_element_attribute(cune_xpath, 'CUNE')
|
||||
|
||||
ambiente = fexml.get_element_attribute(scopexml.xpath_from_root('/InformacionGeneral'), 'Ambiente')
|
||||
codigo_qr = f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={cune}"
|
||||
|
||||
ambiente = fexml.get_element_attribute(
|
||||
scopexml.xpath_from_root('/InformacionGeneral'), 'Ambiente')
|
||||
codigo_qr = (
|
||||
f"https://catalogo-vpfe.dian.gov.co/document/"
|
||||
f"searchqr?documentkey={cune}")
|
||||
|
||||
if InformacionGeneral.AMBIENTE_PRUEBAS == ambiente:
|
||||
codigo_qr = f"https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey={cune}"
|
||||
codigo_qr = (
|
||||
f"https://catalogo-vpfe-hab.dian.gov.co/document/"
|
||||
f"searchqr?documentkey={cune}")
|
||||
elif ambiente is None:
|
||||
raise RuntimeError('fail to get InformacionGeneral/@Ambiente')
|
||||
|
||||
|
||||
scopexml.set_element('./CodigoQR', codigo_qr)
|
||||
|
||||
# NIE020
|
||||
software_code = self._software_security_code(fexml, scopexml)
|
||||
fexml.set_attributes(scopexml.xpath_from_root('/ProveedorXML'), SoftwareSC=software_code)
|
||||
fexml.set_attributes(
|
||||
scopexml.xpath_from_root('/ProveedorXML'),
|
||||
SoftwareSC=software_code)
|
||||
|
||||
def _software_security_code(self, fexml, scopexml):
|
||||
|
||||
|
||||
# 8.2
|
||||
numero = fexml.get_element_attribute(scopexml.xpath_from_root('/NumeroSecuenciaXML'), 'Numero')
|
||||
numero = fexml.get_element_attribute(
|
||||
scopexml.xpath_from_root('/NumeroSecuenciaXML'), 'Numero')
|
||||
if numero is None:
|
||||
raise RuntimeError('fallo obtener NumeroSequenciaXML/@Numero')
|
||||
|
||||
|
||||
id_software = self.software_id
|
||||
software_pin = self.software_pin
|
||||
|
||||
@@ -172,7 +254,8 @@ class Proveedor:
|
||||
h = hashlib.sha384()
|
||||
h.update(code.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class Metadata:
|
||||
novedad: Novedad
|
||||
@@ -181,18 +264,33 @@ class Metadata:
|
||||
lugar_generacion: Lugar
|
||||
proveedor: Proveedor
|
||||
|
||||
def apply(self, novedad, numero_secuencia_xml, lugar_generacion_xml, proveedor_xml):
|
||||
def apply(
|
||||
self,
|
||||
novedad,
|
||||
numero_secuencia_xml,
|
||||
lugar_generacion_xml,
|
||||
proveedor_xml):
|
||||
if novedad:
|
||||
self.novedad.apply(novedad)
|
||||
self.secuencia.apply(numero_secuencia_xml)
|
||||
self.lugar_generacion.apply(lugar_generacion_xml, './LugarGeneracionXML')
|
||||
self.lugar_generacion.apply(
|
||||
lugar_generacion_xml,
|
||||
'./LugarGeneracionXML')
|
||||
self.proveedor.apply(proveedor_xml)
|
||||
|
||||
def post_apply(self, fexml, scopexml, novedad, numero_secuencia_xml, lugar_generacion_xml, proveedor_xml):
|
||||
def post_apply(
|
||||
self,
|
||||
fexml,
|
||||
scopexml,
|
||||
novedad,
|
||||
numero_secuencia_xml,
|
||||
lugar_generacion_xml,
|
||||
proveedor_xml):
|
||||
self.proveedor.post_apply(fexml, scopexml, proveedor_xml)
|
||||
if novedad:
|
||||
self.novedad.post_apply(fexml, scopexml, proveedor_xml)
|
||||
|
||||
self.novedad.post_apply(fexml, scopexml, proveedor_xml)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PeriodoNomina:
|
||||
code: str
|
||||
@@ -203,6 +301,7 @@ class PeriodoNomina:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.PeriodoNomina[self.code]['name']
|
||||
|
||||
|
||||
@dataclass
|
||||
class TipoMoneda:
|
||||
code: str
|
||||
@@ -213,6 +312,7 @@ class TipoMoneda:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.TipoMoneda[self.code]['name']
|
||||
|
||||
|
||||
@dataclass
|
||||
class InformacionGeneral:
|
||||
@dataclass
|
||||
@@ -235,7 +335,7 @@ class InformacionGeneral:
|
||||
valor: str = '2'
|
||||
|
||||
def __str__(self):
|
||||
self.valor
|
||||
self.valor
|
||||
|
||||
# TABLA 5.5.7
|
||||
@dataclass
|
||||
@@ -250,7 +350,7 @@ class InformacionGeneral:
|
||||
valor: str = '102'
|
||||
|
||||
def __str__(self):
|
||||
self.valor
|
||||
self.valor
|
||||
|
||||
@dataclass
|
||||
class TIPO_XML_AJUSTES(TIPO_XML):
|
||||
@@ -259,7 +359,7 @@ class InformacionGeneral:
|
||||
def __str__(self):
|
||||
self.valor
|
||||
|
||||
fecha_generacion: typing.Union[str, Fecha]
|
||||
fecha_generacion: str | Fecha
|
||||
hora_generacion: str
|
||||
periodo_nomina: PeriodoNomina
|
||||
tipo_moneda: TipoMoneda
|
||||
@@ -273,26 +373,26 @@ class InformacionGeneral:
|
||||
def apply(self, fragment, version):
|
||||
fragment.set_attributes('./InformacionGeneral',
|
||||
# NIE022
|
||||
Version = version,
|
||||
Version=version,
|
||||
# NIE023
|
||||
Ambiente = self.tipo_ambiente.valor,
|
||||
Ambiente=self.tipo_ambiente.valor,
|
||||
# NIE202
|
||||
# TABLA 5.5.2
|
||||
# TODO(bit4bit) solo NominaIndividual
|
||||
TipoXML = self.tipo_xml.valor,
|
||||
TipoXML=self.tipo_xml.valor,
|
||||
# NIE024
|
||||
CUNE = None,
|
||||
CUNE=None,
|
||||
# NIE025
|
||||
EncripCUNE = 'CUNE-SHA384',
|
||||
EncripCUNE='CUNE-SHA384',
|
||||
# NIE026
|
||||
FechaGen = self.fecha_generacion,
|
||||
FechaGen=self.fecha_generacion,
|
||||
# NIE027
|
||||
HoraGen = self.hora_generacion,
|
||||
HoraGen=self.hora_generacion,
|
||||
# NIE029
|
||||
PeriodoNomina = self.periodo_nomina.code,
|
||||
PeriodoNomina=self.periodo_nomina.code,
|
||||
# NIE030
|
||||
TipoMoneda = self.tipo_moneda.code,
|
||||
TRM = 0
|
||||
TipoMoneda=self.tipo_moneda.code,
|
||||
TRM=0
|
||||
# TODO(bit4bit) resto...
|
||||
# .....
|
||||
)
|
||||
@@ -321,24 +421,35 @@ class InformacionGeneral:
|
||||
h = hashlib.sha384()
|
||||
h.update(cune.encode('utf-8'))
|
||||
cune_hash = h.hexdigest()
|
||||
|
||||
|
||||
fragment.set_attributes(
|
||||
'./InformacionGeneral',
|
||||
# NIE024
|
||||
CUNE = cune_hash
|
||||
CUNE=cune_hash
|
||||
)
|
||||
|
||||
|
||||
class DianXMLExtensionSigner(fe.DianXMLExtensionSigner):
|
||||
|
||||
def __init__(self, pkcs12_path, passphrase=None, localpolicy=True):
|
||||
super().__init__(pkcs12_path, passphrase=passphrase, localpolicy=localpolicy)
|
||||
super().__init__(
|
||||
pkcs12_path,
|
||||
passphrase=passphrase,
|
||||
localpolicy=localpolicy)
|
||||
|
||||
def _element_extension_content(self, fachoxml):
|
||||
return fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
return fachoxml.builder.xpath(
|
||||
fachoxml.root,
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
|
||||
|
||||
class DIANNominaXML:
|
||||
def __init__(self, tag_document, xpath_ajuste=None, schemaLocation=None, namespace_ajuste=None):
|
||||
def __init__(
|
||||
self,
|
||||
tag_document,
|
||||
xpath_ajuste=None,
|
||||
schemaLocation=None,
|
||||
namespace_ajuste=None):
|
||||
self.informacion_general_version = None
|
||||
|
||||
self.tag_document = tag_document
|
||||
@@ -346,21 +457,26 @@ class DIANNominaXML:
|
||||
if namespace_ajuste:
|
||||
self.fexml = fe.FeXML(tag_document, namespace_ajuste)
|
||||
else:
|
||||
self.fexml = fe.FeXML(tag_document, 'dian:gov:co:facturaelectronica:NominaIndividual')
|
||||
self.fexml = fe.FeXML(
|
||||
tag_document,
|
||||
'dian:gov:co:facturaelectronica:NominaIndividual')
|
||||
|
||||
self.fexml.root.set("SchemaLocation", "")
|
||||
self.fexml.root.set("schemaLocation", schemaLocation)
|
||||
|
||||
# layout, la dian requiere que los elementos
|
||||
# esten ordenados segun el anexo tecnico
|
||||
self.fexml.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
self.fexml.placeholder_for(
|
||||
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
self.fexml.placeholder_for('./TipoNota', optional=True)
|
||||
|
||||
self.root_fragment = self.fexml
|
||||
if xpath_ajuste is not None:
|
||||
self.root_fragment = self.fexml.fragment(xpath_ajuste)
|
||||
self.root_fragment.placeholder_for('./ReemplazandoPredecesor', optional=True)
|
||||
self.root_fragment.placeholder_for('./EliminandoPredecesor', optional=True)
|
||||
self.root_fragment.placeholder_for(
|
||||
'./ReemplazandoPredecesor', optional=True)
|
||||
self.root_fragment.placeholder_for(
|
||||
'./EliminandoPredecesor', optional=True)
|
||||
if not namespace_ajuste:
|
||||
self.root_fragment.placeholder_for('./Novedad', optional=False)
|
||||
self.root_fragment.placeholder_for('./Periodo')
|
||||
@@ -374,16 +490,20 @@ class DIANNominaXML:
|
||||
self.root_fragment.placeholder_for('./Pago')
|
||||
self.root_fragment.placeholder_for('./FechasPagos')
|
||||
self.root_fragment.placeholder_for('./Devengados/Basico')
|
||||
self.root_fragment.placeholder_for('./Devengados/Transporte', optional=True)
|
||||
self.root_fragment.placeholder_for(
|
||||
'./Devengados/Transporte', optional=True)
|
||||
if not namespace_ajuste:
|
||||
self.novedad = self.root_fragment.fragment('./Novedad')
|
||||
else:
|
||||
self.novedad = None
|
||||
self.informacion_general_xml = self.root_fragment.fragment('./InformacionGeneral')
|
||||
self.informacion_general_xml = self.root_fragment.fragment(
|
||||
'./InformacionGeneral')
|
||||
self.periodo_xml = self.root_fragment.fragment('./Periodo')
|
||||
self.fecha_pagos_xml = self.root_fragment.fragment('./FechasPagos')
|
||||
self.numero_secuencia_xml = self.root_fragment.fragment('./NumeroSecuenciaXML')
|
||||
self.lugar_generacion_xml = self.root_fragment.fragment('./LugarGeneracionXML')
|
||||
self.numero_secuencia_xml = self.root_fragment.fragment(
|
||||
'./NumeroSecuenciaXML')
|
||||
self.lugar_generacion_xml = self.root_fragment.fragment(
|
||||
'./LugarGeneracionXML')
|
||||
self.proveedor_xml = self.root_fragment.fragment('./ProveedorXML')
|
||||
self.empleador = self.root_fragment.fragment('./Empleador')
|
||||
self.trabajador = self.root_fragment.fragment('./Trabajador')
|
||||
@@ -398,14 +518,20 @@ class DIANNominaXML:
|
||||
if not isinstance(metadata, Metadata):
|
||||
raise ValueError('se espera tipo Metadata')
|
||||
self.metadata = metadata
|
||||
|
||||
self.metadata.apply(self.novedad, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml)
|
||||
|
||||
|
||||
self.metadata.apply(
|
||||
self.novedad,
|
||||
self.numero_secuencia_xml,
|
||||
self.lugar_generacion_xml,
|
||||
self.proveedor_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, self.informacion_general_version)
|
||||
self.informacion_general.apply(
|
||||
self.informacion_general_xml,
|
||||
self.informacion_general_version)
|
||||
|
||||
def asignar_periodo(self, periodo):
|
||||
if not isinstance(periodo, Periodo):
|
||||
@@ -435,7 +561,7 @@ class DIANNominaXML:
|
||||
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')
|
||||
@@ -482,7 +608,7 @@ class DIANNominaXML:
|
||||
self.fexml.xpath_from_root('/Devengados/Basico'),
|
||||
'se requiere DevengadoBasico'
|
||||
)
|
||||
|
||||
|
||||
check_element(
|
||||
self.fexml.xpath_from_root('/Deducciones/Salud'),
|
||||
'se requiere DeduccionSalud'
|
||||
@@ -498,10 +624,13 @@ class DIANNominaXML:
|
||||
def informacion_general(self):
|
||||
xpath = self.root_fragment.xpath_from_root('/InformacionGeneral')
|
||||
return {
|
||||
'cune': self.fexml.get_element_attribute(cune_xpath, 'CUNE'),
|
||||
'fecha_generacion': self.fexml.get_element_attribute(xpath, 'FechaGen'),
|
||||
'numero': self.fexml.get_element_attribute(self.root_fragment('/NumeroSecuenciaXML', 'Numero'))
|
||||
}
|
||||
'cune': self.fexml.get_element_attribute(
|
||||
xpath, 'CUNE'),
|
||||
'fecha_generacion': self.fexml.get_element_attribute(
|
||||
xpath, 'FechaGen'),
|
||||
'numero': self.fexml.get_element_attribute(
|
||||
self.root_fragment(
|
||||
'/NumeroSecuenciaXML', 'Numero'))}
|
||||
|
||||
def toFachoXML(self):
|
||||
self._devengados_total()
|
||||
@@ -509,59 +638,78 @@ class DIANNominaXML:
|
||||
self._comprobante_total()
|
||||
|
||||
if self.informacion_general is not None:
|
||||
#TODO(bit4bit) acoplamiento temporal
|
||||
# TODO(bit4bit) acoplamiento temporal
|
||||
# es importante el orden de ejecucion
|
||||
|
||||
self.informacion_general.post_apply(self.fexml, self.root_fragment, self.informacion_general_xml)
|
||||
self.informacion_general.post_apply(
|
||||
self.fexml, self.root_fragment, self.informacion_general_xml)
|
||||
|
||||
if self.metadata is not None:
|
||||
self.metadata.post_apply(self.fexml, self.root_fragment, self.novedad, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml)
|
||||
self.metadata.post_apply(
|
||||
self.fexml,
|
||||
self.root_fragment,
|
||||
self.novedad,
|
||||
self.numero_secuencia_xml,
|
||||
self.lugar_generacion_xml,
|
||||
self.proveedor_xml)
|
||||
|
||||
return self.fexml
|
||||
|
||||
def _comprobante_total(self):
|
||||
devengados_total = self.root_fragment.get_element_text_or_attribute('./DevengadosTotal', '0.0')
|
||||
deducciones_total = self.root_fragment.get_element_text_or_attribute('./DeduccionesTotal', '0.0')
|
||||
devengados_total = self.root_fragment.get_element_text_or_attribute(
|
||||
'./DevengadosTotal', '0.0')
|
||||
deducciones_total = self.root_fragment.get_element_text_or_attribute(
|
||||
'./DeduccionesTotal', '0.0')
|
||||
|
||||
comprobante_total = Amount(devengados_total) - Amount(deducciones_total)
|
||||
comprobante_total = Amount(devengados_total) - \
|
||||
Amount(deducciones_total)
|
||||
|
||||
self.root_fragment.set_element('./ComprobanteTotal', str(round(comprobante_total, 2)))
|
||||
self.root_fragment.set_element(
|
||||
'./ComprobanteTotal', str(round(comprobante_total, 2)))
|
||||
|
||||
def _deducciones_total(self):
|
||||
xpaths = [
|
||||
self.root_fragment.xpath_from_root('/Deducciones/Salud/@Deduccion'),
|
||||
self.root_fragment.xpath_from_root('/Deducciones/FondoPension/@Deduccion')
|
||||
]
|
||||
self.root_fragment.xpath_from_root(
|
||||
'/Deducciones/Salud/@Deduccion'),
|
||||
self.root_fragment.xpath_from_root(
|
||||
'/Deducciones/FondoPension/@Deduccion')]
|
||||
deducciones = map(lambda valor: Amount(valor),
|
||||
self._values_of_xpaths(xpaths))
|
||||
|
||||
deducciones_total = Amount(0.0)
|
||||
|
||||
|
||||
for deduccion in deducciones:
|
||||
deducciones_total += deduccion
|
||||
|
||||
self.root_fragment.set_element('./DeduccionesTotal', str(round(deducciones_total, 2)))
|
||||
self.root_fragment.set_element(
|
||||
'./DeduccionesTotal', str(round(deducciones_total, 2)))
|
||||
|
||||
def _devengados_total(self):
|
||||
xpaths = [
|
||||
self.root_fragment.xpath_from_root('/Devengados/Basico/@SueldoTrabajado'),
|
||||
self.root_fragment.xpath_from_root('/Devengados/Transporte/@AuxilioTransporte'),
|
||||
self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojS'),
|
||||
self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojNS')
|
||||
]
|
||||
self.root_fragment.xpath_from_root(
|
||||
'/Devengados/Basico/@SueldoTrabajado'),
|
||||
self.root_fragment.xpath_from_root(
|
||||
'/Devengados/Transporte/@AuxilioTransporte'),
|
||||
self.root_fragment.xpath_from_root(
|
||||
'/Devengados/Transporte/@ViaticoManuAlojS'),
|
||||
self.root_fragment.xpath_from_root(
|
||||
'/Devengados/Transporte/@ViaticoManuAlojNS')]
|
||||
devengados = map(lambda valor: Amount(valor),
|
||||
self._values_of_xpaths(xpaths))
|
||||
|
||||
|
||||
devengados_total = Amount(0.0)
|
||||
for devengado in devengados:
|
||||
devengados_total += devengado
|
||||
# TODO(bit4bit) nque valor va redondeado?
|
||||
# NIE186
|
||||
self.root_fragment.set_element('./Redondeo', str(round(0,2)))
|
||||
self.root_fragment.set_element('./DevengadosTotal', str(round(devengados_total,2)))
|
||||
self.root_fragment.set_element('./Redondeo', str(round(0, 2)))
|
||||
self.root_fragment.set_element(
|
||||
'./DevengadosTotal', str(round(devengados_total, 2)))
|
||||
|
||||
def _values_of_xpaths(self, xpaths):
|
||||
xpaths_values_of_values = map(lambda val: self.fexml.get_element_text_or_attribute(val, multiple=True), xpaths)
|
||||
xpaths_values_of_values = map(
|
||||
lambda val: self.fexml.get_element_text_or_attribute(
|
||||
val, multiple=True), xpaths)
|
||||
xpaths_values = []
|
||||
# toda esta carreta para hacer un aplano de lista
|
||||
for xpath_values in xpaths_values_of_values:
|
||||
@@ -573,15 +721,22 @@ class DIANNominaXML:
|
||||
|
||||
return filter(lambda val: val is not None, xpaths_values)
|
||||
|
||||
|
||||
class DIANNominaIndividual(DIANNominaXML):
|
||||
|
||||
def __init__(self):
|
||||
schema = "dian:gov:co:facturaelectronica:NominaIndividual NominaIndividualElectronicaXSD.xsd"
|
||||
schema = (
|
||||
"dian:gov:co:facturaelectronica:NominaIndividual"
|
||||
" NominaIndividualElectronicaXSD.xsd"
|
||||
)
|
||||
|
||||
super().__init__('NominaIndividual', schemaLocation=schema)
|
||||
self.informacion_general_version = 'V1.0: Documento Soporte de Pago de Nómina Electrónica'
|
||||
self.informacion_general_version = (
|
||||
'V1.0: Documento Soporte de Pago de Nómina Electrónica')
|
||||
|
||||
# TODO(bit4bit) confirmar que no tienen en comun con NominaIndividual
|
||||
|
||||
|
||||
class DIANNominaIndividualDeAjuste(DIANNominaXML):
|
||||
|
||||
class Reemplazar(DIANNominaXML):
|
||||
@@ -593,31 +748,42 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
|
||||
|
||||
def apply(self, fragment):
|
||||
# NIAE214
|
||||
fragment.set_element('./TipoNota', '1')
|
||||
fragment.set_element('./Reemplazar/ReemplazandoPredecesor', None,
|
||||
# NIAE090
|
||||
NumeroPred = self.numero,
|
||||
# NIAE191
|
||||
CUNEPred = self.cune,
|
||||
# NIAE192
|
||||
FechaGenPred = self.fecha_generacion
|
||||
fragment.set_element('./TipoNota', '1')
|
||||
fragment.set_element(
|
||||
'./Reemplazar/ReemplazandoPredecesor', None,
|
||||
# NIAE090
|
||||
NumeroPred=self.numero,
|
||||
# NIAE191
|
||||
CUNEPred=self.cune,
|
||||
# NIAE192
|
||||
FechaGenPred=self.fecha_generacion
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
schema = "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd"
|
||||
|
||||
super().__init__('NominaIndividualDeAjuste', './Reemplazar', schemaLocation=schema, namespace_ajuste='dian:gov:co:facturaelectronica:NominaIndividualDeAjuste')
|
||||
|
||||
self.informacion_general_version = 'V1.0: Nota de Ajuste de Documento Soporte de Pago de Nómina Electrónica'
|
||||
schema = (
|
||||
"dian:gov:co:facturaelectronica:NominaIndividualDeAjuste"
|
||||
" NominaIndividualDeAjusteElectronicaXSD.xsd"
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
'NominaIndividualDeAjuste',
|
||||
'./Reemplazar',
|
||||
schemaLocation=schema,
|
||||
namespace_ajuste=(
|
||||
'dian:gov:co:facturaelectronica:'
|
||||
'NominaIndividualDeAjuste'))
|
||||
|
||||
self.informacion_general_version = (
|
||||
'V1.0: Nota de Ajuste de Documento Soporte de Pago de '
|
||||
'Nómina Electrónica')
|
||||
|
||||
def asignar_predecesor(self, predecesor):
|
||||
if not isinstance(predecesor, self.Predecesor):
|
||||
raise ValueError("se espera tipo Predecesor")
|
||||
predecesor.apply(self.fexml)
|
||||
|
||||
|
||||
class Eliminar(DIANNominaXML):
|
||||
|
||||
|
||||
@dataclass
|
||||
class Predecesor:
|
||||
numero: str
|
||||
@@ -628,24 +794,34 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
|
||||
fragment.set_element('./TipoNota', '2')
|
||||
fragment.set_element('./Eliminar/EliminandoPredecesor', None,
|
||||
# NIAE090
|
||||
NumeroPred = self.numero,
|
||||
NumeroPred=self.numero,
|
||||
# NIAE191
|
||||
CUNEPred = self.cune,
|
||||
CUNEPred=self.cune,
|
||||
# NIAE192
|
||||
FechaGenPred = self.fecha_generacion
|
||||
FechaGenPred=self.fecha_generacion
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
schema = "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd"
|
||||
super().__init__('NominaIndividualDeAjuste', './Eliminar', schemaLocation=schema, namespace_ajuste='dian:gov:co:facturaelectronica:NominaIndividualDeAjuste')
|
||||
schema = (
|
||||
"dian:gov:co:facturaelectronica:NominaIndividualDeAjuste"
|
||||
" NominaIndividualDeAjusteElectronicaXSD.xsd"
|
||||
)
|
||||
super().__init__(
|
||||
'NominaIndividualDeAjuste',
|
||||
'./Eliminar',
|
||||
schemaLocation=schema,
|
||||
namespace_ajuste=(
|
||||
'dian:gov:co:facturaelectronica:'
|
||||
'NominaIndividualDeAjuste'))
|
||||
|
||||
self.informacion_general_version = "V1.0: Nota de Ajuste de Documento Soporte de Pago de Nómina Electrónica"
|
||||
self.informacion_general_version = (
|
||||
"V1.0: Nota de Ajuste de Documento Soporte de Pago de "
|
||||
"Nómina Electrónica")
|
||||
|
||||
def asignar_predecesor(self, predecesor):
|
||||
if not isinstance(predecesor, self.Predecesor):
|
||||
raise ValueError("se espera tipo Eliminar.Predecesor")
|
||||
predecesor.apply(self.fexml)
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('NominaIndividualDeAjuste')
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .. import form
|
||||
|
||||
|
||||
class Amount(form.Amount):
|
||||
pass
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# al crear objetos de valor
|
||||
# se debe exportar en __all__
|
||||
|
||||
from .deduccion import *
|
||||
from .salud import *
|
||||
from .fondo_pension import *
|
||||
from .deduccion import Deduccion
|
||||
from .salud import DeduccionSalud
|
||||
from .fondo_pension import DeduccionFondoPension
|
||||
|
||||
__all__ = [
|
||||
'Deduccion',
|
||||
|
||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from ..amount import Amount
|
||||
from .deduccion import Deduccion
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeduccionFondoPension(Deduccion):
|
||||
porcentaje: Amount
|
||||
@@ -10,9 +11,9 @@ class DeduccionFondoPension(Deduccion):
|
||||
|
||||
def apply(self, fragment):
|
||||
fragment.set_element('./FondoPension', None,
|
||||
append_ = True,
|
||||
append_=True,
|
||||
# NIE164
|
||||
Porcentaje = str(round(self.porcentaje, 2)),
|
||||
Porcentaje=str(round(self.porcentaje, 2)),
|
||||
# NIE166
|
||||
Deduccion = self.deduccion
|
||||
Deduccion=self.deduccion
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from ..amount import Amount
|
||||
from .deduccion import Deduccion
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeduccionSalud(Deduccion):
|
||||
porcentaje: Amount
|
||||
@@ -10,10 +11,9 @@ class DeduccionSalud(Deduccion):
|
||||
|
||||
def apply(self, fragment):
|
||||
fragment.set_element('./Salud', None,
|
||||
append_ = True,
|
||||
append_=True,
|
||||
# NIE161
|
||||
Porcentaje = str(round(self.porcentaje, 2)),
|
||||
Porcentaje=str(round(self.porcentaje, 2)),
|
||||
# NIE163
|
||||
Deduccion = self.deduccion
|
||||
Deduccion=self.deduccion
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .. import form
|
||||
|
||||
|
||||
class Departamento(form.CountrySubentity):
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
|
||||
from .basico import *
|
||||
from .transporte import *
|
||||
from .devengado import *
|
||||
from .horas_extras import *
|
||||
from .basico import DevengadoBasico
|
||||
from .transporte import DevengadoTransporte
|
||||
from .devengado import Devengado
|
||||
from .horas_extras import (
|
||||
DevengadoHoraExtra,
|
||||
DevengadoHorasExtrasDiarias,
|
||||
DevengadoHorasExtrasNocturnas,
|
||||
DevengadoHorasRecargoNocturno,
|
||||
DevengadoHorasExtrasDiariasDominicalesYFestivos,
|
||||
DevengadoHorasRecargoDiariasDominicalesYFestivos,
|
||||
DevengadoHorasExtrasNocturnasDominicalesYFestivos,
|
||||
DevengadoHorasRecargoNocturnoDominicalesYFestivos,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Devengado',
|
||||
|
||||
@@ -11,10 +11,10 @@ class DevengadoBasico(Devengado):
|
||||
|
||||
def apply(self, fragment):
|
||||
fragment.find_or_create_element('./Basico')
|
||||
|
||||
|
||||
fragment.set_attributes('/Basico',
|
||||
# NIE069
|
||||
DiasTrabajados = str(self.dias_trabajados),
|
||||
DiasTrabajados=str(self.dias_trabajados),
|
||||
# NIE070
|
||||
SueldoTrabajado = round(self.sueldo_trabajado, 2)
|
||||
SueldoTrabajado=round(self.sueldo_trabajado, 2)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
from ..amount import Amount
|
||||
from .devengado import Devengado
|
||||
@@ -30,17 +30,18 @@ class DevengadoHoraExtra:
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasExtrasDiarias(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HEDs')
|
||||
for hora_extra in self.horas_extras:
|
||||
hora_extra.apply('./HED', hora_extra_xml)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasExtrasNocturnas(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HENs')
|
||||
for hora_extra in self.horas_extras:
|
||||
@@ -49,44 +50,48 @@ class DevengadoHorasExtrasNocturnas(Devengado):
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasRecargoNocturno(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HRNs')
|
||||
for hora_extra in self.horas_extras:
|
||||
hora_extra.apply('./HRN', hora_extra_xml)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HEDDFs')
|
||||
for hora_extra in self.horas_extras:
|
||||
hora_extra.apply('./HEDDF', hora_extra_xml)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HRDDFs')
|
||||
for hora_extra in self.horas_extras:
|
||||
hora_extra.apply('./HRDDF', hora_extra_xml)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HENDFs')
|
||||
for hora_extra in self.horas_extras:
|
||||
hora_extra.apply('./HENDF', hora_extra_xml)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevengadoHorasRecargoNocturnoDominicalesYFestivos(Devengado):
|
||||
horas_extras: List[DevengadoHoraExtra]
|
||||
|
||||
horas_extras: list[DevengadoHoraExtra]
|
||||
|
||||
def apply(self, fragment):
|
||||
hora_extra_xml = fragment.fragment('./HRNDFs')
|
||||
for hora_extra in self.horas_extras:
|
||||
|
||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from ..amount import Amount
|
||||
from .devengado import Devengado
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevengadoTransporte(Devengado):
|
||||
auxilio_transporte: Amount = None
|
||||
@@ -11,11 +12,12 @@ class DevengadoTransporte(Devengado):
|
||||
|
||||
def apply(self, fragment):
|
||||
fragment.set_element('./Transporte', None,
|
||||
append_ = True,
|
||||
append_=True,
|
||||
# NIE071
|
||||
AuxilioTransporte = self.auxilio_transporte,
|
||||
AuxilioTransporte=self.auxilio_transporte,
|
||||
# NIE072
|
||||
ViaticoManuAlojS = self.viatico_manutencion,
|
||||
ViaticoManuAlojS=self.viatico_manutencion,
|
||||
# NIE073
|
||||
ViaticoManuAlojNS = self.viatico_manutencion_no_salarial
|
||||
ViaticoManuAlojNS=(
|
||||
self.viatico_manutencion_no_salarial)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from ..pais import Pais
|
||||
from ..departamento import Departamento
|
||||
from ..municipio import Municipio
|
||||
|
||||
|
||||
@dataclass
|
||||
class Empleador:
|
||||
razon_social: str
|
||||
@@ -17,19 +18,17 @@ class Empleador:
|
||||
def apply(self, fragment):
|
||||
fragment.set_attributes('./Empleador',
|
||||
# NIE033
|
||||
NIT = self.nit,
|
||||
NIT=self.nit,
|
||||
# NIE034
|
||||
DV = self.dv,
|
||||
DV=self.dv,
|
||||
# NIE035
|
||||
Pais = self.pais.code,
|
||||
Pais=self.pais.code,
|
||||
# NIE036
|
||||
DepartamentoEstado = self.departamento.code,
|
||||
DepartamentoEstado=self.departamento.code,
|
||||
# NIE037
|
||||
MunicipioCiudad = self.municipio.code,
|
||||
MunicipioCiudad=self.municipio.code,
|
||||
# NIE038
|
||||
Direccion = self.direccion,
|
||||
Direccion=self.direccion,
|
||||
|
||||
RazonSocial=self.razon_social
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import datetime
|
||||
|
||||
from facho import fe
|
||||
|
||||
|
||||
class Habilitacion:
|
||||
|
||||
@dataclass
|
||||
@@ -16,19 +17,19 @@ class Habilitacion:
|
||||
self.metadata = metadata
|
||||
|
||||
def generar(self, zipname, fecha):
|
||||
nominas = []
|
||||
dianzip = fe.DianZIP(open(zipname, 'w'))
|
||||
fe.DianZIP(open(zipname, 'w'))
|
||||
|
||||
fechabase = datetime.datetime.now()
|
||||
consecutivo = 0
|
||||
for _ in range(1, 11):
|
||||
consecutivo += 1
|
||||
fechabase += datetime.timedelta(days=1)
|
||||
nomina = self._crear_nomina_individual()
|
||||
self._crear_nomina_individual()
|
||||
|
||||
# pag 96
|
||||
nombre = "nie%010d%s%08x.xml" % (int(self.nit), fecha.strftime('%s'), consecutivo)
|
||||
|
||||
"nie%010d%s%08x.xml" % (
|
||||
int(self.nit), fecha.strftime('%s'), consecutivo)
|
||||
|
||||
def _crear_nomina_individual_reemplazar(self, nomina, fechabase):
|
||||
metadata = self.metadata
|
||||
|
||||
@@ -36,10 +37,15 @@ class Habilitacion:
|
||||
|
||||
nomina_ajuste = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
|
||||
self._poblar_nomina(nomina_ajuste, metadata, fecha, prefijo='R')
|
||||
informacion_general = nomina.informacion_general()
|
||||
|
||||
|
||||
def _poblar_nomina(self, nomina, metadata, fecha, prefijo='N', consecutivo='0001'):
|
||||
nomina.informacion_general()
|
||||
|
||||
def _poblar_nomina(
|
||||
self,
|
||||
nomina,
|
||||
metadata,
|
||||
fecha,
|
||||
prefijo='N',
|
||||
consecutivo='0001'):
|
||||
nomina.asignar_fecha_pago(fecha)
|
||||
|
||||
nomina.asignar_metadata(fe.nomina.Metadata(
|
||||
@@ -48,14 +54,14 @@ class Habilitacion:
|
||||
consecutivo=consecutivo
|
||||
),
|
||||
lugar_generacion=fe.nomina.Lugar(
|
||||
pais = fe.nomina.Pais(
|
||||
code = 'CO'
|
||||
pais=fe.nomina.Pais(
|
||||
code='CO'
|
||||
),
|
||||
departamento = fe.nomina.Departamento(
|
||||
code = '05'
|
||||
departamento=fe.nomina.Departamento(
|
||||
code='05'
|
||||
),
|
||||
municipio = fe.nomina.Municipio(
|
||||
code = '05001'
|
||||
municipio=fe.nomina.Municipio(
|
||||
code='05001'
|
||||
),
|
||||
),
|
||||
proveedor=fe.nomina.Proveedor(
|
||||
@@ -72,14 +78,14 @@ class Habilitacion:
|
||||
fecha_liquidacion_fin=fecha,
|
||||
fecha_generacion=fecha,
|
||||
))
|
||||
|
||||
|
||||
nomina.asignar_informacion_general(fe.nomina.InformacionGeneral(
|
||||
fecha_generacion = fecha,
|
||||
hora_generacion = '20:09:00-05:00',
|
||||
tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRUEBAS,
|
||||
software_pin = metadata.software_pin,
|
||||
periodo_nomina = fe.nomina.PeriodoNomina(code='1'),
|
||||
tipo_moneda = fe.nomina.TipoMoneda(code='COP')
|
||||
fecha_generacion=fecha,
|
||||
hora_generacion='20:09:00-05:00',
|
||||
tipo_ambiente=fe.nomina.InformacionGeneral.AMBIENTE_PRUEBAS,
|
||||
software_pin=metadata.software_pin,
|
||||
periodo_nomina=fe.nomina.PeriodoNomina(code='1'),
|
||||
tipo_moneda=fe.nomina.TipoMoneda(code='COP')
|
||||
))
|
||||
|
||||
nomina.asignar_pago(fe.nomina.Pago(
|
||||
@@ -91,53 +97,53 @@ class Habilitacion:
|
||||
)
|
||||
))
|
||||
nomina.asignar_empleador(fe.nomina.Empleador(
|
||||
nit = metadata.nit,
|
||||
dv = '0',
|
||||
pais = fe.nomina.Pais(
|
||||
code = 'CO'
|
||||
nit=metadata.nit,
|
||||
dv='0',
|
||||
pais=fe.nomina.Pais(
|
||||
code='CO'
|
||||
),
|
||||
departamento = fe.nomina.Departamento(
|
||||
code = '05'
|
||||
departamento=fe.nomina.Departamento(
|
||||
code='05'
|
||||
),
|
||||
municipio = fe.nomina.Municipio(
|
||||
code = '05001'
|
||||
municipio=fe.nomina.Municipio(
|
||||
code='05001'
|
||||
),
|
||||
direccion = 'calle etrivial'
|
||||
direccion='calle etrivial'
|
||||
))
|
||||
|
||||
nomina.asignar_trabajador(fe.nomina.Trabajador(
|
||||
tipo_contrato = fe.nomina.TipoContrato(
|
||||
code = '1'
|
||||
tipo_contrato=fe.nomina.TipoContrato(
|
||||
code='1'
|
||||
),
|
||||
alto_riesgo = False,
|
||||
tipo_documento = fe.nomina.TipoDocumento(
|
||||
code = '11'
|
||||
alto_riesgo=False,
|
||||
tipo_documento=fe.nomina.TipoDocumento(
|
||||
code='11'
|
||||
),
|
||||
primer_apellido = 'gnu',
|
||||
segundo_apellido = 'emacs',
|
||||
primer_nombre = 'facho',
|
||||
lugar_trabajo = fe.nomina.LugarTrabajo(
|
||||
pais = fe.nomina.Pais(code='CO'),
|
||||
departamento = fe.nomina.Departamento(code='05'),
|
||||
municipio = fe.nomina.Municipio(code='05001'),
|
||||
direccion = 'calle facho'
|
||||
primer_apellido='gnu',
|
||||
segundo_apellido='emacs',
|
||||
primer_nombre='facho',
|
||||
lugar_trabajo=fe.nomina.LugarTrabajo(
|
||||
pais=fe.nomina.Pais(code='CO'),
|
||||
departamento=fe.nomina.Departamento(code='05'),
|
||||
municipio=fe.nomina.Municipio(code='05001'),
|
||||
direccion='calle facho'
|
||||
),
|
||||
numero_documento = metadata.nit,
|
||||
tipo = fe.nomina.TipoTrabajador(
|
||||
code = '01'
|
||||
numero_documento=metadata.nit,
|
||||
tipo=fe.nomina.TipoTrabajador(
|
||||
code='01'
|
||||
),
|
||||
salario_integral = True,
|
||||
sueldo = fe.nomina.Amount(1_500_000)
|
||||
salario_integral=True,
|
||||
sueldo=fe.nomina.Amount(1_500_000)
|
||||
))
|
||||
|
||||
|
||||
nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
|
||||
dias_trabajados = 60,
|
||||
sueldo_trabajado = fe.nomina.Amount(3_500_000)
|
||||
dias_trabajados=60,
|
||||
sueldo_trabajado=fe.nomina.Amount(3_500_000)
|
||||
))
|
||||
|
||||
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
|
||||
porcentaje = fe.nomina.Amount(19),
|
||||
deduccion = fe.nomina.Amount(1_000_000)
|
||||
porcentaje=fe.nomina.Amount(19),
|
||||
deduccion=fe.nomina.Amount(1_000_000)
|
||||
))
|
||||
|
||||
nomina.adicionar_deduccion(fe.nomina.DeduccionFondoPension(
|
||||
@@ -152,4 +158,3 @@ class Habilitacion:
|
||||
|
||||
nomina = fe.nomina.DIANNominaIndividual()
|
||||
self._poblar_nomina(nomina, metadata, fecha)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from .departamento import Departamento
|
||||
from .municipio import Municipio
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class Lugar:
|
||||
pais: Pais
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .. import form
|
||||
|
||||
|
||||
class Municipio(form.City):
|
||||
pass
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from .forma_pago import FormaPago
|
||||
from .metodo_pago import MetodoPago
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pago:
|
||||
forma: FormaPago
|
||||
@@ -11,6 +12,6 @@ class Pago:
|
||||
def apply(self, fragment):
|
||||
fragment.set_attributes('./Pago',
|
||||
# NIE064
|
||||
Forma = self.forma.code,
|
||||
Forma=self.forma.code,
|
||||
# NIE065
|
||||
Metodo = self.metodo.code)
|
||||
Metodo=self.metodo.code)
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class FormaPago:
|
||||
code: str
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetodoPago:
|
||||
code: str
|
||||
@@ -11,4 +12,3 @@ class MetodoPago:
|
||||
if self.code not in codelist.MediosPago:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.MediosPago[self.code]['name']
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .. import form
|
||||
|
||||
|
||||
class Pais(form.Country):
|
||||
pass
|
||||
|
||||
@@ -2,12 +2,11 @@ from dataclasses import dataclass, field
|
||||
|
||||
from ..amount import Amount
|
||||
|
||||
from .tipo_contrato import *
|
||||
from .tipo_documento import *
|
||||
from .lugar_trabajo import *
|
||||
from .tipo_trabajador import *
|
||||
from .sub_tipo_trabajador import *
|
||||
|
||||
from .tipo_contrato import TipoContrato
|
||||
from .tipo_documento import TipoDocumento
|
||||
from .lugar_trabajo import LugarTrabajo
|
||||
from .tipo_trabajador import TipoTrabajador
|
||||
from .sub_tipo_trabajador import SubTipoTrabajador
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -29,45 +28,47 @@ class Trabajador:
|
||||
|
||||
codigo_trabajador: str = None
|
||||
otros_nombres: str = None
|
||||
sub_tipo: SubTipoTrabajador = field(default_factory=lambda: SubTipoTrabajador(code='00'))
|
||||
sub_tipo: SubTipoTrabajador = field(
|
||||
default_factory=lambda: SubTipoTrabajador(code="00")
|
||||
)
|
||||
|
||||
def apply(self, fragment):
|
||||
fragment.set_attributes('./Trabajador',
|
||||
# NIE041
|
||||
TipoTrabajador = self.tipo.code,
|
||||
# NIE042
|
||||
SubTipoTrabajador = self.sub_tipo.code,
|
||||
# NIE043
|
||||
AltoRiesgoPension = str(self.alto_riesgo).lower(),
|
||||
# NIE044
|
||||
TipoDocumento = self.tipo_documento.code,
|
||||
# NIE045
|
||||
NumeroDocumento = self.numero_documento,
|
||||
# NIE046
|
||||
PrimerApellido = self.primer_apellido,
|
||||
# NIE047
|
||||
SegundoApellido = self.segundo_apellido,
|
||||
# NIE048
|
||||
PrimerNombre = self.primer_nombre,
|
||||
# NIE049
|
||||
OtrosNombres = self.otros_nombres,
|
||||
# NIE050
|
||||
LugarTrabajoPais = self.lugar_trabajo.pais.code,
|
||||
|
||||
# NIE051
|
||||
LugarTrabajoDepartamentoEstado = self.lugar_trabajo.departamento.code,
|
||||
|
||||
# NIE052
|
||||
LugarTrabajoMunicipioCiudad = self.lugar_trabajo.municipio.code,
|
||||
|
||||
# NIE053
|
||||
LugarTrabajoDireccion = self.lugar_trabajo.direccion,
|
||||
# NIE056
|
||||
SalarioIntegral = str(self.salario_integral).lower(),
|
||||
# NIE061
|
||||
TipoContrato = self.tipo_contrato.code,
|
||||
# NIE062
|
||||
Sueldo = str(self.sueldo),
|
||||
# NIE063
|
||||
CodigoTrabajador = self.codigo_trabajador
|
||||
)
|
||||
fragment.set_attributes(
|
||||
"./Trabajador",
|
||||
# NIE041
|
||||
TipoTrabajador=self.tipo.code,
|
||||
# NIE042
|
||||
SubTipoTrabajador=self.sub_tipo.code,
|
||||
# NIE043
|
||||
AltoRiesgoPension=str(self.alto_riesgo).lower(),
|
||||
# NIE044
|
||||
TipoDocumento=self.tipo_documento.code,
|
||||
# NIE045
|
||||
NumeroDocumento=self.numero_documento,
|
||||
# NIE046
|
||||
PrimerApellido=self.primer_apellido,
|
||||
# NIE047
|
||||
SegundoApellido=self.segundo_apellido,
|
||||
# NIE048
|
||||
PrimerNombre=self.primer_nombre,
|
||||
# NIE049
|
||||
OtrosNombres=self.otros_nombres,
|
||||
# NIE050
|
||||
LugarTrabajoPais=self.lugar_trabajo.pais.code,
|
||||
# NIE051
|
||||
LugarTrabajoDepartamentoEstado=(
|
||||
self.lugar_trabajo.departamento.code
|
||||
),
|
||||
# NIE052
|
||||
LugarTrabajoMunicipioCiudad=self.lugar_trabajo.municipio.code,
|
||||
# NIE053
|
||||
LugarTrabajoDireccion=self.lugar_trabajo.direccion,
|
||||
# NIE056
|
||||
SalarioIntegral=str(self.salario_integral).lower(),
|
||||
# NIE061
|
||||
TipoContrato=self.tipo_contrato.code,
|
||||
# NIE062
|
||||
Sueldo=str(self.sueldo),
|
||||
# NIE063
|
||||
CodigoTrabajador=self.codigo_trabajador,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from . import *
|
||||
from ..pais import Pais
|
||||
from ..departamento import Departamento
|
||||
from ..municipio import Municipio
|
||||
|
||||
|
||||
@dataclass
|
||||
class LugarTrabajo:
|
||||
pais: Pais
|
||||
|
||||
@@ -2,12 +2,13 @@ from dataclasses import dataclass
|
||||
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubTipoTrabajador:
|
||||
code: str
|
||||
name: str = ''
|
||||
name: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.code not in codelist.SubTipoTrabajador:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.SubTipoTrabajador[self.code]['name']
|
||||
self.name = codelist.SubTipoTrabajador[self.code]["name"]
|
||||
|
||||
@@ -2,14 +2,13 @@ from dataclasses import dataclass
|
||||
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class TipoContrato:
|
||||
code: str
|
||||
name: str = ''
|
||||
name: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.code not in codelist.TipoContrato:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.TipoContrato[self.code]['name']
|
||||
|
||||
|
||||
self.name = codelist.TipoContrato[self.code]["name"]
|
||||
|
||||
@@ -2,12 +2,13 @@ from dataclasses import dataclass
|
||||
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class TipoDocumento:
|
||||
code: str
|
||||
name: str = ''
|
||||
name: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.code not in codelist.TipoIdFiscal:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.TipoIdFiscal[self.code]['name']
|
||||
self.name = codelist.TipoIdFiscal[self.code]["name"]
|
||||
|
||||
@@ -2,12 +2,13 @@ from dataclasses import dataclass
|
||||
|
||||
from facho.fe.data.dian import codelist
|
||||
|
||||
|
||||
@dataclass
|
||||
class TipoTrabajador:
|
||||
code: str
|
||||
name: str = ''
|
||||
name: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.code not in codelist.TipoTrabajador:
|
||||
raise ValueError("code [%s] not found" % (self.code))
|
||||
self.name = codelist.TipoTrabajador[self.code]['name']
|
||||
self.name = codelist.TipoTrabajador[self.code]["name"]
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
from .fields import Field
|
||||
from collections import defaultdict
|
||||
|
||||
class ModelMeta(type):
|
||||
def __new__(cls, name, bases, ns):
|
||||
new = type.__new__(cls, name, bases, ns)
|
||||
|
||||
# mapeamos asignacion en declaracion de clase
|
||||
# a attributo de objeto
|
||||
if '__name__' in ns:
|
||||
new.__name__ = ns['__name__']
|
||||
if '__namespace__' in ns:
|
||||
new.__namespace__ = ns['__namespace__']
|
||||
else:
|
||||
new.__namespace__ = {}
|
||||
|
||||
return new
|
||||
|
||||
class ModelBase(object, metaclass=ModelMeta):
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
obj = super().__new__(cls, *args, **kwargs)
|
||||
obj._xml_attributes = {}
|
||||
obj._fields = {}
|
||||
obj._value = None
|
||||
obj._namespace_prefix = None
|
||||
obj._on_change_fields = defaultdict(list)
|
||||
obj._order_fields = []
|
||||
|
||||
def on_change_fields_for_function():
|
||||
# se recorre arbol de herencia buscando attributo on_changes
|
||||
for parent_cls in type(obj).__mro__:
|
||||
for parent_attr in dir(parent_cls):
|
||||
parent_meth = getattr(parent_cls, parent_attr, None)
|
||||
if not callable(parent_meth):
|
||||
continue
|
||||
on_changes = getattr(parent_meth, 'on_changes', None)
|
||||
if on_changes:
|
||||
return (parent_meth, on_changes)
|
||||
return (None, [])
|
||||
|
||||
# forzamos registros de campos al modelo
|
||||
# al instanciar
|
||||
for (key, v) in type(obj).__dict__.items():
|
||||
if isinstance(v, fields.Field):
|
||||
obj._order_fields.append(key)
|
||||
|
||||
if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function) or isinstance(v, fields.Amount):
|
||||
if hasattr(v, 'default') and v.default is not None:
|
||||
setattr(obj, key, v.default)
|
||||
if hasattr(v, 'create') and v.create == True:
|
||||
setattr(obj, key, '')
|
||||
|
||||
# register callbacks for changes
|
||||
(fun, on_change_fields) = on_change_fields_for_function()
|
||||
for field in on_change_fields:
|
||||
obj._on_change_fields[field].append(fun)
|
||||
|
||||
|
||||
# post inicializacion del objeto
|
||||
obj.__setup__()
|
||||
return obj
|
||||
|
||||
def _set_attribute(self, field, name, value):
|
||||
self._xml_attributes[field] = (name, value)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
self._xml_attributes[key] = val
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._xml_attributes[key]
|
||||
|
||||
def _get_field(self, name):
|
||||
return self._fields[name]
|
||||
|
||||
def _set_field(self, name, field):
|
||||
field.name = name
|
||||
self._fields[name] = field
|
||||
|
||||
def _set_content(self, value):
|
||||
default = self.__default_set__(value)
|
||||
if default is not None:
|
||||
self._value = default
|
||||
|
||||
def to_xml(self):
|
||||
"""
|
||||
Genera xml del modelo y sus relaciones
|
||||
"""
|
||||
def _hook_before_xml():
|
||||
self.__before_xml__()
|
||||
for field in self._fields.values():
|
||||
if hasattr(field, '__before_xml__'):
|
||||
field.__before_xml__()
|
||||
|
||||
_hook_before_xml()
|
||||
|
||||
tag = self.__name__
|
||||
ns = ''
|
||||
if self._namespace_prefix is not None:
|
||||
ns = "%s:" % (self._namespace_prefix)
|
||||
|
||||
pair_attributes = ["%s=\"%s\"" % (k, v) for (k, v) in self._xml_attributes.values()]
|
||||
|
||||
for (prefix, url) in self.__namespace__.items():
|
||||
pair_attributes.append("xmlns:%s=\"%s\"" % (prefix, url))
|
||||
attributes = ""
|
||||
if pair_attributes:
|
||||
attributes = " " + " ".join(pair_attributes)
|
||||
|
||||
content = ""
|
||||
|
||||
ordered_fields = {}
|
||||
for name in self._order_fields:
|
||||
if name in self._fields:
|
||||
ordered_fields[name] = True
|
||||
else:
|
||||
for key in self._fields.keys():
|
||||
if key.startswith(name):
|
||||
ordered_fields[key] = True
|
||||
|
||||
for name in ordered_fields.keys():
|
||||
value = self._fields[name]
|
||||
# al ser virtual no adicinamos al arbol xml
|
||||
if hasattr(value, 'virtual') and value.virtual:
|
||||
continue
|
||||
|
||||
if hasattr(value, 'to_xml'):
|
||||
content += value.to_xml()
|
||||
elif isinstance(value, str):
|
||||
content += value
|
||||
|
||||
if self._value is not None:
|
||||
content += str(self._value)
|
||||
|
||||
if content == "":
|
||||
return "<%s%s%s/>" % (ns, tag, attributes)
|
||||
else:
|
||||
return "<%s%s%s>%s</%s%s>" % (ns, tag, attributes, content, ns, tag)
|
||||
|
||||
def __str__(self):
|
||||
return self.to_xml()
|
||||
|
||||
|
||||
class Model(ModelBase):
|
||||
"""
|
||||
Model clase que representa el modelo
|
||||
"""
|
||||
|
||||
def __before_xml__(self):
|
||||
"""
|
||||
Ejecuta antes de generar el xml, este
|
||||
metodo sirve para realizar actualizaciones
|
||||
en los campos en el ultimo momento
|
||||
"""
|
||||
pass
|
||||
|
||||
def __default_set__(self, value):
|
||||
"""
|
||||
Al asignar un valor al modelo atraves de una relacion (person.relation = '33')
|
||||
se puede personalizar como hacer esta asignacion.
|
||||
"""
|
||||
return value
|
||||
|
||||
def __default_get__(self, name, value):
|
||||
"""
|
||||
Al obtener el valor atraves de una relacion (age = person.age)
|
||||
Retorno de valor por defecto
|
||||
"""
|
||||
return value
|
||||
|
||||
def __setup__(self):
|
||||
"""
|
||||
Inicializar modelo
|
||||
"""
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
from .attribute import Attribute
|
||||
from .many2one import Many2One
|
||||
from .one2many import One2Many
|
||||
from .function import Function
|
||||
from .virtual import Virtual
|
||||
from .field import Field
|
||||
from .amount import Amount
|
||||
|
||||
__all__ = [Attribute, One2Many, Many2One, Virtual, Field, Amount]
|
||||
|
||||
def on_change(fields):
|
||||
from functools import wraps
|
||||
|
||||
def decorator(func):
|
||||
setattr(func, 'on_changes', fields)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *arg, **kwargs):
|
||||
return func(self, *arg, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -1,35 +0,0 @@
|
||||
from .field import Field
|
||||
from collections import defaultdict
|
||||
import facho.fe.form as form
|
||||
|
||||
class Amount(Field):
|
||||
"""
|
||||
Amount representa un campo moneda usando form.Amount
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, default=None, precision=6):
|
||||
self.field_name = name
|
||||
self.values = {}
|
||||
self.default = default
|
||||
self.precision = precision
|
||||
|
||||
def __get__(self, model, cls):
|
||||
if model is None:
|
||||
return self
|
||||
assert self.name is not None
|
||||
|
||||
self.__init_value(model)
|
||||
model._set_field(self.name, self)
|
||||
return self.values[model]
|
||||
|
||||
def __set__(self, model, value):
|
||||
assert self.name is not None
|
||||
self.__init_value(model)
|
||||
model._set_field(self.name, self)
|
||||
self.values[model] = form.Amount(value, precision=self.precision)
|
||||
|
||||
self._changed_field(model, self.name, value)
|
||||
|
||||
def __init_value(self, model):
|
||||
if model not in self.values:
|
||||
self.values[model] = form.Amount(self.default or 0)
|
||||
@@ -1,29 +0,0 @@
|
||||
from .field import Field
|
||||
|
||||
class Attribute(Field):
|
||||
"""
|
||||
Attribute es un atributo del elemento actual.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default=None):
|
||||
"""
|
||||
:param name: nombre del atribute
|
||||
:param default: valor por defecto del attributo
|
||||
"""
|
||||
self.attribute = name
|
||||
self.value = default
|
||||
self.default = default
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
if inst is None:
|
||||
return self
|
||||
|
||||
assert self.name is not None
|
||||
return self.value
|
||||
|
||||
def __set__(self, inst, value):
|
||||
assert self.name is not None
|
||||
self.value = value
|
||||
|
||||
self._changed_field(inst, self.name, value)
|
||||
inst._set_attribute(self.name, self.attribute, value)
|
||||
@@ -1,60 +0,0 @@
|
||||
import warnings
|
||||
|
||||
class Field:
|
||||
def __set_name__(self, owner, name, virtual=False):
|
||||
self.name = name
|
||||
self.virtual = virtual
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
if inst is None:
|
||||
return self
|
||||
assert self.name is not None
|
||||
return inst._fields[self.name]
|
||||
|
||||
def __set__(self, inst, value):
|
||||
assert self.name is not None
|
||||
inst._fields[self.name] = value
|
||||
|
||||
def _set_namespace(self, inst, name, namespaces):
|
||||
if name is None:
|
||||
return
|
||||
|
||||
#TODO(bit4bit) aunque las pruebas confirmar
|
||||
#que si se escribe el namespace que es
|
||||
#no ahi confirmacion de declaracion previa del namespace
|
||||
|
||||
inst._namespace_prefix = name
|
||||
|
||||
def _call(self, inst, method, *args):
|
||||
call = getattr(inst, method or '', None)
|
||||
|
||||
if callable(call):
|
||||
return call(*args)
|
||||
|
||||
def _create_model(self, inst, name=None, model=None, attribute=None, namespace=None):
|
||||
try:
|
||||
return inst._fields[self.name]
|
||||
except KeyError:
|
||||
if model is not None:
|
||||
obj = model()
|
||||
else:
|
||||
obj = self.model()
|
||||
if name is not None:
|
||||
obj.__name__ = name
|
||||
|
||||
if namespace:
|
||||
self._set_namespace(obj, namespace, inst.__namespace__)
|
||||
else:
|
||||
self._set_namespace(obj, self.namespace, inst.__namespace__)
|
||||
|
||||
if attribute:
|
||||
inst._fields[attribute] = obj
|
||||
else:
|
||||
inst._fields[self.name] = obj
|
||||
|
||||
return obj
|
||||
|
||||
def _changed_field(self, inst, name, value):
|
||||
for fun in inst._on_change_fields[name]:
|
||||
fun(inst, name, value)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
from .field import Field
|
||||
|
||||
class Function(Field):
|
||||
"""
|
||||
Permite modificar el modelo cuando se intenta,
|
||||
obtener el valor de este campo.
|
||||
|
||||
DEPRECATED usar Virtual
|
||||
"""
|
||||
def __init__(self, field, getter=None, default=None):
|
||||
self.field = field
|
||||
self.getter = getter
|
||||
self.default = default
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
if inst is None:
|
||||
return self
|
||||
assert self.name is not None
|
||||
|
||||
# si se indica `field` se adiciona
|
||||
# como campo del modelo, esto es
|
||||
# que se serializa a xml
|
||||
inst._set_field(self.name, self.field)
|
||||
|
||||
if self.getter is not None:
|
||||
value = self._call(inst, self.getter, self.name, self.field)
|
||||
|
||||
if value is not None:
|
||||
self.field.__set__(inst, value)
|
||||
|
||||
return self.field
|
||||
|
||||
def __set__(self, inst, value):
|
||||
inst._set_field(self.name, self.field)
|
||||
self._changed_field(inst, self.name, value)
|
||||
self.field.__set__(inst, value)
|
||||
@@ -1,62 +0,0 @@
|
||||
from .field import Field
|
||||
from collections import defaultdict
|
||||
|
||||
class Many2One(Field):
|
||||
"""
|
||||
Many2One describe una relacion pertenece a.
|
||||
"""
|
||||
|
||||
def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False, create=False):
|
||||
"""
|
||||
:param model: nombre del modelo destino
|
||||
:param name: nombre del elemento xml
|
||||
:param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3
|
||||
:param namespace: sufijo del namespace al que pertenece el elemento
|
||||
:param default: el valor o contenido por defecto
|
||||
:param virtual: se crea la relacion por no se ve reflejada en el xml final
|
||||
:param create: fuerza la creacion del elemento en el xml, ya que los elementos no son creados sino tienen contenido
|
||||
"""
|
||||
self.model = model
|
||||
self.setter = setter
|
||||
self.namespace = namespace
|
||||
self.field_name = name
|
||||
self.default = default
|
||||
self.virtual = virtual
|
||||
self.relations = defaultdict(dict)
|
||||
self.create = create
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
if inst is None:
|
||||
return self
|
||||
assert self.name is not None
|
||||
|
||||
if self.name in self.relations:
|
||||
value = self.relations[inst][self.name]
|
||||
else:
|
||||
value = self._create_model(inst, name=self.field_name)
|
||||
self.relations[inst][self.name] = value
|
||||
|
||||
# se puede obtener directamente un valor indicado por el modelo
|
||||
if hasattr(value, '__default_get__'):
|
||||
return value.__default_get__(self.name, value)
|
||||
elif hasattr(inst, '__default_get__'):
|
||||
return inst.__default_get__(self.name, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __set__(self, inst, value):
|
||||
assert self.name is not None
|
||||
inst_model = self._create_model(inst, name=self.field_name, model=self.model)
|
||||
self.relations[inst][self.name] = inst_model
|
||||
|
||||
# si hay setter manual se ejecuta
|
||||
# de lo contrario se asigna como texto del elemento
|
||||
setter = getattr(inst, self.setter or '', None)
|
||||
if callable(setter):
|
||||
setter(inst_model, value)
|
||||
else:
|
||||
inst_model._set_content(value)
|
||||
|
||||
self._changed_field(inst, self.name, value)
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
from .field import Field
|
||||
from collections import defaultdict
|
||||
|
||||
# TODO(bit4bit) lograr que isinstance se aplique
|
||||
# al objeto envuelto
|
||||
class _RelationProxy():
|
||||
def __init__(self, obj, inst, attribute):
|
||||
self.__dict__['_obj'] = obj
|
||||
self.__dict__['_inst'] = inst
|
||||
self.__dict__['_attribute'] = attribute
|
||||
|
||||
def __getattr__(self, name):
|
||||
if (name in self.__dict__):
|
||||
return self.__dict__[name]
|
||||
|
||||
rel = getattr(self.__dict__['_obj'], name)
|
||||
if hasattr(rel, '__default_get__'):
|
||||
return rel.__default_get__(name, rel)
|
||||
|
||||
return rel
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
# TODO(bit4bit) hacemos proxy al sistema de notificacion de cambios
|
||||
# algo burdo, se usa __dict__ para saltarnos el __getattr__ y evitar un fallo por recursion
|
||||
rel = getattr(self.__dict__['_obj'], attr)
|
||||
if hasattr(rel, '__default_set__'):
|
||||
response = setattr(self._obj, attr, rel.__default_set__(value))
|
||||
else:
|
||||
response = setattr(self._obj, attr, value)
|
||||
|
||||
for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]:
|
||||
fun(self.__dict__['_inst'], self.__dict__['_attribute'], value)
|
||||
return response
|
||||
|
||||
class _Relation():
|
||||
def __init__(self, creator, inst, attribute):
|
||||
self.creator = creator
|
||||
self.inst = inst
|
||||
self.attribute = attribute
|
||||
self.relations = []
|
||||
|
||||
def create(self):
|
||||
n_relations = len(self.relations)
|
||||
attribute = '%s_%d' % (self.attribute, n_relations)
|
||||
relation = self.creator(attribute)
|
||||
proxy = _RelationProxy(relation, self.inst, self.attribute)
|
||||
|
||||
self.relations.append(relation)
|
||||
return proxy
|
||||
|
||||
def __len__(self):
|
||||
return len(self.relations)
|
||||
|
||||
def __iter__(self):
|
||||
for relation in self.relations:
|
||||
yield relation
|
||||
|
||||
class One2Many(Field):
|
||||
"""
|
||||
One2Many describe una relacion tiene muchos.
|
||||
"""
|
||||
|
||||
def __init__(self, model, name=None, namespace=None, default=None):
|
||||
"""
|
||||
:param model: nombre del modelo destino
|
||||
:param name: nombre del elemento xml cuando se crea hijo
|
||||
:param namespace: sufijo del namespace al que pertenece el elemento
|
||||
:param default: el valor o contenido por defecto
|
||||
"""
|
||||
self.model = model
|
||||
self.field_name = name
|
||||
self.namespace = namespace
|
||||
self.default = default
|
||||
self.relation = {}
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
assert self.name is not None
|
||||
|
||||
def creator(attribute):
|
||||
return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute, namespace=self.namespace)
|
||||
|
||||
if inst in self.relation:
|
||||
return self.relation[inst]
|
||||
else:
|
||||
self.relation[inst] = _Relation(creator, inst, self.name)
|
||||
return self.relation[inst]
|
||||
@@ -1,54 +0,0 @@
|
||||
from .field import Field
|
||||
|
||||
# Un campo virtual
|
||||
# no participa del renderizado
|
||||
# pero puede interactura con este
|
||||
class Virtual(Field):
|
||||
"""
|
||||
Virtual es un campo que no es renderizado en el xml final
|
||||
"""
|
||||
def __init__(self,
|
||||
setter=None,
|
||||
getter='',
|
||||
default=None,
|
||||
update_internal=False):
|
||||
"""
|
||||
:param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3
|
||||
:param getter: nombre del metodo usando cuando se obtiene, ejemplo: valor = mode.relation
|
||||
:param default: valor por defecto
|
||||
:param update_internal: indica que cuando se asigne algun valor este se almacena localmente
|
||||
"""
|
||||
self.default = default
|
||||
self.setter = setter
|
||||
self.getter = getter
|
||||
self.values = {}
|
||||
self.update_internal = update_internal
|
||||
self.virtual = True
|
||||
|
||||
def __get__(self, inst, cls):
|
||||
if inst is None:
|
||||
return self
|
||||
assert self.name is not None
|
||||
|
||||
value = self.default
|
||||
try:
|
||||
value = self.values[inst]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.values[inst] = getattr(inst, self.getter)(self.name, value)
|
||||
except AttributeError:
|
||||
self.values[inst] = value
|
||||
|
||||
return self.values[inst]
|
||||
|
||||
def __set__(self, inst, value):
|
||||
if self.update_internal:
|
||||
inst._value = value
|
||||
|
||||
if self.setter is None:
|
||||
self.values[inst] = value
|
||||
else:
|
||||
self.values[inst] = self._call(inst, self.setter, self.name, value)
|
||||
self._changed_field(inst, self.name, value)
|
||||
1499
poetry.lock
generated
Normal file
1499
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
pyproject.toml
Normal file
79
pyproject.toml
Normal file
@@ -0,0 +1,79 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "facho"
|
||||
version = "0.1.0"
|
||||
description = "Facturacion Electronica Colombia"
|
||||
readme = "README.rst"
|
||||
license = { text = "GPL-3.0-or-later" }
|
||||
authors = [
|
||||
{ name = "Jovany Leandro G.C", email = "bit4bit@riseup.net" },
|
||||
]
|
||||
keywords = ["facho"]
|
||||
classifiers = [
|
||||
"Development Status :: 2 - Pre-Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"Click>=8.1.7",
|
||||
"zeep>=4.2.1",
|
||||
"lxml>=5.2.2",
|
||||
"cryptography>=41.0.0",
|
||||
"pyOpenSSL>=23.2.0",
|
||||
"xmlsig>=0.1.9",
|
||||
"xades>=1.0.0",
|
||||
"xmlsec>=1.3.12",
|
||||
"python-dateutil>=2.9.0",
|
||||
# usamos esta dependencia en runtime
|
||||
# para forzar uso de policy_id de archivo local
|
||||
"mock>=5.1.0",
|
||||
"xmlschema>=3.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
facho = "facho.cli:main"
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/bit4bit/facho"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
build = ">=1.2.2"
|
||||
coverage = ">=7.8.0"
|
||||
flake8 = ">=7.0.0"
|
||||
pytest = ">=9.1.1,<10.0.0"
|
||||
tox = ">=4.23.2"
|
||||
|
||||
|
||||
[tool.setuptools]
|
||||
packages = { find = { exclude = ["tests*"] } }
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["*.gc", "*.xsd", "politicadefirmav2.pdf"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "0.1.0"
|
||||
commit = true
|
||||
tag = true
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "pyproject.toml"
|
||||
search = 'version = "{current_version}"'
|
||||
replace = 'version = "{new_version}"'
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "facho/__init__.py"
|
||||
search = "__version__ = '{current_version}'"
|
||||
replace = "__version__ = '{new_version}'"
|
||||
@@ -1,17 +0,0 @@
|
||||
attrs==22.1.0
|
||||
distlib==0.3.6
|
||||
filelock==3.8.0
|
||||
iniconfig==1.1.1
|
||||
packaging==21.3
|
||||
platformdirs==2.5.2
|
||||
pluggy==1.0.0
|
||||
py==1.11.0
|
||||
pyparsing==3.0.9
|
||||
pytest==7.1.3
|
||||
semantic-version==2.10.0
|
||||
setuptools-rust==1.5.2
|
||||
six==1.16.0
|
||||
tomli==2.0.1
|
||||
tox==3.26.0
|
||||
typing_extensions==4.4.0
|
||||
virtualenv==20.16.5
|
||||
25
setup.cfg
25
setup.cfg
@@ -1,25 +0,0 @@
|
||||
[bumpversion]
|
||||
current_version = 0.1.0
|
||||
commit = True
|
||||
tag = True
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
search = version='{current_version}'
|
||||
replace = version='{new_version}'
|
||||
|
||||
[bumpversion:file:facho/__init__.py]
|
||||
search = __version__ = '{current_version}'
|
||||
replace = __version__ = '{new_version}'
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[flake8]
|
||||
exclude = docs
|
||||
|
||||
[aliases]
|
||||
# Define setup.py command aliases here
|
||||
test = pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --ignore=setup.py
|
||||
93
setup.py
93
setup.py
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of facho. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
"""The setup script."""
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open('README.rst') as readme_file:
|
||||
readme = readme_file.read()
|
||||
|
||||
with open('HISTORY.rst') as history_file:
|
||||
history = history_file.read()
|
||||
|
||||
requirements = ['Click>=8.1.7',
|
||||
'zeep==4.2.1',
|
||||
'lxml==5.2.2',
|
||||
'cryptography==3.3.2',
|
||||
'pyOpenSSL==20.0.1',
|
||||
'xmlsig==0.1.7',
|
||||
'xades==1.0.0',
|
||||
'xmlsec==1.3.14',
|
||||
'python-dateutil==2.9.0.post0',
|
||||
# usamos esta dependencia en runtime
|
||||
# para forzar uso de policy_id de archivo local
|
||||
'mock>=5.1.0',
|
||||
'xmlschema>=3.0.0']
|
||||
|
||||
"""
|
||||
Listado de Versiones Anteriores
|
||||
requirements = ['Click>=6.0',
|
||||
'zeep==4.0.0',
|
||||
'lxml==4.6.3',
|
||||
'cryptography==3.3.2',
|
||||
'pyOpenSSL==20.0.1',
|
||||
'xmlsig==0.1.7',
|
||||
'xades==0.2.2',
|
||||
'xmlsec==1.3.12',
|
||||
# usamos esta dependencia en runtime
|
||||
# para forzar uso de policy_id de archivo local
|
||||
'mock>=2.0.0',
|
||||
'xmlschema>=1.8']
|
||||
|
||||
"""
|
||||
|
||||
setup_requirements = ['pytest-runner', ]
|
||||
|
||||
test_requirements = ['pytest', ]
|
||||
|
||||
setup(
|
||||
author="Jovany Leandro G.C",
|
||||
author_email='bit4bit@riseup.net',
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
],
|
||||
description="Facturacion Electronica Colombia",
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'facho=facho.cli:main',
|
||||
],
|
||||
},
|
||||
install_requires=requirements,
|
||||
license="GNU General Public License v3",
|
||||
long_description=readme + '\n\n' + history,
|
||||
long_description_content_type='text/x-rst',
|
||||
include_package_data=True,
|
||||
package_data = {
|
||||
# If any package contains *.txt or *.rst files, include them:
|
||||
'': ['*.gc', '*.xsd', 'politicadefirmav2.pdf']
|
||||
},
|
||||
keywords='facho',
|
||||
name='facho',
|
||||
packages=find_packages(exclude=("tests",)),
|
||||
setup_requires=setup_requirements,
|
||||
test_suite='tests',
|
||||
tests_require=test_requirements,
|
||||
url='https://github.com/bit4bit/facho',
|
||||
<<<<<<< HEAD
|
||||
version='0.2.0',
|
||||
=======
|
||||
version='0.2.1',
|
||||
>>>>>>> morfo
|
||||
zip_safe=False,
|
||||
)
|
||||
@@ -2,6 +2,7 @@ import pytest
|
||||
import facho.fe.form as form
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_debit_note_without_lines():
|
||||
inv = form.DebitNote(form.InvoiceDocumentReference(
|
||||
@@ -15,7 +16,7 @@ def simple_debit_note_without_lines():
|
||||
inv.set_supplier(form.Party(
|
||||
name='facho-supplier',
|
||||
ident=form.PartyIdentification('123', '', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
@@ -36,38 +37,47 @@ def simple_debit_note_without_lines():
|
||||
))
|
||||
return inv
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_credit_note_without_lines():
|
||||
inv = form.CreditNote(form.InvoiceDocumentReference('1234', 'xx', datetime.now()))
|
||||
inv = form.CreditNote(
|
||||
form.InvoiceDocumentReference(
|
||||
'1234', 'xx', datetime.now()))
|
||||
inv.set_period(datetime.now(), datetime.now())
|
||||
inv.set_issue(datetime.now())
|
||||
inv.set_ident('ABC123')
|
||||
inv.set_operation_type('20')
|
||||
inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234'))
|
||||
inv.set_payment_mean(
|
||||
form.PaymentMean(
|
||||
form.PaymentMean.DEBIT,
|
||||
'41',
|
||||
datetime.now(),
|
||||
'1234'))
|
||||
inv.set_supplier(form.Party(
|
||||
name = 'facho-supplier',
|
||||
ident = form.PartyIdentification('123','', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_regime_code = '48',
|
||||
organization_code = '1',
|
||||
address = form.Address(
|
||||
name='facho-supplier',
|
||||
ident=form.PartyIdentification('123', '', '31'),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia'))
|
||||
))
|
||||
inv.set_customer(form.Party(
|
||||
name = 'facho-customer',
|
||||
ident = form.PartyIdentification('321', '', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_regime_code = '48',
|
||||
organization_code = '1',
|
||||
address = form.Address(
|
||||
name='facho-customer',
|
||||
ident=form.PartyIdentification('321', '', '31'),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia'))
|
||||
))
|
||||
return inv
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_invoice_without_lines():
|
||||
inv = form.NationalSalesInvoice()
|
||||
@@ -75,25 +85,30 @@ def simple_invoice_without_lines():
|
||||
inv.set_issue(datetime.now())
|
||||
inv.set_ident('ABC123')
|
||||
inv.set_operation_type('10')
|
||||
inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234'))
|
||||
inv.set_payment_mean(
|
||||
form.PaymentMean(
|
||||
form.PaymentMean.DEBIT,
|
||||
'41',
|
||||
datetime.now(),
|
||||
'1234'))
|
||||
inv.set_supplier(form.Party(
|
||||
name = 'facho-supplier',
|
||||
ident = form.PartyIdentification('123','', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_regime_code = '48',
|
||||
organization_code = '1',
|
||||
address = form.Address(
|
||||
name='facho-supplier',
|
||||
ident=form.PartyIdentification('123', '', '31'),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia'))
|
||||
))
|
||||
inv.set_customer(form.Party(
|
||||
name = 'facho-customer',
|
||||
ident = form.PartyIdentification('321', '', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_regime_code = '48',
|
||||
organization_code = '1',
|
||||
address = form.Address(
|
||||
name='facho-customer',
|
||||
ident=form.PartyIdentification('321', '', '31'),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia'))
|
||||
@@ -108,25 +123,30 @@ def simple_invoice():
|
||||
inv.set_issue(datetime.now())
|
||||
inv.set_ident('ABC123')
|
||||
inv.set_operation_type('10')
|
||||
inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), ' 1234'))
|
||||
inv.set_payment_mean(
|
||||
form.PaymentMean(
|
||||
form.PaymentMean.DEBIT,
|
||||
'41',
|
||||
datetime.now(),
|
||||
' 1234'))
|
||||
inv.set_supplier(form.Party(
|
||||
name = 'facho-supplier',
|
||||
ident = form.PartyIdentification('123','', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_regime_code = '48',
|
||||
organization_code = '1',
|
||||
address = form.Address(
|
||||
name='facho-supplier',
|
||||
ident=form.PartyIdentification('123', '', '31'),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia'))
|
||||
))
|
||||
inv.set_customer(form.Party(
|
||||
name = 'facho-customer',
|
||||
ident = form.PartyIdentification('321','', '31'),
|
||||
responsability_code = form.Responsability(['ZZ']),
|
||||
responsability_regime_code = '48',
|
||||
organization_code = '1',
|
||||
address = form.Address(
|
||||
name='facho-customer',
|
||||
ident=form.PartyIdentification('321', '', '31'),
|
||||
responsability_code=form.Responsability(['ZZ']),
|
||||
responsability_regime_code='48',
|
||||
organization_code='1',
|
||||
address=form.Address(
|
||||
'', '', form.City('05001', 'Medellín'),
|
||||
form.Country('CO', 'Colombia'),
|
||||
form.CountrySubentity('05', 'Antioquia'))
|
||||
@@ -136,7 +156,7 @@ def simple_invoice():
|
||||
quantity=form.Quantity(1, '94'),
|
||||
description='productofacho',
|
||||
item=form.StandardItem(9999),
|
||||
price=form.Price(form.Amount(100.0),'01',''),
|
||||
price=form.Price(form.Amount(100.0), '01', ''),
|
||||
tax=form.TaxTotal(
|
||||
tax_amount=form.Amount(0.0),
|
||||
taxable_amount=form.Amount(0.0),
|
||||
|
||||
@@ -14,6 +14,7 @@ def test_amount_positive():
|
||||
with pytest.raises(ValueError):
|
||||
form.Amount(-1.0)
|
||||
|
||||
|
||||
def test_amount_equals():
|
||||
price1 = form.Amount(110.0)
|
||||
price2 = form.Amount(100 + 10.0)
|
||||
@@ -22,6 +23,7 @@ def test_amount_equals():
|
||||
assert price1 == form.Amount(10) * form.Amount(10) + form.Amount(10)
|
||||
assert form.Amount(110) == (form.Amount(1.10) * form.Amount(100))
|
||||
|
||||
|
||||
def test_round_half_even():
|
||||
# https://www.w3.org/TR/xpath-functions-31/#func-round-half-to-even
|
||||
assert form.Amount(0.5).round(0).float() == 0.0
|
||||
@@ -30,16 +32,20 @@ def test_round_half_even():
|
||||
assert form.Amount(3.567812e+3).round(2).float() == 3567.81e0
|
||||
assert form.Amount(4.7564e-3).round(2).float() == 0.0e0
|
||||
|
||||
|
||||
def test_round():
|
||||
# Entre 0 y 5 Mantener el dígito menos significativo
|
||||
assert form.Amount(1.133).round(2) == form.Amount(1.13)
|
||||
# Entre 6 y 9 Incrementar el dígito menos significativo
|
||||
assert form.Amount(1.166).round(2) == form.Amount(1.17)
|
||||
# 5, y el segundo dígito siguiente al dígito menos significativo es cero o par Mantener el dígito menos significativo
|
||||
# 5, y el segundo dígito siguiente al dígito menos significativo es cero o
|
||||
# par Mantener el dígito menos significativo
|
||||
assert str(form.Amount(1.1542).round(2)) == str(form.Amount(1.15))
|
||||
# 5, y el segundo dígito siguiente al dígito menos significativo es impar Incrementar el dígito menos significativo
|
||||
# 5, y el segundo dígito siguiente al dígito menos significativo es impar
|
||||
# Incrementar el dígito menos significativo
|
||||
assert str(form.Amount(1.1563).round(2)) == str(form.Amount(1.16))
|
||||
|
||||
|
||||
def test_amount_truncate():
|
||||
assert form.Amount(1.1569).truncate_as_string(2) == '1.15'
|
||||
assert form.Amount(587.0700).truncate_as_string(2) == '587.07'
|
||||
@@ -49,5 +55,6 @@ def test_amount_truncate():
|
||||
assert form.Amount(10000.02245).truncate_as_string(2) == '10000.02'
|
||||
assert form.Amount(10000.02357).truncate_as_string(2) == '10000.02'
|
||||
|
||||
|
||||
def test_amount_format():
|
||||
assert str(round(form.Amount(1.1569),2)) == '1.16'
|
||||
assert str(round(form.Amount(1.1569), 2)) == '1.16'
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of facho. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import pytest
|
||||
from facho.fe import form_xml
|
||||
from fixtures import simple_invoice
|
||||
|
||||
simple_invoice = simple_invoice
|
||||
|
||||
|
||||
def test_application_response(simple_invoice):
|
||||
|
||||
doc = form_xml.ApplicationResponse(simple_invoice)
|
||||
xml = doc.toFachoXML()
|
||||
|
||||
with open("application_response.xml", "w") as fh:
|
||||
fh.write(xml.tostring())
|
||||
# raise Exception(xml.tostring())
|
||||
# assert xml.get_element_text(
|
||||
# './apr:ApplicationResponse')
|
||||
@@ -4,122 +4,13 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
# from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from facho.fe import form_xml
|
||||
from datetime import datetime
|
||||
import helpers
|
||||
from fixtures import simple_invoice
|
||||
# import pytest
|
||||
# from facho.fe import form_xml
|
||||
|
||||
simple_invoice = simple_invoice
|
||||
# import helpers
|
||||
|
||||
def test_xml_with_required_elements(simple_invoice):
|
||||
|
||||
xml_header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
|
||||
DIANInvoiceXML = form_xml.DIANInvoiceXML(
|
||||
simple_invoice)
|
||||
|
||||
doc = form_xml.AttachedDocument(
|
||||
simple_invoice,
|
||||
DIANInvoiceXML,
|
||||
id='123')
|
||||
|
||||
xml = doc.toFachoXML()
|
||||
|
||||
DIANInvoiceXML = form_xml.DIANInvoiceXML(
|
||||
simple_invoice, 'Invoice').attach_invoice
|
||||
|
||||
ApplicationResponse = xml_header + form_xml.ApplicationResponse(simple_invoice).toFachoXML().tostring()
|
||||
|
||||
attached_document = xml_header + DIANInvoiceXML.tostring()
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:UBLVersionID') == 'UBL 2.1'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:CustomizationID') == 'Documentos adjuntos'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:ProfileID') == 'Factura Electrónica de Venta'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:ProfileExecutionID') == '1'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:ID') == '123'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:IssueDate') == str(datetime.today().date())
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:IssueTime') == datetime.today(
|
||||
).time().strftime(
|
||||
'%H:%M:%S-05:00')
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:DocumentType'
|
||||
) == 'Contenedor de Factura Electrónica'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cbc:ParentDocumentID'
|
||||
) == 'ABC123'
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cbc:RegistrationName'
|
||||
) == 'facho-supplier'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cbc:CompanyID'
|
||||
) == '123'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cbc:TaxLevelCode'
|
||||
) == 'ZZ'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID'
|
||||
) == '01'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name'
|
||||
) == 'IVA'
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cbc:RegistrationName'
|
||||
) == 'facho-customer'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cbc:CompanyID'
|
||||
) == '321'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cbc:TaxLevelCode'
|
||||
) == 'ZZ'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID'
|
||||
) == '01'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name'
|
||||
) == 'IVA'
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:Attachment/cac:ExternalReference/cbc:MimeCode'
|
||||
) == "text/xml"
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:Attachment/cac:ExternalReference/cbc:EncodingCode'
|
||||
) == "UTF-8"
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:Attachment/cac:ExternalReference/cbc:Description'
|
||||
) == "<![CDATA[{}]]>".format(attached_document)
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cbc:LineID'
|
||||
) == '1'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:ID'
|
||||
) == '1234'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:UUID'
|
||||
) == '1234'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:IssueDate'
|
||||
) == '2024-11-28'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:DocumentType'
|
||||
) == 'ApplicationResponse'
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:MimeCode'
|
||||
) == 'text/xml'
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:EncodingCode'
|
||||
) == "UTF-8"
|
||||
|
||||
assert xml.get_element_text(
|
||||
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:Description'
|
||||
) == "<![CDATA[{}]]>".format(ApplicationResponse)
|
||||
# def test_xml_with_required_elements():
|
||||
# doc = form_xml.AttachedDocument(id='123')
|
||||
# xml = doc.toFachoXML()
|
||||
# assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123'
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from facho.fe.client import dian
|
||||
from facho import facho
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
class FakeDianClient(dian.DianClient):
|
||||
def __init__(self, username, password, resp):
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
#from click.testing import CliRunner
|
||||
# from click.testing import CliRunner
|
||||
|
||||
from facho import facho
|
||||
#from facho import cli
|
||||
# from facho import cli
|
||||
|
||||
|
||||
def test_facho_xml():
|
||||
@@ -20,18 +20,19 @@ def test_facho_xml():
|
||||
invoice.text = 'Test'
|
||||
assert xml.tostring() == '<root><Invoice>Test</Invoice></root>'
|
||||
|
||||
invoice_line = xml.find_or_create_element('/root/Invoice/Line')
|
||||
xml.find_or_create_element('/root/Invoice/Line')
|
||||
assert xml.tostring() == '<root><Invoice>Test<Line/></Invoice></root>'
|
||||
|
||||
|
||||
def test_facho_xml_with_attr():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.find_or_create_element('/root/Invoice[id=123]')
|
||||
xml.find_or_create_element('/root/Invoice[id=123]')
|
||||
assert xml.tostring() == '<root><Invoice id="123"/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_idempotent():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.find_or_create_element('/root/Invoice')
|
||||
xml.find_or_create_element('/root/Invoice')
|
||||
assert xml.tostring() == '<root><Invoice/></root>'
|
||||
|
||||
xml.find_or_create_element('/root/Invoice')
|
||||
@@ -46,6 +47,7 @@ def test_facho_xml_idempotent():
|
||||
xml.find_or_create_element('/root/Invoice/Line')
|
||||
assert xml.tostring() == '<root><Invoice><Line/></Invoice></root>'
|
||||
|
||||
|
||||
def test_facho_xml_aliases():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.register_alias_xpath('Invoice', '/root/Invoice')
|
||||
@@ -54,31 +56,42 @@ def test_facho_xml_aliases():
|
||||
invoice.text = 'Test'
|
||||
assert xml.tostring() == '<root><Invoice>Test</Invoice></root>'
|
||||
|
||||
|
||||
def test_facho_xmlns():
|
||||
xml = facho.FachoXML('root', nsmap={
|
||||
'ext': 'https://ext',
|
||||
'sts': 'https://sts',
|
||||
})
|
||||
|
||||
invoiceAuthorization = xml.find_or_create_element('/root/ext:UBLExtensions/ext:UBLExtension/'
|
||||
'ext:ExtensionContent/sts:DianExtensions/'
|
||||
'sts:InvoiceControl/sts:InvoiceAuthorization')
|
||||
assert xml.tostring().strip() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:UBLExtensions>'\
|
||||
'<ext:UBLExtension>'\
|
||||
'<ext:ExtensionContent>'\
|
||||
'<sts:DianExtensions>'\
|
||||
'<sts:InvoiceControl>'\
|
||||
'<sts:InvoiceAuthorization/>'\
|
||||
'</sts:InvoiceControl></sts:DianExtensions></ext:ExtensionContent></ext:UBLExtension></ext:UBLExtensions></root>'
|
||||
invoiceAuthorization = xml.find_or_create_element(
|
||||
'/root/ext:UBLExtensions/ext:UBLExtension/'
|
||||
'ext:ExtensionContent/sts:DianExtensions/'
|
||||
'sts:InvoiceControl/sts:InvoiceAuthorization')
|
||||
assert xml.tostring().strip() == (
|
||||
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
|
||||
'<ext:UBLExtensions>'
|
||||
'<ext:UBLExtension>'
|
||||
'<ext:ExtensionContent>'
|
||||
'<sts:DianExtensions>'
|
||||
'<sts:InvoiceControl>'
|
||||
'<sts:InvoiceAuthorization/>'
|
||||
'</sts:InvoiceControl></sts:DianExtensions>'
|
||||
'</ext:ExtensionContent></ext:UBLExtension>'
|
||||
'</ext:UBLExtensions></root>')
|
||||
|
||||
invoiceAuthorization.text = '123456789'
|
||||
assert xml.tostring().strip() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:UBLExtensions>'\
|
||||
'<ext:UBLExtension>'\
|
||||
'<ext:ExtensionContent>'\
|
||||
'<sts:DianExtensions>'\
|
||||
'<sts:InvoiceControl>'\
|
||||
'<sts:InvoiceAuthorization>123456789</sts:InvoiceAuthorization>'\
|
||||
'</sts:InvoiceControl></sts:DianExtensions></ext:ExtensionContent></ext:UBLExtension></ext:UBLExtensions></root>'
|
||||
assert xml.tostring().strip() == (
|
||||
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
|
||||
'<ext:UBLExtensions>'
|
||||
'<ext:UBLExtension>'
|
||||
'<ext:ExtensionContent>'
|
||||
'<sts:DianExtensions>'
|
||||
'<sts:InvoiceControl>'
|
||||
'<sts:InvoiceAuthorization>123456789</sts:InvoiceAuthorization>'
|
||||
'</sts:InvoiceControl></sts:DianExtensions>'
|
||||
'</ext:ExtensionContent></ext:UBLExtension>'
|
||||
'</ext:UBLExtensions></root>')
|
||||
|
||||
|
||||
def test_facho_xmlns_idempotent():
|
||||
xml = facho.FachoXML('root', nsmap={
|
||||
@@ -87,16 +100,22 @@ def test_facho_xmlns_idempotent():
|
||||
})
|
||||
|
||||
xml.find_or_create_element('/root/ext:Extension/sts:Sotoros')
|
||||
assert xml.tostring() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:Extension><sts:Sotoros/></ext:Extension></root>'
|
||||
assert xml.tostring() == (
|
||||
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
|
||||
'<ext:Extension><sts:Sotoros/></ext:Extension></root>')
|
||||
|
||||
xml.find_or_create_element('/root/ext:Extension/sts:Sotoros')
|
||||
assert xml.tostring() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:Extension><sts:Sotoros/></ext:Extension></root>'
|
||||
assert xml.tostring() == (
|
||||
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
|
||||
'<ext:Extension><sts:Sotoros/></ext:Extension></root>')
|
||||
|
||||
|
||||
def test_facho_xml_set_element_with_format():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.set_element('/root/Invoice', 1, format_='%02d')
|
||||
xml.set_element('/root/Invoice', 1, format_='%02d')
|
||||
assert xml.tostring() == '<root><Invoice>01</Invoice></root>'
|
||||
|
||||
|
||||
def test_facho_xml_fragment():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.fragment('/root/Invoice')
|
||||
@@ -116,7 +135,10 @@ def test_facho_xml_fragments():
|
||||
line = xml.fragment('/Invoice/Line', append=True)
|
||||
line.set_element('/Line/Id', 3)
|
||||
|
||||
assert xml.tostring() == '<Invoice><Line><Id>1</Id></Line><Line><Id>2</Id></Line><Line><Id>3</Id></Line></Invoice>'
|
||||
assert xml.tostring() == (
|
||||
'<Invoice><Line><Id>1</Id></Line><Line><Id>2</Id></Line>'
|
||||
'<Line><Id>3</Id></Line></Invoice>')
|
||||
|
||||
|
||||
def test_facho_xml_nested_fragments():
|
||||
xml = facho.FachoXML('Invoice')
|
||||
@@ -128,15 +150,20 @@ def test_facho_xml_nested_fragments():
|
||||
|
||||
party.set_element('/Party/LastName', 'test')
|
||||
|
||||
assert xml.tostring() == '<Invoice><Party><Name>test</Name><Address><Line>line 1</Line></Address><LastName>test</LastName></Party></Invoice>'
|
||||
assert xml.tostring() == (
|
||||
'<Invoice><Party><Name>test</Name>'
|
||||
'<Address><Line>line 1</Line></Address>'
|
||||
'<LastName>test</LastName></Party></Invoice>')
|
||||
|
||||
|
||||
def test_facho_xml_get_element_text_of_fragment():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.fragment('/root/Invoice')
|
||||
invoice.set_element('/Invoice/Id', 1)
|
||||
|
||||
|
||||
assert invoice.get_element_text('/Invoice/Id') == '1'
|
||||
|
||||
|
||||
def test_facho_xml_get_element_text():
|
||||
xml = facho.FachoXML('Invoice')
|
||||
xml.set_element('/Invoice/ID', 'ABC123')
|
||||
@@ -147,6 +174,7 @@ def test_facho_xml_get_element_text():
|
||||
line.set_element('/Line/Quantity', 5)
|
||||
assert line.get_element_text('/Line/Quantity', format_=int) == 5
|
||||
|
||||
|
||||
def test_facho_xml_get_element_text_next_child():
|
||||
xml = facho.FachoXML('Invoice')
|
||||
xml.set_element('/Invoice/ID', 'ABC123')
|
||||
@@ -166,25 +194,32 @@ def test_facho_xml_set_element_relative():
|
||||
|
||||
assert xml.get_element_text('/Invoice/ID') == 'ABC123'
|
||||
|
||||
|
||||
def test_facho_xml_set_element_relative_with_namespace():
|
||||
xml = facho.FachoXML('{%s}Invoice' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
|
||||
nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
|
||||
xml = facho.FachoXML(
|
||||
'{%s}Invoice' %
|
||||
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
|
||||
nsmap={
|
||||
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
|
||||
xml.set_element('./ID', 'ABC123')
|
||||
|
||||
assert xml.get_element_text('/fe:Invoice/ID') == 'ABC123'
|
||||
|
||||
|
||||
def test_facho_xml_fragment_relative():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.fragment('./Invoice')
|
||||
invoice.set_element('./Id', 1)
|
||||
assert xml.tostring() == '<root><Invoice><Id>1</Id></Invoice></root>'
|
||||
|
||||
|
||||
def test_facho_xml_get_element_fragment_relative():
|
||||
xml = facho.FachoXML('root')
|
||||
invoice = xml.fragment('./Invoice')
|
||||
invoice.set_element('./Id', 1)
|
||||
assert invoice.get_element_text('./Id') == '1'
|
||||
|
||||
|
||||
def test_facho_xml_replacement_for():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./child/type')
|
||||
@@ -192,37 +227,47 @@ def test_facho_xml_replacement_for():
|
||||
'./child/code', 'test')
|
||||
assert xml.tostring() == '<root><child><code>test</code></child></root>'
|
||||
|
||||
|
||||
def test_facho_xml_set_element_content_invalid_validation():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
with pytest.raises(facho.FachoValueInvalid) as e:
|
||||
with pytest.raises(facho.FachoValueInvalid):
|
||||
xml.set_element_validator('./Id', lambda text, attrs: text == 'mero')
|
||||
xml.set_element('./Id', 'bad')
|
||||
|
||||
|
||||
def test_facho_xml_set_element_content_valid_validation():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
xml.set_element_validator('./Id', lambda text, attrs: text == 'mero')
|
||||
xml.set_element('./Id', 'mero')
|
||||
|
||||
|
||||
def test_facho_xml_set_element_attribute_invalid_validation():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
with pytest.raises(facho.FachoValueInvalid) as e:
|
||||
xml.set_element_validator('./Id', lambda text, attrs: attrs['code'] == 'ABC')
|
||||
xml.set_element('./Id', 'mero', code = 'CBA')
|
||||
with pytest.raises(facho.FachoValueInvalid):
|
||||
xml.set_element_validator(
|
||||
'./Id', lambda text, attrs: attrs['code'] == 'ABC')
|
||||
xml.set_element('./Id', 'mero', code='CBA')
|
||||
|
||||
|
||||
def test_facho_xml_set_element_attribute_valid_validation():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
xml.set_element_validator('./Id', lambda text, attrs: attrs['code'] == 'ABC')
|
||||
xml.set_element('./Id', 'mero', code = 'ABC')
|
||||
xml.set_element_validator(
|
||||
'./Id',
|
||||
lambda text,
|
||||
attrs: attrs['code'] == 'ABC')
|
||||
xml.set_element('./Id', 'mero', code='ABC')
|
||||
|
||||
|
||||
def test_facho_xml_get_element_attribute():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.set_element('./Id', 'mero', code = 'ABC')
|
||||
xml.set_element('./Id', 'mero', code='ABC')
|
||||
assert xml.get_element_attribute('/root/Id', 'code') == 'ABC'
|
||||
|
||||
|
||||
def test_facho_xml_keep_orden_slibing():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.find_or_create_element('./A')
|
||||
@@ -233,14 +278,16 @@ def test_facho_xml_keep_orden_slibing():
|
||||
|
||||
assert xml.tostring() == '<root><A/><A/><B/><B/><C/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_optional():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./A')
|
||||
xml.placeholder_for('./B', optional=True)
|
||||
xml.placeholder_for('./C')
|
||||
|
||||
|
||||
assert xml.tostring() == '<root><A/><C/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_append_to_optional():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./A')
|
||||
@@ -250,6 +297,7 @@ def test_facho_xml_placeholder_append_to_optional():
|
||||
xml.find_or_create_element('./B')
|
||||
assert xml.tostring() == '<root><A/><B/><C/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_set_element_to_optional():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./A')
|
||||
@@ -259,6 +307,7 @@ def test_facho_xml_placeholder_set_element_to_optional():
|
||||
xml.set_element('./B', '2')
|
||||
assert xml.tostring() == '<root><A/><B>2</B><C/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_set_element_to_optional_with_append():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./A')
|
||||
@@ -275,8 +324,8 @@ def test_facho_xml_set_attributes():
|
||||
xml.find_or_create_element('./A')
|
||||
|
||||
xml.set_attributes('./A',
|
||||
value1 = '1',
|
||||
value2 = '2'
|
||||
value1='1',
|
||||
value2='2'
|
||||
)
|
||||
assert xml.get_element_attribute('/root/A', 'value1') == '1'
|
||||
assert xml.get_element_attribute('/root/A', 'value2') == '2'
|
||||
@@ -287,13 +336,14 @@ def test_facho_xml_set_attributes_not_set_optional():
|
||||
xml.find_or_create_element('./A')
|
||||
|
||||
xml.set_attributes('./A',
|
||||
value1 = None,
|
||||
value2 = '2'
|
||||
value1=None,
|
||||
value2='2'
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
xml.get_element_attribute('/root/A', 'value1')
|
||||
assert xml.get_element_attribute('/root/A', 'value2') == '2'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_with_fragment():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./A')
|
||||
@@ -305,8 +355,10 @@ def test_facho_xml_placeholder_with_fragment():
|
||||
AA.find_or_create_element('./B', append=True)
|
||||
|
||||
AA = xml.fragment('./AA/Child', append=True)
|
||||
|
||||
assert xml.tostring() == '<root><A/><AA><Child><B/><B/></Child><Child/></AA><AAA/></root>'
|
||||
|
||||
assert xml.tostring() == (
|
||||
'<root><A/><AA><Child><B/><B/></Child><Child/></AA><AAA/></root>')
|
||||
|
||||
|
||||
def test_facho_xml_create_on_first_append():
|
||||
xml = facho.FachoXML('root')
|
||||
@@ -314,6 +366,7 @@ def test_facho_xml_create_on_first_append():
|
||||
xml.find_or_create_element('./A', append=True)
|
||||
assert xml.tostring() == '<root><A/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_create_on_first_append_multiple_appends():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -324,6 +377,7 @@ def test_facho_xml_create_on_first_append_multiple_appends():
|
||||
xml.find_or_create_element('./C', append=True)
|
||||
assert xml.tostring() == '<root><B/><A/><A/><A/><C/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_fragment_create_on_first_append():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -333,6 +387,7 @@ def test_facho_xml_fragment_create_on_first_append():
|
||||
A.find_or_create_element('./C')
|
||||
assert xml.tostring() == '<root><A><B/></A><A><C/></A></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_optional_and_fragment():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -346,6 +401,7 @@ def test_facho_xml_placeholder_optional_and_fragment():
|
||||
|
||||
assert xml.tostring() == '<root><A><AA><B/><C/></AA></A></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_optional_and_set_attributes():
|
||||
xml = facho.FachoXML('root')
|
||||
xml.placeholder_for('./A')
|
||||
@@ -354,6 +410,7 @@ def test_facho_xml_placeholder_optional_and_set_attributes():
|
||||
assert xml.get_element_attribute('/root/A', 'prueba') == 'OK'
|
||||
assert xml.tostring() == '<root><A prueba="OK"/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_placeholder_optional_and_fragment_with_set_element():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -365,17 +422,19 @@ 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.exist_element('/root/A') is False
|
||||
assert xml.tostring() == '<root><A/></root>'
|
||||
|
||||
|
||||
xml.find_or_create_element('./A')
|
||||
assert xml.exist_element('/root/A') == True
|
||||
assert xml.exist_element('/root/A')
|
||||
assert xml.tostring() == '<root><A/></root>'
|
||||
|
||||
|
||||
def test_facho_xml_query_element_text_or_attribute():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -384,6 +443,7 @@ def test_facho_xml_query_element_text_or_attribute():
|
||||
assert xml.get_element_text_or_attribute('/root/A') == 'contenido'
|
||||
assert xml.get_element_text_or_attribute('/root/A/@clave') == 'valor'
|
||||
|
||||
|
||||
def test_facho_xml_query_element_text_or_attribute_from_fragment():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -392,6 +452,7 @@ def test_facho_xml_query_element_text_or_attribute_from_fragment():
|
||||
|
||||
assert invoice.get_element_text_or_attribute('/Invoice/A') == 'contenido'
|
||||
|
||||
|
||||
def test_facho_xml_build_xml_absolute():
|
||||
xml = facho.FachoXML('root')
|
||||
|
||||
@@ -400,18 +461,23 @@ def test_facho_xml_build_xml_absolute():
|
||||
|
||||
|
||||
def test_facho_xml_build_xml_absolute_namespace():
|
||||
xml = facho.FachoXML('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
|
||||
nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
|
||||
xml = facho.FachoXML(
|
||||
'{%s}root' %
|
||||
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
|
||||
nsmap={
|
||||
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
|
||||
|
||||
xpath = xml.xpath_from_root('/A')
|
||||
assert xpath == '/fe:root/A'
|
||||
|
||||
|
||||
def test_facho_xml_build_xml_absolute_namespace_from_fragment():
|
||||
xml = facho.FachoXML('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
|
||||
nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
|
||||
xml = facho.FachoXML(
|
||||
'{%s}root' %
|
||||
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
|
||||
nsmap={
|
||||
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
|
||||
invoice = xml.fragment('/root/Invoice')
|
||||
|
||||
|
||||
xpath = invoice.xpath_from_root('/A')
|
||||
assert xpath == '/fe:root/Invoice/A'
|
||||
|
||||
|
||||
@@ -4,16 +4,14 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from facho import fe
|
||||
|
||||
import helpers
|
||||
|
||||
|
||||
def test_xmlsigned_build(monkeypatch):
|
||||
# openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 -newkey rsa:2048 -keyout example.key -out example.pem
|
||||
# openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem
|
||||
# openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1
|
||||
# -newkey rsa:2048 -keyout example.key -out example.pem
|
||||
# openssl pkcs12 -export -out example.p12 -inkey example.key -in
|
||||
# example.pem
|
||||
signer = fe.DianXMLExtensionSigner('./tests/example.p12')
|
||||
|
||||
xml = fe.FeXML('Invoice',
|
||||
@@ -21,68 +19,90 @@ def test_xmlsigned_build(monkeypatch):
|
||||
|
||||
signer.sign_xml_element(xml.root)
|
||||
|
||||
elem = xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature')
|
||||
elem = xml.find_or_create_element(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
|
||||
'/ds:Signature')
|
||||
|
||||
assert elem is not None
|
||||
#assert elem.findall('ds:SignedInfo', fe.NAMESPACES) is not None
|
||||
# assert elem.findall('ds:SignedInfo', fe.NAMESPACES) is not None
|
||||
|
||||
|
||||
def test_xmlsigned_with_passphrase_build(monkeypatch):
|
||||
#openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 -newkey rsa:2048 -keyout example.key -out example.pem
|
||||
#openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem
|
||||
signer = fe.DianXMLExtensionSigner('./tests/example-with-passphrase.p12', 'test')
|
||||
# openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1
|
||||
# -newkey rsa:2048 -keyout example.key -out example.pem
|
||||
# openssl pkcs12 -export -out example.p12 -inkey example.key -in
|
||||
# example.pem
|
||||
signer = fe.DianXMLExtensionSigner(
|
||||
'./tests/example-with-passphrase.p12', 'test')
|
||||
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
|
||||
signer.sign_xml_element(xml.root)
|
||||
|
||||
elem = xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature')
|
||||
elem = xml.find_or_create_element(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
|
||||
'/ds:Signature')
|
||||
|
||||
assert elem is not None
|
||||
#assert elem.findall('ds:SignedInfo', fe.NAMESPACES) is not None
|
||||
# assert elem.findall('ds:SignedInfo', fe.NAMESPACES) is not None
|
||||
|
||||
|
||||
def test_dian_extension_software_security_code():
|
||||
security_code = fe.DianXMLExtensionSoftwareSecurityCode('idsoftware', '1234', '1')
|
||||
security_code = fe.DianXMLExtensionSoftwareSecurityCode(
|
||||
'idsoftware', '1234', '1')
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.add_extension(security_code)
|
||||
content = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode')
|
||||
content = xml.get_element_text(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
|
||||
'/sts:DianExtensions/sts:SoftwareSecurityCode')
|
||||
assert content is not None
|
||||
|
||||
|
||||
def test_dian_extension_invoice_authorization():
|
||||
invoice_authorization = '18762002346472'
|
||||
inv_auth_ext = fe.DianXMLExtensionInvoiceAuthorization(invoice_authorization,
|
||||
datetime(2017, 2, 23),
|
||||
datetime(2019, 8, 23),
|
||||
'MD', 100001, 174999)
|
||||
inv_auth_ext = fe.DianXMLExtensionInvoiceAuthorization(
|
||||
invoice_authorization, datetime(
|
||||
2017, 2, 23), datetime(
|
||||
2019, 8, 23), 'MD', 100001, 174999)
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.add_extension(inv_auth_ext)
|
||||
auth = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl/sts:InvoiceAuthorization')
|
||||
auth = xml.get_element_text(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
|
||||
'/sts:DianExtensions/sts:InvoiceControl/sts:InvoiceAuthorization')
|
||||
assert auth == invoice_authorization
|
||||
|
||||
|
||||
def test_dian_extension_software_provider():
|
||||
nit = '123456789'
|
||||
id_software = 'ABCDASDF123'
|
||||
software_provider_extension = fe.DianXMLExtensionSoftwareProvider(nit, '', id_software)
|
||||
software_provider_extension = fe.DianXMLExtensionSoftwareProvider(
|
||||
nit, '', id_software)
|
||||
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.add_extension(software_provider_extension)
|
||||
|
||||
give_nit = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider/sts:ProviderID')
|
||||
give_nit = xml.get_element_text(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
|
||||
'/sts:DianExtensions/sts:SoftwareProvider/sts:ProviderID')
|
||||
assert nit == give_nit
|
||||
|
||||
|
||||
def test_dian_extension_authorization_provider():
|
||||
auth_provider_extension = fe.DianXMLExtensionAuthorizationProvider()
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.add_extension(auth_provider_extension)
|
||||
dian_nit = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
|
||||
dian_nit = xml.get_element_text(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/'
|
||||
'ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/'
|
||||
'sts:AuthorizationProviderID')
|
||||
assert dian_nit == '800197268'
|
||||
|
||||
|
||||
def test_dian_invoice_without_namespace_in_root():
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
@@ -92,10 +112,14 @@ def test_dian_invoice_without_namespace_in_root():
|
||||
|
||||
def test_xml_sign_dian(monkeypatch):
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True)
|
||||
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.find_or_create_element(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
ublextension = xml.fragment(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension',
|
||||
append=True)
|
||||
ublextension.find_or_create_element(
|
||||
'/ext:UBLExtension/ext:ExtensionContent')
|
||||
|
||||
xmlstring = xml.tostring()
|
||||
print(xmlstring)
|
||||
@@ -103,12 +127,17 @@ def test_xml_sign_dian(monkeypatch):
|
||||
xmlsigned = signer.sign_xml_string(xmlstring)
|
||||
assert "Signature" in xmlsigned
|
||||
|
||||
|
||||
def test_xml_sign_dian_using_bytes(monkeypatch):
|
||||
xml = fe.FeXML('Invoice',
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True)
|
||||
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
|
||||
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||
xml.find_or_create_element(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
|
||||
ublextension = xml.fragment(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension',
|
||||
append=True)
|
||||
ublextension.find_or_create_element(
|
||||
'/ext:UBLExtension/ext:ExtensionContent')
|
||||
|
||||
xmlstring = xml.tostring()
|
||||
p12_data = open('./tests/example.p12', 'rb').read()
|
||||
@@ -117,6 +146,7 @@ def test_xml_sign_dian_using_bytes(monkeypatch):
|
||||
xmlsigned = signer.sign_xml_string(xmlstring)
|
||||
assert "Signature" in xmlsigned
|
||||
|
||||
|
||||
def test_xml_signature_timestamp(monkeypatch):
|
||||
xml = fe.FeXML(
|
||||
'Invoice',
|
||||
@@ -130,3 +160,6 @@ def test_xml_signature_timestamp(monkeypatch):
|
||||
xmlstring = xml.tostring()
|
||||
signer = fe.DianXMLExtensionSigner('./tests/example.p12')
|
||||
xmlsigned = signer.sign_xml_string(xmlstring)
|
||||
|
||||
with open('invoice.xml', 'w') as file_:
|
||||
file_.write(xmlsigned)
|
||||
|
||||
@@ -41,11 +41,13 @@ def test_invoicesimple_build(simple_invoice):
|
||||
xml = DIANInvoiceXML(simple_invoice)
|
||||
|
||||
supplier_name = xml.get_element_text(
|
||||
'/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name')
|
||||
'/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc'
|
||||
':Name')
|
||||
assert supplier_name == simple_invoice.invoice_supplier.name
|
||||
|
||||
customer_name = xml.get_element_text(
|
||||
'/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name')
|
||||
'/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc'
|
||||
':Name')
|
||||
assert customer_name == simple_invoice.invoice_customer.name
|
||||
|
||||
|
||||
@@ -66,7 +68,8 @@ def test_invoicesimple_xml_signed(monkeypatch, simple_invoice):
|
||||
xml.add_extension(signer)
|
||||
|
||||
elem = xml.get_element(
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature')
|
||||
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext'
|
||||
':ExtensionContent/ds:Signature')
|
||||
assert elem.text is not None
|
||||
|
||||
|
||||
@@ -75,14 +78,16 @@ def test_invoicesimple_zip(simple_invoice):
|
||||
|
||||
zipdata = io.BytesIO()
|
||||
with fe.DianZIP(zipdata) as dianzip:
|
||||
name_invoice = dianzip.add_invoice_xml(simple_invoice.invoice_ident, str(xml_invoice))
|
||||
name_invoice = dianzip.add_invoice_xml(
|
||||
simple_invoice.invoice_ident, str(xml_invoice))
|
||||
|
||||
# el zip ademas de archivar debe comprimir los archivos
|
||||
# de lo contrario la DIAN lo rechaza
|
||||
with zipfile.ZipFile(zipdata) as dianzip:
|
||||
dianzip.testzip()
|
||||
for zipinfo in dianzip.infolist():
|
||||
assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, "se espera el zip comprimido"
|
||||
assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, (
|
||||
"se espera el zip comprimido")
|
||||
|
||||
with zipfile.ZipFile(zipdata) as dianzip:
|
||||
xml_data = dianzip.open(name_invoice).read().decode('utf-8')
|
||||
@@ -122,7 +127,8 @@ def test_invoice_invoice_type_code(simple_invoice):
|
||||
def test_invoice_totals(simple_invoice_without_lines):
|
||||
simple_invoice = simple_invoice_without_lines
|
||||
simple_invoice.invoice_ident = '323200000129'
|
||||
simple_invoice.invoice_issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
|
||||
simple_invoice.invoice_issue = datetime.strptime(
|
||||
'2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
|
||||
simple_invoice.invoice_supplier.ident = '700085371'
|
||||
simple_invoice.invoice_customer.ident = '800199436'
|
||||
simple_invoice.add_invoice_line(form.InvoiceLine(
|
||||
@@ -222,7 +228,8 @@ def test_invoice_cufe(simple_invoice_without_lines):
|
||||
assert formatVars[12] == '800199436', "NumAdq"
|
||||
|
||||
# ClTec
|
||||
assert formatVars[13] == '693ff6f2a553c3646a063436fd4dd9ded0311471', "ClTec"
|
||||
assert formatVars[13] == (
|
||||
'693ff6f2a553c3646a063436fd4dd9ded0311471'), "ClTec"
|
||||
|
||||
# TipoAmbiente
|
||||
assert formatVars[14] == '1', "TipoAmbiente"
|
||||
@@ -323,8 +330,13 @@ def test_debit_note_cude(simple_debit_note_without_lines):
|
||||
assert build_vars['TipoAmb'] == 2
|
||||
|
||||
cude_composicion = "".join(cude_extension.formatVars())
|
||||
assert cude_composicion == 'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.0090019726410254102102012'
|
||||
assert cude_composicion == (
|
||||
'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.009'
|
||||
'001'
|
||||
'9726410254102102012')
|
||||
|
||||
xml_invoice.add_extension(cude_extension)
|
||||
cude = xml_invoice.get_element_text('/fe:DebitNote/cbc:UUID')
|
||||
assert cude == '3fa73a86d57d9341c536afde1f85c4efd9d4591c2c22bce4dfb0e6b0d2e83b8f047a8bde7098292e9d2493e60d1c31da'
|
||||
assert cude == (
|
||||
'3fa73a86d57d9341c536afde1f85c4efd9d4591c2c22bce4dfb0e6b0d2e83b8f047a8'
|
||||
'bde7098292e9d2493e60d1c31da')
|
||||
|
||||
@@ -121,7 +121,9 @@ def test_FAU14():
|
||||
|
||||
wants = form.Amount(119.0 + 19.0 - 50.0)
|
||||
|
||||
assert inv.invoice_legal_monetary_total.payable_amount == wants, "got %s want %s" % (inv.invoice_legal_monetary_total.payable_amount, wants)
|
||||
assert inv.invoice_legal_monetary_total.payable_amount == wants, (
|
||||
"got %s want %s" % (
|
||||
inv.invoice_legal_monetary_total.payable_amount, wants))
|
||||
|
||||
|
||||
def test_invalid_tipo_operacion_nota_debito():
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
"""Tests for `facho` package."""
|
||||
|
||||
import pytest
|
||||
# from datetime import datetime
|
||||
import copy
|
||||
|
||||
from facho.fe import form
|
||||
from facho.fe import form_xml
|
||||
# from fixtures import *
|
||||
|
||||
|
||||
@@ -1,601 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of facho. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
"""Tests for `facho` package."""
|
||||
|
||||
import pytest
|
||||
|
||||
import facho.model
|
||||
import facho.model.fields as fields
|
||||
|
||||
def test_model_to_element():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
person = Person()
|
||||
|
||||
assert "<Person/>" == person.to_xml()
|
||||
|
||||
def test_model_to_element_with_attribute():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
id = fields.Attribute('id')
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
|
||||
personb = Person()
|
||||
personb.id = 44
|
||||
|
||||
assert "<Person id=\"33\"/>" == person.to_xml()
|
||||
assert "<Person id=\"44\"/>" == personb.to_xml()
|
||||
|
||||
def test_model_to_element_with_attribute_as_element():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID)
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
assert "<Person><ID>33</ID></Person>" == person.to_xml()
|
||||
|
||||
def test_many2one_with_custom_attributes():
|
||||
class TaxAmount(facho.model.Model):
|
||||
__name__ = 'TaxAmount'
|
||||
|
||||
currencyID = fields.Attribute('currencyID')
|
||||
|
||||
class TaxTotal(facho.model.Model):
|
||||
__name__ = 'TaxTotal'
|
||||
|
||||
amount = fields.Many2One(TaxAmount)
|
||||
|
||||
tax_total = TaxTotal()
|
||||
tax_total.amount = 3333
|
||||
tax_total.amount.currencyID = 'COP'
|
||||
assert '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml()
|
||||
|
||||
def test_many2one_with_custom_setter():
|
||||
|
||||
class PhysicalLocation(facho.model.Model):
|
||||
__name__ = 'PhysicalLocation'
|
||||
|
||||
id = fields.Attribute('ID')
|
||||
|
||||
class Party(facho.model.Model):
|
||||
__name__ = 'Party'
|
||||
|
||||
location = fields.Many2One(PhysicalLocation, setter='location_setter')
|
||||
|
||||
def location_setter(self, field, value):
|
||||
field.id = value
|
||||
|
||||
party = Party()
|
||||
party.location = 99
|
||||
assert '<Party><PhysicalLocation ID="99"/></Party>' == party.to_xml()
|
||||
|
||||
def test_many2one_always_create():
|
||||
class Name(facho.model.Model):
|
||||
__name__ = 'Name'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
name = fields.Many2One(Name, default='facho')
|
||||
|
||||
person = Person()
|
||||
assert '<Person><Name>facho</Name></Person>' == person.to_xml()
|
||||
|
||||
def test_many2one_nested_always_create():
|
||||
class Name(facho.model.Model):
|
||||
__name__ = 'Name'
|
||||
|
||||
class Contact(facho.model.Model):
|
||||
__name__ = 'Contact'
|
||||
|
||||
name = fields.Many2One(Name, default='facho')
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
contact = fields.Many2One(Contact, create=True)
|
||||
|
||||
person = Person()
|
||||
assert '<Person><Contact><Name>facho</Name></Contact></Person>' == person.to_xml()
|
||||
|
||||
def test_many2one_auto_create():
|
||||
class TaxAmount(facho.model.Model):
|
||||
__name__ = 'TaxAmount'
|
||||
|
||||
currencyID = fields.Attribute('currencyID')
|
||||
|
||||
class TaxTotal(facho.model.Model):
|
||||
__name__ = 'TaxTotal'
|
||||
|
||||
amount = fields.Many2One(TaxAmount)
|
||||
|
||||
tax_total = TaxTotal()
|
||||
tax_total.amount.currencyID = 'COP'
|
||||
tax_total.amount = 3333
|
||||
assert '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml()
|
||||
|
||||
def test_field_model():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID)
|
||||
|
||||
person = Person()
|
||||
person.id = ID()
|
||||
person.id = 33
|
||||
assert "<Person><ID>33</ID></Person>" == person.to_xml()
|
||||
|
||||
def test_field_multiple_model():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID)
|
||||
id2 = fields.Many2One(ID)
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
person.id2 = 44
|
||||
assert "<Person><ID>33</ID><ID>44</ID></Person>" == person.to_xml()
|
||||
|
||||
def test_field_model_failed_initialization():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID)
|
||||
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
assert "<Person><ID>33</ID></Person>" == person.to_xml()
|
||||
|
||||
def test_field_model_with_custom_name():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID, name='DID')
|
||||
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
assert "<Person><DID>33</DID></Person>" == person.to_xml()
|
||||
|
||||
def test_field_model_default_initialization_with_attributes():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
reference = fields.Attribute('REFERENCE')
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID)
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
person.id.reference = 'haber'
|
||||
assert '<Person><ID REFERENCE="haber">33</ID></Person>' == person.to_xml()
|
||||
|
||||
def test_model_with_xml_namespace():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
__namespace__ = {
|
||||
'facho': 'http://lib.facho.cyou'
|
||||
}
|
||||
|
||||
person = Person()
|
||||
assert '<Person xmlns:facho="http://lib.facho.cyou"/>'
|
||||
|
||||
def test_model_with_xml_namespace_nested():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
__namespace__ = {
|
||||
'facho': 'http://lib.facho.cyou'
|
||||
}
|
||||
|
||||
id = fields.Many2One(ID, namespace='facho')
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == person.to_xml()
|
||||
|
||||
def test_model_with_xml_namespace_nested_nested():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Party(facho.model.Model):
|
||||
__name__ = 'Party'
|
||||
|
||||
id = fields.Many2One(ID, namespace='party')
|
||||
|
||||
def __default_set__(self, value):
|
||||
self.id = value
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
__namespace__ = {
|
||||
'person': 'http://lib.facho.cyou',
|
||||
'party': 'http://lib.facho.cyou'
|
||||
}
|
||||
|
||||
id = fields.Many2One(Party, namespace='person')
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
assert '<Person xmlns:person="http://lib.facho.cyou" xmlns:party="http://lib.facho.cyou"><person:Party><party:ID>33</party:ID></person:Party></Person>' == person.to_xml()
|
||||
|
||||
def test_model_with_xml_namespace_nested_one_many():
|
||||
class Name(facho.model.Model):
|
||||
__name__ = 'Name'
|
||||
|
||||
class Contact(facho.model.Model):
|
||||
__name__ = 'Contact'
|
||||
|
||||
name = fields.Many2One(Name, namespace='contact')
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
__namespace__ = {
|
||||
'facho': 'http://lib.facho.cyou',
|
||||
'contact': 'http://lib.facho.cyou'
|
||||
}
|
||||
|
||||
contacts = fields.One2Many(Contact, namespace='facho')
|
||||
|
||||
person = Person()
|
||||
contact = person.contacts.create()
|
||||
contact.name = 'contact1'
|
||||
|
||||
contact = person.contacts.create()
|
||||
contact.name = 'contact2'
|
||||
|
||||
assert '<Person xmlns:facho="http://lib.facho.cyou" xmlns:contact="http://lib.facho.cyou"><facho:Contact><contact:Name>contact1</contact:Name></facho:Contact><facho:Contact><contact:Name>contact2</contact:Name></facho:Contact></Person>' == person.to_xml()
|
||||
|
||||
def test_field_model_with_namespace():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
__namespace__ = {
|
||||
"facho": "http://lib.facho.cyou"
|
||||
}
|
||||
id = fields.Many2One(ID, namespace="facho")
|
||||
|
||||
|
||||
person = Person()
|
||||
person.id = 33
|
||||
assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == person.to_xml()
|
||||
|
||||
def test_field_hook_before_xml():
|
||||
class Hash(facho.model.Model):
|
||||
__name__ = 'Hash'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Many2One(Hash)
|
||||
|
||||
def __before_xml__(self):
|
||||
self.hash = "calculate"
|
||||
|
||||
person = Person()
|
||||
assert "<Person><Hash>calculate</Hash></Person>" == person.to_xml()
|
||||
|
||||
|
||||
def test_field_function_with_attribute():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Function(fields.Attribute('hash'), getter='get_hash')
|
||||
|
||||
def get_hash(self, name, field):
|
||||
return 'calculate'
|
||||
|
||||
person = Person()
|
||||
assert '<Person hash="calculate"/>'
|
||||
|
||||
def test_field_function_with_model():
|
||||
class Hash(facho.model.Model):
|
||||
__name__ = 'Hash'
|
||||
|
||||
id = fields.Attribute('id')
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Function(fields.Many2One(Hash), getter='get_hash')
|
||||
|
||||
def get_hash(self, name, field):
|
||||
field.id = 'calculate'
|
||||
|
||||
|
||||
person = Person()
|
||||
assert person.hash.id == 'calculate'
|
||||
assert '<Person/>'
|
||||
|
||||
|
||||
def test_field_function_setter():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Attribute('hash')
|
||||
password = fields.Virtual(setter='set_hash')
|
||||
|
||||
def set_hash(self, name, value):
|
||||
self.hash = "%s+2" % (value)
|
||||
|
||||
person = Person()
|
||||
person.password = 'calculate'
|
||||
assert '<Person hash="calculate+2"/>' == person.to_xml()
|
||||
|
||||
def test_field_function_only_setter():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Attribute('hash')
|
||||
password = fields.Virtual(setter='set_hash')
|
||||
|
||||
def set_hash(self, name, value):
|
||||
self.hash = "%s+2" % (value)
|
||||
|
||||
person = Person()
|
||||
person.password = 'calculate'
|
||||
assert '<Person hash="calculate+2"/>' == person.to_xml()
|
||||
|
||||
def test_model_set_default_setter():
|
||||
class Hash(facho.model.Model):
|
||||
__name__ = 'Hash'
|
||||
|
||||
def __default_set__(self, value):
|
||||
return "%s+3" % (value)
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Many2One(Hash)
|
||||
|
||||
person = Person()
|
||||
person.hash = 'hola'
|
||||
assert '<Person><Hash>hola+3</Hash></Person>' == person.to_xml()
|
||||
|
||||
|
||||
def test_field_virtual():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
age = fields.Virtual()
|
||||
|
||||
person = Person()
|
||||
person.age = 55
|
||||
assert person.age == 55
|
||||
assert "<Person/>" == person.to_xml()
|
||||
|
||||
|
||||
def test_field_inserted_default_attribute():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Attribute('hash', default='calculate')
|
||||
|
||||
|
||||
person = Person()
|
||||
assert '<Person hash="calculate"/>' == person.to_xml()
|
||||
|
||||
def test_field_function_inserted_default_attribute():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
hash = fields.Function(fields.Attribute('hash'), default='calculate')
|
||||
|
||||
person = Person()
|
||||
assert '<Person hash="calculate"/>' == person.to_xml()
|
||||
|
||||
def test_field_inserted_default_many2one():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
key = fields.Attribute('key')
|
||||
|
||||
def __default_set__(self, value):
|
||||
self.key = value
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID, default="oe")
|
||||
|
||||
person = Person()
|
||||
assert '<Person><ID key="oe"/></Person>' == person.to_xml()
|
||||
|
||||
def test_field_inserted_default_nested_many2one():
|
||||
class ID(facho.model.Model):
|
||||
__name__ = 'ID'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
id = fields.Many2One(ID, default="ole")
|
||||
|
||||
person = Person()
|
||||
assert '<Person><ID>ole</ID></Person>' == person.to_xml()
|
||||
|
||||
def test_model_on_change_field():
|
||||
class Hash(facho.model.Model):
|
||||
__name__ = 'Hash'
|
||||
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
react = fields.Attribute('react')
|
||||
hash = fields.Many2One(Hash)
|
||||
|
||||
@fields.on_change(['hash'])
|
||||
def on_change_react(self, name, value):
|
||||
assert name == 'hash'
|
||||
self.react = "%s+4" % (value)
|
||||
|
||||
person = Person()
|
||||
person.hash = 'hola'
|
||||
assert '<Person react="hola+4"><Hash>hola</Hash></Person>' == person.to_xml()
|
||||
|
||||
def test_model_on_change_field_attribute():
|
||||
class Person(facho.model.Model):
|
||||
__name__ = 'Person'
|
||||
|
||||
react = fields.Attribute('react')
|
||||
hash = fields.Attribute('Hash')
|
||||
|
||||
@fields.on_change(['hash'])
|
||||
def on_react(self, name, value):
|
||||
assert name == 'hash'
|
||||
self.react = "%s+4" % (value)
|
||||
|
||||
person = Person()
|
||||
person.hash = 'hola'
|
||||
assert '<Person react="hola+4" Hash="hola"/>' == person.to_xml()
|
||||
|
||||
def test_model_one2many():
|
||||
class Line(facho.model.Model):
|
||||
__name__ = 'Line'
|
||||
|
||||
quantity = fields.Attribute('quantity')
|
||||
|
||||
class Invoice(facho.model.Model):
|
||||
__name__ = 'Invoice'
|
||||
|
||||
lines = fields.One2Many(Line)
|
||||
|
||||
invoice = Invoice()
|
||||
line = invoice.lines.create()
|
||||
line.quantity = 3
|
||||
line = invoice.lines.create()
|
||||
line.quantity = 5
|
||||
assert '<Invoice><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml()
|
||||
|
||||
|
||||
def test_model_one2many_with_on_changes():
|
||||
class Line(facho.model.Model):
|
||||
__name__ = 'Line'
|
||||
|
||||
quantity = fields.Attribute('quantity')
|
||||
|
||||
class Invoice(facho.model.Model):
|
||||
__name__ = 'Invoice'
|
||||
|
||||
lines = fields.One2Many(Line)
|
||||
count = fields.Attribute('count', default=0)
|
||||
|
||||
@fields.on_change(['lines'])
|
||||
def refresh_count(self, name, value):
|
||||
self.count = len(self.lines)
|
||||
|
||||
invoice = Invoice()
|
||||
line = invoice.lines.create()
|
||||
line.quantity = 3
|
||||
line = invoice.lines.create()
|
||||
line.quantity = 5
|
||||
|
||||
assert len(invoice.lines) == 2
|
||||
assert '<Invoice count="2"><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml()
|
||||
|
||||
def test_model_one2many_as_list():
|
||||
class Line(facho.model.Model):
|
||||
__name__ = 'Line'
|
||||
|
||||
quantity = fields.Attribute('quantity')
|
||||
|
||||
class Invoice(facho.model.Model):
|
||||
__name__ = 'Invoice'
|
||||
|
||||
lines = fields.One2Many(Line)
|
||||
|
||||
invoice = Invoice()
|
||||
line = invoice.lines.create()
|
||||
line.quantity = 3
|
||||
line = invoice.lines.create()
|
||||
line.quantity = 5
|
||||
|
||||
lines = list(invoice.lines)
|
||||
assert len(list(invoice.lines)) == 2
|
||||
|
||||
for line in lines:
|
||||
assert isinstance(line, Line)
|
||||
assert '<Invoice><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml()
|
||||
|
||||
|
||||
def test_model_attributes_order():
|
||||
class Line(facho.model.Model):
|
||||
__name__ = 'Line'
|
||||
|
||||
quantity = fields.Attribute('quantity')
|
||||
|
||||
class Invoice(facho.model.Model):
|
||||
__name__ = 'Invoice'
|
||||
|
||||
line1 = fields.Many2One(Line, name='Line1')
|
||||
line2 = fields.Many2One(Line, name='Line2')
|
||||
line3 = fields.Many2One(Line, name='Line3')
|
||||
|
||||
|
||||
invoice = Invoice()
|
||||
invoice.line2.quantity = 2
|
||||
invoice.line3.quantity = 3
|
||||
invoice.line1.quantity = 1
|
||||
|
||||
assert '<Invoice><Line1 quantity="1"/><Line2 quantity="2"/><Line3 quantity="3"/></Invoice>' == invoice.to_xml()
|
||||
|
||||
|
||||
def test_field_amount():
|
||||
class Line(facho.model.Model):
|
||||
__name__ = 'Line'
|
||||
|
||||
amount = fields.Amount(name='Amount', precision=1)
|
||||
amount_as_attribute = fields.Attribute('amount')
|
||||
|
||||
@fields.on_change(['amount'])
|
||||
def on_amount(self, name, value):
|
||||
self.amount_as_attribute = self.amount
|
||||
|
||||
line = Line()
|
||||
line.amount = 33
|
||||
|
||||
assert '<Line amount="33.0"/>' == line.to_xml()
|
||||
|
||||
|
||||
def test_model_setup():
|
||||
class Line(facho.model.Model):
|
||||
__name__ = 'Line'
|
||||
|
||||
amount = fields.Attribute(name='amount')
|
||||
|
||||
def __setup__(self):
|
||||
self.amount = 23
|
||||
|
||||
line = Line()
|
||||
assert '<Line amount="23"/>' == line.to_xml()
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of facho. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
"""Nuevo esquema para modelar segun decreto"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from lxml import etree
|
||||
import facho.fe.model as model
|
||||
import facho.fe.form as form
|
||||
from facho import fe
|
||||
import helpers
|
||||
|
||||
def simple_invoice():
|
||||
invoice = model.Invoice()
|
||||
invoice.dian.software_security_code = '12345'
|
||||
invoice.dian.software_provider.provider_id = 'provider-id'
|
||||
invoice.dian.software_provider.software_id = 'facho'
|
||||
invoice.dian.control.prefix = 'SETP'
|
||||
invoice.dian.control.from_range = '1000'
|
||||
invoice.dian.control.to_range = '1000'
|
||||
invoice.id = '323200000129'
|
||||
invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
|
||||
invoice.supplier.party.id = '700085371'
|
||||
invoice.customer.party.id = '800199436'
|
||||
|
||||
line = invoice.lines.create()
|
||||
line.add_tax(model.Taxes.Iva(19.0))
|
||||
|
||||
# TODO(bit4bit) acoplamiento temporal
|
||||
# se debe crear primero el subotatl
|
||||
# para poder calcularse al cambiar el precio
|
||||
line.quantity = 1
|
||||
line.price = 1_500_000
|
||||
|
||||
return invoice
|
||||
|
||||
def test_simple_invoice_cufe():
|
||||
token = '693ff6f2a553c3646a063436fd4dd9ded0311471'
|
||||
environment = fe.AMBIENTE_PRODUCCION
|
||||
invoice = simple_invoice()
|
||||
assert invoice.cufe(token, environment) == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4'
|
||||
|
||||
def test_simple_invoice_sign_dian(monkeypatch):
|
||||
invoice = simple_invoice()
|
||||
|
||||
xmlstring = invoice.to_xml()
|
||||
p12_data = open('./tests/example.p12', 'rb').read()
|
||||
signer = fe.DianXMLExtensionSigner.from_bytes(p12_data)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
helpers.mock_urlopen(m)
|
||||
xmlsigned = signer.sign_xml_string(xmlstring)
|
||||
assert "Signature" in xmlsigned
|
||||
|
||||
|
||||
def test_dian_extension_authorization_provider():
|
||||
invoice = simple_invoice()
|
||||
xml = fe.FeXML.from_string(invoice.to_xml())
|
||||
provider_id = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
|
||||
|
||||
assert provider_id.attrib['schemeID'] == '4'
|
||||
assert provider_id.attrib['schemeName'] == '31'
|
||||
assert provider_id.attrib['schemeAgencyName'] == 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)'
|
||||
assert provider_id.attrib['schemeAgencyID'] == '195'
|
||||
assert provider_id.text == '800197268'
|
||||
|
||||
def test_invoicesimple_xml_signed_using_fexml(monkeypatch):
|
||||
invoice = simple_invoice()
|
||||
|
||||
xml = fe.FeXML.from_string(invoice.to_xml())
|
||||
|
||||
signer = fe.DianXMLExtensionSigner('./tests/example.p12')
|
||||
|
||||
print(xml.tostring())
|
||||
with monkeypatch.context() as m:
|
||||
import helpers
|
||||
helpers.mock_urlopen(m)
|
||||
xml.add_extension(signer)
|
||||
|
||||
elem = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature')
|
||||
assert elem.text is not None
|
||||
|
||||
def test_invoice_supplier_party():
|
||||
invoice = simple_invoice()
|
||||
invoice.supplier.party.name = 'superfacho'
|
||||
invoice.supplier.party.tax_scheme.registration_name = 'legal-superfacho'
|
||||
invoice.supplier.party.contact.email = 'superfacho@etrivial.net'
|
||||
|
||||
xml = fe.FeXML.from_string(invoice.to_xml())
|
||||
|
||||
name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name')
|
||||
assert name.text == 'superfacho'
|
||||
|
||||
name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName')
|
||||
assert name.text == 'legal-superfacho'
|
||||
|
||||
name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicEmail')
|
||||
assert name.text == 'superfacho@etrivial.net'
|
||||
|
||||
def test_invoice_customer_party():
|
||||
invoice = simple_invoice()
|
||||
invoice.customer.party.name = 'superfacho-customer'
|
||||
invoice.customer.party.tax_scheme.registration_name = 'legal-superfacho-customer'
|
||||
invoice.customer.party.contact.email = 'superfacho@etrivial.net'
|
||||
|
||||
xml = fe.FeXML.from_string(invoice.to_xml())
|
||||
|
||||
name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name')
|
||||
assert name.text == 'superfacho-customer'
|
||||
|
||||
name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName')
|
||||
assert name.text == 'legal-superfacho-customer'
|
||||
|
||||
name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicEmail')
|
||||
assert name.text == 'superfacho@etrivial.net'
|
||||
@@ -28,8 +28,12 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30'
|
||||
# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00'
|
||||
# assert
|
||||
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico',
|
||||
# 'DiasTrabajados') == '30'
|
||||
# assert
|
||||
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico',
|
||||
# 'SueldoTrabajado') == '1000000.00'
|
||||
|
||||
# def test_adicionar_devengado_transporte():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -40,7 +44,9 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
|
||||
# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte', 'AuxilioTransporte') == '2000000.0'
|
||||
# assert
|
||||
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte',
|
||||
# 'AuxilioTransporte') == '2000000.0'
|
||||
|
||||
# def test_adicionar_devengado_comprobante_total():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -58,7 +64,9 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
|
||||
# assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '1000000.00'
|
||||
# assert
|
||||
# xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') ==
|
||||
# '1000000.00'
|
||||
|
||||
# def test_adicionar_devengado_comprobante_total_cero():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -75,7 +83,8 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
|
||||
# assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00'
|
||||
# assert
|
||||
# xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00'
|
||||
|
||||
# def test_adicionar_devengado_transporte_muchos():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -90,7 +99,8 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# print(xml)
|
||||
# assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal') == '5000000.00'
|
||||
# assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal')
|
||||
# == '5000000.00'
|
||||
|
||||
# def test_adicionar_deduccion_salud():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -107,7 +117,9 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# print(xml)
|
||||
# assert xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') == '1000.00'
|
||||
# assert
|
||||
# xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') ==
|
||||
# '1000.00'
|
||||
|
||||
# def test_nomina_obligatorios_segun_anexo_tecnico():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -213,26 +225,62 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab'
|
||||
# assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune
|
||||
# assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') == f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_cune}"
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT') == '700085371'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True'
|
||||
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov') == 'N0111'
|
||||
# expected_cune =
|
||||
# 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484
|
||||
# b5da15ff585e773b0b0ab'
|
||||
# assert
|
||||
# xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral',
|
||||
# 'CUNE') == expected_cune
|
||||
# assert
|
||||
# xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral',
|
||||
# 'TipoXML') == '102'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaX
|
||||
# ML/@Numero') == 'N00001'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaX
|
||||
# ML/@Consecutivo') == '00001'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX
|
||||
# ML/@Pais') == 'CO'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX
|
||||
# ML/@DepartamentoEstado') == '05'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX
|
||||
# ML/@MunicipioCiudad') == '05001'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NI
|
||||
# T') == '999999'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV
|
||||
# ') == '2'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@So
|
||||
# ftwareID') == 'xx'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@So
|
||||
# ftwareSC') is not None
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') ==
|
||||
# f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_
|
||||
# cune}"
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT')
|
||||
# == '700085371'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@Nume
|
||||
# roDocumento') == '800199436'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') ==
|
||||
# 'True'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov
|
||||
# ') == 'N0111'
|
||||
|
||||
# # confirmar el namespace
|
||||
# assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in xml.tostring()
|
||||
# assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in
|
||||
# xml.tostring()
|
||||
|
||||
# def test_asignar_pago():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
@@ -249,11 +297,11 @@
|
||||
# xml.add_extension(signer)
|
||||
|
||||
# print(xml.tostring())
|
||||
# elem = xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature')
|
||||
# elem =
|
||||
# xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension
|
||||
# /ext:ExtensionContent/ds:Signature')
|
||||
# assert elem is not None
|
||||
|
||||
|
||||
|
||||
# def test_nomina_devengado_horas_extras_nocturnas():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
@@ -277,7 +325,9 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN', multiple=True)
|
||||
# extras =
|
||||
# xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN',
|
||||
# multiple=True)
|
||||
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
|
||||
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
|
||||
# assert extras[0].get('Cantidad') == '1'
|
||||
@@ -312,7 +362,9 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN', multiple=True)
|
||||
# extras =
|
||||
# xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN',
|
||||
# multiple=True)
|
||||
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
|
||||
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
|
||||
# assert extras[0].get('Cantidad') == '1'
|
||||
@@ -327,7 +379,9 @@
|
||||
# def test_nomina_devengado_horas_extras_diarias_dominicales_y_festivos():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiariasDominicalesYFestivos(
|
||||
#
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiariasDominicalesY
|
||||
# Festivos(
|
||||
# horas_extras=[
|
||||
# fe.nomina.DevengadoHoraExtra(
|
||||
# hora_inicio='2021-11-30T19:09:55',
|
||||
@@ -347,7 +401,9 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF', multiple=True)
|
||||
# extras =
|
||||
# xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF',
|
||||
# multiple=True)
|
||||
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
|
||||
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
|
||||
# assert extras[0].get('Cantidad') == '1'
|
||||
@@ -362,7 +418,9 @@
|
||||
# def test_nomina_devengado_horas_recargo_diarias_dominicales_y_festivos():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoDiariasDominicalesYFestivos(
|
||||
#
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoDiariasDominicales
|
||||
# YFestivos(
|
||||
# horas_extras=[
|
||||
# fe.nomina.DevengadoHoraExtra(
|
||||
# hora_inicio='2021-11-30T19:09:55',
|
||||
@@ -382,7 +440,9 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF', multiple=True)
|
||||
# extras =
|
||||
# xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF',
|
||||
# multiple=True)
|
||||
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
|
||||
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
|
||||
# assert extras[0].get('Cantidad') == '1'
|
||||
@@ -398,7 +458,9 @@
|
||||
# def test_nomina_devengado_horas_extras_nocturnas_dominicales_y_festivos():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnasDominicalesYFestivos(
|
||||
#
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnasDominicale
|
||||
# sYFestivos(
|
||||
# horas_extras=[
|
||||
# fe.nomina.DevengadoHoraExtra(
|
||||
# hora_inicio='2021-11-30T19:09:55',
|
||||
@@ -418,7 +480,9 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF', multiple=True)
|
||||
# extras =
|
||||
# xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF',
|
||||
# multiple=True)
|
||||
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
|
||||
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
|
||||
# assert extras[0].get('Cantidad') == '1'
|
||||
@@ -433,7 +497,9 @@
|
||||
# def test_nomina_devengado_horas_recargo_nocturno_dominicales_y_festivos():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturnoDominicalesYFestivos(
|
||||
#
|
||||
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturnoDominicale
|
||||
# sYFestivos(
|
||||
# horas_extras=[
|
||||
# fe.nomina.DevengadoHoraExtra(
|
||||
# hora_inicio='2021-11-30T19:09:55',
|
||||
@@ -453,7 +519,9 @@
|
||||
# ))
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF', multiple=True)
|
||||
# extras =
|
||||
# xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF',
|
||||
# multiple=True)
|
||||
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
|
||||
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
|
||||
# assert extras[0].get('Cantidad') == '1'
|
||||
|
||||
@@ -101,7 +101,9 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
|
||||
# assert xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/InformacionGeneral', 'TipoXML') == '103'
|
||||
# assert
|
||||
# xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar
|
||||
# /InformacionGeneral', 'TipoXML') == '103'
|
||||
|
||||
|
||||
# def test_adicionar_reemplazar_devengado_comprobante_total():
|
||||
@@ -119,13 +121,17 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
|
||||
# assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ComprobanteTotal') == '1000000.00'
|
||||
# assert
|
||||
# xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Comp
|
||||
# robanteTotal') == '1000000.00'
|
||||
|
||||
|
||||
# def test_adicionar_reemplazar_asignar_predecesor():
|
||||
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
|
||||
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor(
|
||||
#
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.
|
||||
# Predecesor(
|
||||
# numero = '123456',
|
||||
# cune = 'ABC123456',
|
||||
# fecha_generacion = '2021-11-16'
|
||||
@@ -133,15 +139,23 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# print(xml.tostring())
|
||||
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@NumeroPred') == '123456'
|
||||
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456'
|
||||
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re
|
||||
# emplazar/ReemplazandoPredecesor/@NumeroPred') == '123456'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re
|
||||
# emplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re
|
||||
# emplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16'
|
||||
|
||||
|
||||
# def test_adicionar_reemplazar_eliminar_predecesor_opcional():
|
||||
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
|
||||
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor(
|
||||
#
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.
|
||||
# Predecesor(
|
||||
# numero = '123456',
|
||||
# cune = 'ABC123456',
|
||||
# fecha_generacion = '2021-11-16'
|
||||
@@ -150,13 +164,19 @@
|
||||
# xml = nomina.toFachoXML()
|
||||
# print(xml.tostring())
|
||||
|
||||
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None
|
||||
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is None
|
||||
# assert
|
||||
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Reemplaza
|
||||
# ndoPredecesor') is not None
|
||||
# assert
|
||||
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoP
|
||||
# redecesor') is None
|
||||
|
||||
# def test_adicionar_eliminar_reemplazar_predecesor_opcional():
|
||||
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
|
||||
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor(
|
||||
#
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Pr
|
||||
# edecesor(
|
||||
# numero = '123456',
|
||||
# cune = 'ABC123456',
|
||||
# fecha_generacion = '2021-11-16'
|
||||
@@ -164,8 +184,12 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# print(xml.tostring())
|
||||
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None
|
||||
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is None
|
||||
# assert
|
||||
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoP
|
||||
# redecesor') is not None
|
||||
# assert
|
||||
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Reemplaza
|
||||
# ndoPredecesor') is None
|
||||
|
||||
# def test_adicionar_eliminar_devengado_comprobante_total():
|
||||
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
|
||||
@@ -182,12 +206,16 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
|
||||
# assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/ComprobanteTotal') == '1000000.00'
|
||||
# assert
|
||||
# xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/Compro
|
||||
# banteTotal') == '1000000.00'
|
||||
|
||||
# def test_adicionar_eliminar_asignar_predecesor():
|
||||
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
|
||||
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor(
|
||||
#
|
||||
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Pr
|
||||
# edecesor(
|
||||
# numero = '123456',
|
||||
# cune = 'ABC123456',
|
||||
# fecha_generacion = '2021-11-16'
|
||||
@@ -195,9 +223,15 @@
|
||||
|
||||
# xml = nomina.toFachoXML()
|
||||
# print(xml.tostring())
|
||||
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@NumeroPred') == '123456'
|
||||
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456'
|
||||
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El
|
||||
# iminar/EliminandoPredecesor/@NumeroPred') == '123456'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El
|
||||
# iminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456'
|
||||
# assert
|
||||
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El
|
||||
# iminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16'
|
||||
|
||||
# def test_nomina_devengado_horas_extras_diarias():
|
||||
# nomina = fe.nomina.DIANNominaIndividual()
|
||||
|
||||
22
tox.ini
22
tox.ini
@@ -1,26 +1,18 @@
|
||||
[tox]
|
||||
envlist = py39, py310, py311, py312, flake8
|
||||
envlist = py310, py311, py312, py313, flake8
|
||||
|
||||
[travis]
|
||||
python =
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
3.12: py312
|
||||
[flake8]
|
||||
exclude = docs, build, venv, .tox, .git, .pytest_cache, *.egg-info
|
||||
|
||||
[testenv:flake8]
|
||||
basepython = python
|
||||
deps = flake8
|
||||
commands = flake8 facho
|
||||
basepython = python3.13
|
||||
deps = flake8>=7.0.0
|
||||
commands = flake8 facho tests examples
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}
|
||||
deps =
|
||||
-r{toxinidir}/requirements_dev.txt
|
||||
; If you want to make tox run the tests with the same versions, create a
|
||||
; requirements.txt with the pinned versions and uncomment the following line:
|
||||
; -r{toxinidir}/requirements.txt
|
||||
deps = pytest>=9.1.1,<10.0.0
|
||||
commands =
|
||||
pip install -U pip
|
||||
py.test --basetemp={envtmpdir}
|
||||
|
||||
Reference in New Issue
Block a user