50 Commits

Author SHA1 Message Date
4eaf0c052f Merge branch 'morfo' 2025-12-04 15:48:00 -05:00
c49e67b8a6 Fix: Test incluir ApplicationResponse 2025-01-01 18:40:05 -05:00
7672ae4b7f Fix: Delete escribir archivo para ver salida 2025-01-01 18:39:22 -05:00
25c5fda3f0 Fix: Delete archivo para ver salida 2025-01-01 18:37:09 -05:00
a758b8678b Feat: Se agrega ApplicationResponse a AttachedDocument 2025-01-01 16:56:15 -05:00
3a385c63e3 Feat: Attachment, ParentDocumentLine 2024-12-30 19:00:51 -05:00
9b33b4486c Fix: Se elimina simple_invoice arg 2024-12-30 11:01:22 -05:00
612aae1f86 Fix: Se reemplazan datos de prueba por objeto Invoice issue #4 2024-12-27 21:04:35 -05:00
4fe82daac6 AttachedDocument(WIP): Se escribe envoltorio AttachedDocument issue #4 2024-12-27 18:54:56 -05:00
e237d1b45f Fix: Se descomenta test AttachedDocument 2024-12-27 00:10:03 -05:00
8cc6146be2 Merge remote-tracking branch 'origin/documento_soporte' 2024-12-26 23:20:16 -05:00
cosmos
c919d8e36c Add timestamp 2024-08-22 09:41:37 -05:00
bit4bit
48d1ba3735 Create new branch named "morfo"
FossilOrigin-Name: 49206404a73c744c29382b1a54ea35227b248c483f44dd09a77d7a7fbdefa691
2021-10-06 02:10:01 +00:00
bit4bit
2e130d39e6 se adicionan mas campos a Invoice.Party
FossilOrigin-Name: 033c6a3c0297c83c1d0bf2bb246f55fa7ce0d8aaa752f2ae3ec9515c763d61bf
2021-10-06 01:08:01 +00:00
bit4bit
6716efd121 se actualiza a 0.2.1
FossilOrigin-Name: 65ab09bc6e7823c31d3b201825edc868c46e92ecca963a2c6a0bb9aec9957661
2021-08-09 01:47:30 +00:00
bit4bit
302812328e se adicionan comentarios
FossilOrigin-Name: 7137f93d5b3081e2d8f3d1245c97f4aa109c2c671744eef5a4abc0f7551183d0
2021-08-09 01:16:44 +00:00
bit4bit
3a89c6d3e5 se adicionan comentarios
FossilOrigin-Name: 13a8728e99e2a715d68bee47a54adcfef5a1f0043abfdfe6aabb3afa99ca9f7f
2021-08-09 01:15:51 +00:00
bit4bit
3a349a746e se crea parcialmente Address, Country
FossilOrigin-Name: 9db11d9b88e2e0065960ac07efe88e8dd8d87706bda88074845f6bfcfecc737b
2021-08-09 00:29:37 +00:00
bit4bit
efe93ecc3c mas pruebas y algunos pequenos cambios
FossilOrigin-Name: 02bc90719bb17216a749568086887a7d27878fdbd81febfb88a9fb1e68aa8205
2021-08-09 00:21:03 +00:00
bit4bit
64b312a432 se adiciona extensions para la dian
FossilOrigin-Name: f5521ddbfb903915de88a26ba5197b67efa1ebfd66337061ee9e3653c59dd217
2021-08-08 22:00:08 +00:00
bit4bit
088fa9e6e0 se adiciona namespaces a invoice
FossilOrigin-Name: caf85d4a30e7945ca2a6fc3a9430855fa8623442b1a30ae824f742e2a93e2956
2021-08-08 21:20:54 +00:00
bit4bit
ddee0e45c1 el uso de __default_set__ y __default_get__ facilitan la creacion de un objeto como tipo
FossilOrigin-Name: e7fa7e3a3b312ad6b88a85b508b496308e5bb54b1d51ab5d50f4d4207830f175
2021-07-31 18:31:38 +00:00
bit4bit
69a74c0714 generacion de cufe desde invoice
FossilOrigin-Name: d3494f20063452571b1e86d505f211e61fdf435aa43b870408136e3e9302bc17
2021-07-31 17:09:42 +00:00
bit4bit
a1a9746353 se adiciona __setup__ para inicializar modelos
FossilOrigin-Name: 504ad84bee1c708c2b55fdde3552d39980bf1efad3a51de8050018e4d1f387f3
2021-07-09 01:03:51 +00:00
bit4bit
b3e4a088b7 se adicion campo fields.Amount
FossilOrigin-Name: b23b2c243daaf0788cf47736015d75eecae9f4eb55cd4d31b9e063d7fa9a0691
2021-07-03 21:50:56 +00:00
bit4bit
507ddbe558 se crea legal monetary total
FossilOrigin-Name: 8ae3dfadfe9b90b8cc5ad59d2a73f4c8a987c5aac498573e56754a2d32e9e2ae
2021-06-27 02:16:16 +00:00
bit4bit
2e8aa35b29 prueba que confirma el orden del model y xml
FossilOrigin-Name: fc039dec57eec4287d1c2352f8fd0cc6fceaf1f409af02e115c2d99a96bdc7bd
2021-06-26 23:25:51 +00:00
bit4bit
ba908b938c se refleja el orden de los atributos del modelo en el xml
FossilOrigin-Name: 5ff5ebc397a11977916b7008ab4d5104a375290a5c5d0356098b69242378f1f8
2021-06-26 23:23:25 +00:00
bit4bit
4f15926656 se adiciona mas modelos a nuevo esquema
FossilOrigin-Name: acac57e60f808abdd89937be338d819f4f6fa9f8b4dda725569f445f96c982d3
2021-06-26 22:05:30 +00:00
bit4bit
1d6d1e2601 One2Many se puede iterar como lista
FossilOrigin-Name: 1de9daae362feb6693b928c4a58c11423127eb1783fdf509f0ef3083b5563b24
2021-06-26 21:32:24 +00:00
bit4bit
47a0dd33e2 se adiciona notificaciones de cambios para fields.One2Many
FossilOrigin-Name: b422aa912c7c7873edcbbecf1914e9ff21a24cab3fc7e2a788e00efc16fe2f51
2021-06-26 21:28:02 +00:00
bit4bit
53b5207e35 fields.on_changes no requiere un nombre especifico para su ejecucion
FossilOrigin-Name: 55d11605df9d1228737da18bf04b242fff3b08939021488f169ad2b042330d6f
2021-06-26 20:25:07 +00:00
bit4bit
bd25bef21f se continua con el nuevo modelado de facturacion
FossilOrigin-Name: 68ecac65b7ee5c2884161943e120df58ad596ffd3be82c3ce107ecf00eae6afa
2021-06-26 00:11:57 +00:00
bit4bit
5f5a6182c9 se adiciona fields.One2Many
FossilOrigin-Name: 94c1cca50451a46c417d925b27fdd53d8199b8dc58783e600c84179eac666a36
2021-06-25 23:55:36 +00:00
bit4bit
ab462a6ca5 se retiran archivos no usados
FossilOrigin-Name: 05431311b7cfe21cbde218f35b5adaef4171a78ed19c87b9e8bc8119ea091e45
2021-06-25 23:23:07 +00:00
bit4bit
c694603505 se adiciona @fields.on_change para ejecutar funcion cuando se cambian los varoles de algun attributo
FossilOrigin-Name: bee19b201f8c1a6b972c2a9abfe5fb57a558a67be6ecddce4f7f07b5b6980215
2021-06-25 23:21:04 +00:00
bit4bit
b6219bd171 fields.Function manejan default
FossilOrigin-Name: ec954ac9253429b99095fbcce443a2691f516f603282aec1ee59c4b7cbbd6c4a
2021-06-25 01:56:49 +00:00
bit4bit
a9dde83e81 se adiciona atributo default a many2one y attribute
FossilOrigin-Name: 9ddb1d1b8bebef24da17cc47d8fc70392f6015bb61866f251992aea518ed3d0f
2021-06-25 01:39:02 +00:00
bit4bit
3eacb29afa se separa responsabilidad de fields.Function
FossilOrigin-Name: 4d5daa47a75a0e283e86bf992126bf60f3a8a14287e9acc437d5f2f3eca43150
2021-06-24 23:38:28 +00:00
bit4bit
f630a544c2 se inicia modelo de facturacion usando nuevo esquema
FossilOrigin-Name: 8e6c23e7baa837c64b81baaed342b07eaab7ab631302cd2a8fa86f4989227d07
2021-06-24 02:11:33 +00:00
bit4bit
ba4e3d546f se depreca fields.Model port fields.Many2One
FossilOrigin-Name: 73d74488ca7458ff7dc84898ff76fa9b16b427cc6bc77540d7c81450e4f33869
2021-06-24 02:10:46 +00:00
bit4bit
92bae58e51 se instancia modelo en caso de no existir para Many2One
FossilOrigin-Name: 006f6a780ae0436649addd2abe89eb6a9bfc5ad573ee1a1835a8f65ab039fd26
2021-06-24 01:51:05 +00:00
bit4bit
58e7387292 fields.Model se adiciona __default_set__ para remplaza comportamiento de asignacion directa
FossilOrigin-Name: 436c5483cf534c8d457fb403302e511e7aad4b220d66569612f7ceb2da8d8cf8
2021-06-24 01:37:05 +00:00
bit4bit
a015a9361b fields.Function no requiere getter
FossilOrigin-Name: 47f9b9427ef55c688678001361260e5d00ea53d82977ea13e3414ed04878fb36
2021-06-24 01:28:07 +00:00
bit4bit
0216d0141a se adiciona archivo faltante
FossilOrigin-Name: 9b0c4d69d898ddbcd9279b3055e72df525feaefb0d46700dc6c019acdba01e80
2021-06-24 01:23:51 +00:00
bit4bit
6cc4610b45 fields.Model se permite cambiar el nombre de la etiqueta
FossilOrigin-Name: 896b797629e426a5e366d5be76fc00c3cc272299d6749e40f8317893b1545a9e
2021-06-24 01:18:27 +00:00
bit4bit
49feee8809 se adiciona mas pruebas
FossilOrigin-Name: 68e716388ee3328b1b451997eca99dc1f20b47db4ebe3dfc761daec6fec3c8d6
2021-06-24 01:15:16 +00:00
bit4bit
d78a429711 se adiciona field.Function
FossilOrigin-Name: 45a288bc30ad9b25fed59cd01c89bd2f7632926083384a7853c3b753a4d7f95b
2021-06-24 01:04:49 +00:00
bit4bit
84996066fa se implementa un esquema para modelar el xml
FossilOrigin-Name: e4de658f60fe8fcbb330923e14958a5d8f8e0e6395db4f992ec7da45062fa193
2021-06-23 23:04:00 +00:00
bit4bit
7d060e1786 Create new branch named "model"
FossilOrigin-Name: 55404ca978d7476847f2b6de1d9e8d78bd2ad67200d63695501e1a3a22265eb5
2021-06-23 13:55:41 +00:00
80 changed files with 3760 additions and 3908 deletions

View File

@@ -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 $ python3 -mvenv facho-venv && source facho-venv/bin/activate
$ cd facho/ $ cd facho/
$ pre-commit install $ pre-commit install
$ pip install -e . $ python setup.py develop
4. Create a branch for local development:: 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:: tests, including testing other Python versions with tox::
$ flake8 facho tests $ flake8 facho tests
$ py.test $ python setup.py test or py.test
$ tox $ tox
To get flake8 and tox, just pip install them into your virtualenv. 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 Using docker
------------ ------------
1. make -f Makefile.dev dev-setup 1. make -f Makefile.dev build
2. make -f Makefile.dev dev-shell 2. make -f Makefile.dev dev-shell
3. make -f Makefile.dev test 3. make -f Makefile.dev python3.8 setup.py develop
4. make -f Makefile.dev tox 4. make -f Makefile.dev python3.8 setup.py test
Pull Request Guidelines Pull Request Guidelines
----------------------- -----------------------

View File

@@ -7,45 +7,34 @@ RUN apt install software-properties-common -y \
&& add-apt-repository ppa:deadsnakes/ppa && add-apt-repository ppa:deadsnakes/ppa
RUN apt-get install -y --no-install-recommends \ 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.10 python3.10-distutils python3.10-dev \
python3.11 python3.11-distutils python3.11-dev \ python3.11 python3.11-distutils python3.11-dev \
python3.12 python3-setuptools python3.12-dev \ python3.12 python3-setuptools python3.12-dev \
python3.13 python3-setuptools python3.13-dev \
wget \ wget \
ca-certificates ca-certificates
RUN wget https://bootstrap.pypa.io/get-pip.py \ 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.10 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.11 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.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 && rm get-pip.py
RUN apt-get install -y --no-install-recommends \ RUN apt-get install -y --no-install-recommends \
gcc \
build-essential \
pkg-config \
libxml2-dev \ libxml2-dev \
libxmlsec1-dev \ libxmlsec1-dev \
xmlsec1 \ build-essential \
libxml2-dev \
libxslt-dev \
zip zip
RUN python3.9 --version
RUN python3.10 --version RUN python3.10 --version
RUN python3.11 --version RUN python3.11 --version
RUN python3.12 --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.10 install setuptools setuptools-rust
RUN pip3.11 install setuptools setuptools-rust --break-system-packages RUN pip3.11 install setuptools setuptools-rust --break-system-packages
RUN pip3.12 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 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

View File

@@ -3,4 +3,4 @@ History
======= =======
* First release on PyPI. * 0.2.1 version usada en produccion.

View File

@@ -80,8 +80,9 @@ release: dist ## package and upload a release
twine upload dist/* twine upload dist/*
dist: clean ## builds source and wheel package dist: clean ## builds source and wheel package
python -m build python setup.py sdist
python setup.py bdist_wheel
ls -l dist ls -l dist
install: clean ## install the package to the active Python's site-packages install: clean ## install the package to the active Python's site-packages
pip install . python setup.py install

View File

@@ -15,7 +15,7 @@ dev-shell:
docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash
test: test:
docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.13 -m pytest -vv' docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.12 setup.py test'
tox: tox:
docker run -it -v $(PWD)/:/app -w /app facho tox docker run -it -v $(PWD)/:/app -w /app facho tox

View File

@@ -18,11 +18,11 @@
# relative to the documentation root, use os.path.abspath to make it # relative to the documentation root, use os.path.abspath to make it
# absolute, like shown here. # absolute, like shown here.
# #
import facho
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
import facho
# -- General configuration --------------------------------------------- # -- General configuration ---------------------------------------------

View File

@@ -44,7 +44,7 @@ Once you have a copy of the source, you can install it with:
.. code-block:: console .. code-block:: console
$ pip install . $ python setup.py install
.. _Github repo: https://github.com/bit4bit/facho .. _Github repo: https://github.com/bit4bit/facho

View File

@@ -11,18 +11,13 @@ from facho.fe import form_xml
from facho.fe import fe from facho.fe import fe
# importar otras necesarias # importar otras necesarias
from datetime import datetime from datetime import datetime, date
# Datos del fomulario del SET de pruebas # Datos del fomulario del SET de pruebas
# Número suministrado por la Dian en el momento de la creación del SET de INVOICE_AUTHORIZATION = '181360000001' # Número suministrado por la Dian en el momento de la creación del SET de Pruebas
# Pruebas ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas
INVOICE_AUTHORIZATION = '181360000001' PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas
# Id suministrado por la Dian en el momento de la creación del SET de Pruebas CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##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 # callback que retonar las extensiones XML necesarias
@@ -31,24 +26,15 @@ CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d'
# muchos de los valores usados son obtenidos # muchos de los valores usados son obtenidos
# del servicio web de la DIAN. # del servicio web de la DIAN.
def extensions(inv): def extensions(inv):
security_code = fe.DianXMLExtensionSoftwareSecurityCode( security_code = fe.DianXMLExtensionSoftwareSecurityCode(ID_SOFTWARE, PIN, inv.invoice_ident)
ID_SOFTWARE, PIN, inv.invoice_ident)
authorization_provider = fe.DianXMLExtensionAuthorizationProvider() authorization_provider = fe.DianXMLExtensionAuthorizationProvider()
cufe = fe.DianXMLExtensionCUFE(inv, CLAVE_TECNICA, fe.AMBIENTE_PRUEBAS) cufe = fe.DianXMLExtensionCUFE(inv, CLAVE_TECNICA, fe.AMBIENTE_PRUEBAS)
software_provider = fe.DianXMLExtensionSoftwareProvider( software_provider = fe.DianXMLExtensionSoftwareProvider('nit_empresa', 'dígito_verificación', ID_SOFTWARE)
'nit_empresa', 'dígito_verificación', ID_SOFTWARE) inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(INVOICE_AUTHORIZATION,
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization( datetime(2019, 1, 19),#Datos toamdos de
INVOICE_AUTHORIZATION, datetime(2030, 1, 19),#la configuración
# Datos tomados de la configuración
datetime(2019, 1, 19),
datetime(2030, 1, 19),
'SETP', 990000000, 995000000)#del SET de pruebas 'SETP', 990000000, 995000000)#del SET de pruebas
return [ return [security_code, authorization_provider, cufe, software_provider, inv_authorization]
security_code,
authorization_provider,
cufe,
software_provider,
inv_authorization]
def invoice(): def invoice():

View File

@@ -1,11 +1,9 @@
from facho import fe from facho import fe
def extensions(nomina): def extensions(nomina):
return [] return []
def nomina(): def nomina():
nomina = fe.nomina.DIANNominaIndividual() nomina = fe.nomina.DIANNominaIndividual()

View File

@@ -1,37 +1,39 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
import base64
import warnings import warnings
import click import click
import logging.config import logging.config
logging.config.dictConfig( logging.config.dictConfig({
{ 'version': 1,
"version": 1, 'formatters': {
"formatters": {"verbose": {"format": "%(name)s: %(message)s"}}, 'verbose': {
"handlers": { 'format': '%(name)s: %(message)s'
"console": { }
"level": "DEBUG", },
"class": "logging.StreamHandler", 'handlers': {
"formatter": "verbose", 'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
}, },
}, },
"loggers": { 'loggers': {
"zeep.transports": { 'zeep.transports': {
"level": "DEBUG", 'level': 'DEBUG',
"propagate": True, 'propagate': True,
"handlers": ["console"], 'handlers': ['console'],
},
}, },
} }
) })
def disable_ssl(): def disable_ssl():
# MACHETE # MACHETE
import ssl 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 ssl._create_default_https_context = ssl._create_unverified_context
warnings.warn("be sure!! ssl disable") warnings.warn("be sure!! ssl disable")
else: else:
@@ -39,122 +41,99 @@ def disable_ssl():
@click.command() @click.command()
@click.option("--nit", required=True) @click.option('--nit', required=True)
@click.option("--nit-proveedor", required=True) @click.option('--nit-proveedor', required=True)
@click.option("--id-software", required=True) @click.option('--id-software', required=True)
@click.option("--username", required=True) @click.option('--username', required=True)
@click.option("--password", required=True) @click.option('--password', required=True)
def consultaResolucionesFacturacion( def consultaResolucionesFacturacion(nit, nit_proveedor, id_software, username, password):
nit, nit_proveedor, id_software, username, password
):
from facho.fe.client import dian from facho.fe.client import dian
client_dian = dian.DianClient(username,
client_dian = dian.DianClient(username, password) password)
resp = client_dian.request( resp = client_dian.request(dian.ConsultaResolucionesFacturacionPeticion(
dian.ConsultaResolucionesFacturacionPeticion(
nit, nit_proveedor, id_software nit, nit_proveedor, id_software
) ))
)
print(str(resp)) print(str(resp))
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.option("--test-setid", required=True) @click.option('--test-setid', required=True)
@click.argument("filename", required=True) @click.argument('filename', required=True)
@click.argument("zipfile", type=click.Path(exists=True)) @click.argument('zipfile', type=click.Path(exists=True))
def soap_send_test_set_async( def soap_send_test_set_async(private_key, public_key, habilitacion, password, test_setid, filename, zipfile):
private_key,
public_key,
habilitacion,
password,
test_setid,
filename,
zipfile,
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.SendTestSetAsync req = dian.SendTestSetAsync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendTestSetAsync req = dian.Habilitacion.SendTestSetAsync
resp = client.request( resp = client.request(req(
req(
filename, filename,
open(zipfile, "rb").read(), open(zipfile, 'rb').read(),
test_setid, test_setid,
) ))
)
print(resp) print(resp)
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.argument("filename", required=True) @click.argument('filename', required=True)
@click.argument("zipfile", type=click.Path(exists=True)) @click.argument('zipfile', type=click.Path(exists=True))
def soap_send_bill_async( def soap_send_bill_async(private_key, public_key, habilitacion, password, filename, zipfile):
private_key, public_key, habilitacion, password, filename, zipfile
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.SendBillAsync req = dian.SendBillAsync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendBillAsync req = dian.Habilitacion.SendBillAsync
resp = client.request(req(filename, open(zipfile, "rb").read())) resp = client.request(req(
filename,
open(zipfile, 'rb').read()
))
print(resp) print(resp)
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.argument("filename", required=True) @click.argument('filename', required=True)
@click.argument("zipfile", type=click.Path(exists=True)) @click.argument('zipfile', type=click.Path(exists=True))
def soap_send_bill_sync( def soap_send_bill_sync(private_key, public_key, habilitacion, password, filename, zipfile):
private_key, public_key, habilitacion, password, filename, zipfile
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.SendBillSync req = dian.SendBillSync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendBillSync req = dian.Habilitacion.SendBillSync
resp = client.request(req(filename, open(zipfile, "rb").read())) resp = client.request(req(
filename,
open(zipfile, 'rb').read()
))
print(resp) print(resp)
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.option("--track-id", required=True) @click.option('--track-id', required=True)
def soap_get_status_zip( def soap_get_status_zip(private_key, public_key, habilitacion, password, track_id):
private_key, public_key, habilitacion, password, track_id
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.GetStatusZip req = dian.GetStatusZip
if habilitacion: if habilitacion:
req = dian.Habilitacion.GetStatusZip req = dian.Habilitacion.GetStatusZip
resp = client.request(req(trackId=track_id)) resp = client.request(req(
trackId = track_id
))
print("StatusCode:", resp.StatusCode) print("StatusCode:", resp.StatusCode)
print("StatusDescription:", resp.StatusDescription) print("StatusDescription:", resp.StatusDescription)
@@ -164,78 +143,68 @@ def soap_get_status_zip(
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.option("--track-id", required=True) @click.option('--track-id', required=True)
def soap_get_status( def soap_get_status(private_key, public_key, habilitacion, password, track_id):
private_key, public_key, habilitacion, password, track_id
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.GetStatus req = dian.GetStatus
if habilitacion: if habilitacion:
req = dian.Habilitacion.GetStatus req = dian.Habilitacion.GetStatus
resp = client.request(req(trackId=track_id)) resp = client.request(req(
trackId = track_id
))
print(resp) print(resp)
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.option("--nit", required=True) @click.option('--nit', required=True)
@click.option("--nit-proveedor", required=True) @click.option('--nit-proveedor', required=True)
@click.option("--id-software", required=True) @click.option('--id-software', required=True)
def soap_get_numbering_range( def soap_get_numbering_range(private_key,
private_key,
public_key, public_key,
habilitacion, habilitacion,
password, password,
nit, nit, nit_proveedor, id_software):
nit_proveedor,
id_software,
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.GetNumberingRange req = dian.GetNumberingRange
if habilitacion: if habilitacion:
req = dian.Habilitacion.GetNumberingRange req = dian.Habilitacion.GetNumberingRange
resp = client.request(req(nit, nit_proveedor, id_software)) resp = client.request(req(
nit, nit_proveedor, id_software
))
print(resp) print(resp)
@click.command() @click.command()
@click.argument("invoice_path") @click.argument('invoice_path')
def validate_invoice(invoice_path): def validate_invoice(invoice_path):
warnings.warn("!! NO APROBADO FUNCIONAMIENTO") warnings.warn("!! NO APROBADO FUNCIONAMIENTO")
from facho.fe.data.dian import XSD from facho.fe.data.dian import XSD
content = open(invoice_path, 'r').read()
content = open(invoice_path, "r").read()
# TODO donde ubicar esta responsabilidad? # TODO donde ubicar esta responsabilidad?
# esto es requerido por el XSD de la DIAN # esto es requerido por el XSD de la DIAN
content = content.replace( content = content.replace(
'xmlns:fe="http://www.dian.gov.co/contratos/facturaelectronica/v1"', '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) XSD.validate(content, XSD.UBLInvoice)
@click.command() @click.command()
@click.argument("nomina_path") @click.argument('nomina_path')
def validate_nominaindividual(nomina_path): def validate_nominaindividual(nomina_path):
from facho.fe.data.dian import XSD from facho.fe.data.dian import XSD
content = open(nomina_path, 'r').read()
content = open(nomina_path, "r").read()
content = content.replace( content = content.replace(
'xmlns="http://www.dian.gov.co/contratos/facturaelectronica/v1"', 'xmlns="http://www.dian.gov.co/contratos/facturaelectronica/v1"',
'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"', 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"',
@@ -244,55 +213,35 @@ def validate_nominaindividual(nomina_path):
@click.command() @click.command()
@click.option("--private-key", type=click.Path(exists=True)) @click.option('--private-key', type=click.Path(exists=True))
@click.option("--passphrase") @click.option('--passphrase')
@click.option("--ssl/--no-ssl", default=False) @click.option('--ssl/--no-ssl', default=False)
@click.option("--use-cache-policy/--no-use-cache-policy", 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('xmlfile', type=click.Path(exists=True), required=True)
@click.argument("output", required=True) @click.argument('output', required=True)
def sign_xml( def sign_xml(private_key, passphrase, xmlfile, ssl=True, use_cache_policy=False, output=None):
private_key,
passphrase,
xmlfile,
ssl=True,
use_cache_policy=False,
output=None,
):
if not ssl: if not ssl:
disable_ssl() disable_ssl()
from facho import fe from facho import fe
if use_cache_policy: if use_cache_policy:
warnings.warn("xades using cache policy") warnings.warn("xades using cache policy")
signer = fe.DianXMLExtensionSigner( signer = fe.DianXMLExtensionSigner(private_key, passphrase=passphrase, localpolicy=use_cache_policy)
private_key, passphrase=passphrase, localpolicy=use_cache_policy document = open(xmlfile, 'r').read().encode('utf-8')
) with open(output, 'w') as f:
document = open(xmlfile, "r").read().encode("utf-8")
with open(output, "w") as f:
f.write(signer.sign_xml_string(document)) f.write(signer.sign_xml_string(document))
@click.command() @click.command()
@click.option("--private-key", type=click.Path(exists=True)) @click.option('--private-key', type=click.Path(exists=True))
@click.option("--generate/--validate", default=False) @click.option('--generate/--validate', default=False)
@click.option("--passphrase") @click.option('--passphrase')
@click.option("--ssl/--no-ssl", default=False) @click.option('--ssl/--no-ssl', default=False)
@click.option("--sign/--no-sign", default=False) @click.option('--sign/--no-sign', default=False)
@click.option("--use-cache-policy/--no-use-cache-policy", 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('scriptname', type=click.Path(exists=True), required=True)
@click.argument("output", required=True) @click.argument('output', required=True)
def generate_invoice( def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=True, sign=False, use_cache_policy=False, output=None):
private_key,
passphrase,
scriptname,
generate=False,
ssl=True,
sign=False,
use_cache_policy=False,
output=None,
):
""" """
imprime xml en pantalla. imprime xml en pantalla.
SCRIPTNAME espera SCRIPTNAME espera
@@ -305,15 +254,13 @@ def generate_invoice(
import importlib.util 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) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
from facho.fe.form_xml import ( import facho.fe.form as form
DIANWriteSigned, from facho.fe.form_xml import DIANInvoiceXML, DIANWriteSigned, DIANWrite, DIANSupportDocumentXML
DIANWrite, from facho import fe
DIANSupportDocumentXML,
)
try: try:
invoice_xml = module.document_xml() invoice_xml = module.document_xml()
@@ -332,39 +279,24 @@ def generate_invoice(
xml.add_extension(extension) xml.add_extension(extension)
if sign: if sign:
DIANWriteSigned( DIANWriteSigned(xml, output, private_key, passphrase, use_cache_policy)
xml, output, private_key, passphrase, use_cache_policy
)
else: else:
DIANWrite(xml, output) DIANWrite(xml, output)
@click.command() @click.command()
@click.option("--private-key", type=click.Path(exists=True)) @click.option('--private-key', type=click.Path(exists=True))
@click.option("--passphrase") @click.option('--passphrase')
@click.option("--ssl/--no-ssl", default=False) @click.option('--ssl/--no-ssl', default=False)
@click.option("--sign/--no-sign", default=False) @click.option('--sign/--no-sign', default=False)
@click.option("--use-cache-policy/--no-use-cache-policy", 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('scriptname', type=click.Path(exists=True), required=True)
@click.argument("output", required=True) @click.argument('output', required=True)
def generate_nomina( def generate_nomina(private_key, passphrase, scriptname, ssl=True, sign=False, use_cache_policy=False, output=None):
private_key,
passphrase,
scriptname,
ssl=True,
sign=False,
use_cache_policy=False,
output=None,
):
""" """
imprime xml en pantalla. imprime xml en pantalla.
SCRIPTNAME espera SCRIPTNAME espera
def nomina() -> ( def nomina() -> fe.nomina.NominaIndividual
fe.nomina.NominaIndividual def extensions(fe.nomina.NominaIndividual): -> List[facho.FachoXMLExtension]
)
def extensions(
fe.nomina.NominaIndividual
): -> List[facho.FachoXMLExtension]
""" """
if not ssl: if not ssl:
@@ -372,7 +304,7 @@ def generate_nomina(
import importlib.util 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) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
@@ -388,83 +320,59 @@ def generate_nomina(
xml.add_extension(extension) xml.add_extension(extension)
if sign: if sign:
DIANWriteSigned( DIANWriteSigned(xml, output, private_key, passphrase, use_cache_policy, dian_signer=facho.fe.nomina.DianXMLExtensionSigner)
xml,
output,
private_key,
passphrase,
use_cache_policy,
dian_signer=facho.fe.nomina.DianXMLExtensionSigner,
)
else: else:
DIANWrite(xml, output) DIANWrite(xml, output)
@click.command() @click.command()
@click.option("--private-key", required=True) @click.option('--private-key', required=True)
@click.option("--public-key", required=True) @click.option('--public-key', required=True)
@click.option("--habilitacion/--produccion", default=False) @click.option('--habilitacion/--produccion', default=False)
@click.option("--password") @click.option('--password')
@click.argument("filename", required=True) @click.argument('filename', required=True)
@click.argument("zipfile", type=click.Path(exists=True)) @click.argument('zipfile', type=click.Path(exists=True))
def soap_send_nomina_sync( def soap_send_nomina_sync(private_key, public_key, habilitacion, password, filename, zipfile):
private_key, public_key, habilitacion, password, filename, zipfile
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient( client = dian.DianSignatureClient(private_key, public_key, password=password)
private_key, public_key, password=password
)
req = dian.SendNominaSync req = dian.SendNominaSync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendNominaSync req = dian.Habilitacion.SendNominaSync
resp = client.request(req(open(zipfile, "rb").read())) resp = client.request(req(
open(zipfile, 'rb').read()
))
print(resp) print(resp)
@click.command() @click.command()
@click.option("--private-key", type=click.Path(exists=True)) @click.option('--private-key', type=click.Path(exists=True))
@click.option("--passphrase") @click.option('--passphrase')
@click.option("--ssl/--no-ssl", default=False) @click.option('--ssl/--no-ssl', default=False)
@click.option("--use-cache-policy/--no-use-cache-policy", 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('xmlfile', type=click.Path(exists=True), required=True)
def sign_verify_xml( def sign_verify_xml(private_key, passphrase, xmlfile, ssl=True, use_cache_policy=False, output=None):
private_key,
passphrase,
xmlfile,
ssl=True,
use_cache_policy=False,
output=None,
):
if not ssl: if not ssl:
disable_ssl() disable_ssl()
from facho.fe import fe from facho.fe import fe
if use_cache_policy: if use_cache_policy:
warnings.warn("xades using cache policy") warnings.warn("xades using cache policy")
print("THIS ONLY WORKS FOR DOCUMENTS GENERATE WITH FACHO") print("THIS ONLY WORKS FOR DOCUMENTS GENERATE WITH FACHO")
signer = fe.DianXMLExtensionSignerVerifier( signer = fe.DianXMLExtensionSignerVerifier(private_key, passphrase=passphrase, localpolicy=use_cache_policy)
private_key, passphrase=passphrase, localpolicy=use_cache_policy document = open(xmlfile, 'r').read().encode('utf-8')
)
document = open(xmlfile, "r").read().encode("utf-8")
if signer.verify_string(document): if signer.verify_string(document):
print("+OK") print("+OK")
else: else:
print("-INVALID") print("-INVALID")
@click.command() @click.command()
@click.option("--software-id") @click.option('--software-id')
@click.option("--software-pin") @click.option('--software-pin')
@click.option("--nit") @click.option('--nit')
@click.option("--dv") @click.option('--dv')
@click.option("--output-zippath") @click.option('--output-zippath')
def generate_nomina_habilitacion( def generate_nomina_habilitacion(software_id, software_pin, nit, dv, output_zippath):
software_id, software_pin, nit, dv, output_zippath
):
from facho import fe from facho import fe
generador = fe.nomina.habilitacion.Habilitacion( generador = fe.nomina.habilitacion.Habilitacion(
@@ -472,17 +380,15 @@ def generate_nomina_habilitacion(
software_id=software_id, software_id=software_id,
software_pin=software_pin, software_pin=software_pin,
nit=nit, nit=nit,
dv=dv, dv=dv
) )
) )
generador.generar(output_zippath) generador.generar(output_zippath)
@click.group() @click.group()
def main(): def main():
pass pass
main.add_command(consultaResolucionesFacturacion) main.add_command(consultaResolucionesFacturacion)
main.add_command(soap_send_test_set_async) main.add_command(soap_send_test_set_async)
main.add_command(soap_send_bill_async) main.add_command(soap_send_bill_async)

View File

@@ -140,12 +140,12 @@ class LXMLBuilder:
attrs['pretty_print'] = attrs.pop('pretty_print', False) attrs['pretty_print'] = attrs.pop('pretty_print', False)
attrs['encoding'] = attrs.pop('encoding', 'UTF-8') attrs['encoding'] = attrs.pop('encoding', 'UTF-8')
for el in elem.iter(): for el in elem.getiterator():
keys = filter(lambda key: key.startswith('facho_'), el.keys()) keys = filter(lambda key: key.startswith('facho_'), el.keys())
self.remove_attributes(el, keys, exclude=['facho_optional']) self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True' is_optional = el.get('facho_optional', 'False') == 'True'
if is_optional and list(el) == [] and el.keys() == [ if is_optional and el.getchildren() == [] and el.keys() == [
'facho_optional']: 'facho_optional']:
el.getparent().remove(el) el.getparent().remove(el)
@@ -156,7 +156,6 @@ class FachoXML:
""" """
Decora XML con funciones de consulta XPATH de un solo elemento Decora XML con funciones de consulta XPATH de un solo elemento
""" """
def __init__(self, root, builder=None, nsmap=None, fragment_prefix='', def __init__(self, root, builder=None, nsmap=None, fragment_prefix='',
fragment_root_element=None): fragment_root_element=None):
if builder is None: if builder is None:
@@ -292,7 +291,7 @@ class FachoXML:
# se fuerza la adicion como un nuevo elemento # se fuerza la adicion como un nuevo elemento
if append: if append:
last_slibing = None last_slibing = None
for child in list(parent): for child in parent.getchildren():
if child.tag == node_tag: if child.tag == node_tag:
last_slibing = child last_slibing = child
@@ -390,94 +389,12 @@ class FachoXML:
def get_element(self, xpath, multiple=False): def get_element(self, xpath, multiple=False):
xpath = self.fragment_prefix + self._path_xpath_for(xpath) xpath = self.fragment_prefix + self._path_xpath_for(xpath)
return self.builder.xpath(self.root, xpath, multiple=multiple) elem = self.builder.xpath(self.root, xpath)
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: if elem is None:
return False raise AttributeError('xpath %s invalid' % (xpath))
# el placeholder no ha sido populado text = self.builder.get_text(elem)
if elem.get('facho_placeholder') == 'True': return str(text)
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): def tostring(self, **kw):
return self.builder.tostring(self.root, **kw) return self.builder.tostring(self.root, **kw)

View File

@@ -14,22 +14,3 @@ from .fe import AMBIENTE_PRUEBAS
from .fe import AMBIENTE_PRODUCCION from .fe import AMBIENTE_PRODUCCION
from . import form_xml from . import form_xml
from . import nomina 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',
]

View File

@@ -1,13 +1,23 @@
from facho import facho
import zeep import zeep
from zeep.wsse.username import UsernameToken from zeep.wsse.username import UsernameToken
from .wsse.signature import BinarySignature from .wsse.signature import Signature, BinarySignature
from zeep.wsa import WsAddressingPlugin
import xmlsec import xmlsec
from dataclasses import dataclass, asdict 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
__all__ = ['DianClient'] __all__ = ['DianClient',
'ConsultaResolucionesFacturacionPeticion',
'ConsultaResolucionesFacturacionRespuesta']
class SOAPService: class SOAPService:
@@ -23,7 +33,6 @@ class SOAPService:
def todict(self): def todict(self):
return asdict(self) return asdict(self)
@dataclass @dataclass
class GetNumberingRangeResponse: class GetNumberingRangeResponse:
@@ -38,7 +47,8 @@ class GetNumberingRangeResponse:
ValidateDateTo: str ValidateDateTo: str
TechnicalKey: str TechnicalKey: str
NumberRangeResponse: list[NumberRangeResponse] NumberRangeResponse: List[NumberRangeResponse]
@classmethod @classmethod
def fromdict(cls, data): def fromdict(cls, data):
@@ -81,7 +91,7 @@ class SendBillAsync(SOAPService):
@dataclass @dataclass
class SendTestSetAsyncResponse: class SendTestSetAsyncResponse:
ZipKey: str ZipKey: str
ErrorMessageList: list[str] ErrorMessageList: List[str]
@classmethod @classmethod
def fromdict(cls, data): def fromdict(cls, data):
@@ -90,7 +100,6 @@ class SendTestSetAsyncResponse:
data['ErrorMessageList'] or [] data['ErrorMessageList'] or []
) )
@dataclass @dataclass
class SendTestSetAsync(SOAPService): class SendTestSetAsync(SOAPService):
fileName: str fileName: str
@@ -106,7 +115,6 @@ class SendTestSetAsync(SOAPService):
def build_response(self, as_dict): def build_response(self, as_dict):
return SendTestSetAsyncResponse.fromdict(as_dict) return SendTestSetAsyncResponse.fromdict(as_dict)
@dataclass @dataclass
class SendBillSync(SOAPService): class SendBillSync(SOAPService):
fileName: str fileName: str
@@ -121,13 +129,12 @@ class SendBillSync(SOAPService):
def build_response(self, as_dict): def build_response(self, as_dict):
return as_dict return as_dict
@dataclass @dataclass
class GetStatusResponse: class GetStatusResponse:
IsValid: bool IsValid: bool
StatusDescription: str StatusDescription: str
StatusCode: int StatusCode: int
ErrorMessage: list[str] ErrorMessage: List[str]
@classmethod @classmethod
def fromdict(cls, data): def fromdict(cls, data):
@@ -155,7 +162,6 @@ class GetStatus(SOAPService):
def build_response(self, as_dict): def build_response(self, as_dict):
return GetStatusResponse.fromdict(as_dict) return GetStatusResponse.fromdict(as_dict)
@dataclass @dataclass
class GetStatusZip(SOAPService): class GetStatusZip(SOAPService):
trackId: bytes trackId: bytes
@@ -169,7 +175,6 @@ class GetStatusZip(SOAPService):
def build_response(self, as_dict): def build_response(self, as_dict):
return GetStatusResponse.fromdict(as_dict[0]) return GetStatusResponse.fromdict(as_dict[0])
@dataclass @dataclass
class SendNominaSync(SOAPService): class SendNominaSync(SOAPService):
contentFile: bytes contentFile: bytes
@@ -215,7 +220,6 @@ class Habilitacion:
def get_wsdl(self): def get_wsdl(self):
return Habilitacion.WSDL return Habilitacion.WSDL
class DianGateway: class DianGateway:
def _open(self, service): def _open(self, service):
@@ -246,11 +250,7 @@ class DianClient(DianGateway):
self._password = password self._password = password
def _open(self, service): def _open(self, service):
return zeep.Client( return zeep.Client(service.get_wsdl(), wsse=UsernameToken(self._username, self._password))
service.get_wsdl(),
wsse=UsernameToken(
self._username,
self._password))
class DianSignatureClient(DianGateway): class DianSignatureClient(DianGateway):
@@ -262,10 +262,13 @@ class DianSignatureClient(DianGateway):
def _open(self, service): def _open(self, service):
# RESOLUCCION 0004: pagina 756 # RESOLUCCION 0004: pagina 756
from zeep.wsse import utils
client = zeep.Client(service.get_wsdl(), wsse=BinarySignature( client = zeep.Client(service.get_wsdl(), wsse=
BinarySignature(
self.private_key_path, self.public_key_path, self.password, self.private_key_path, self.public_key_path, self.password,
signature_method=xmlsec.Transform.RSA_SHA256, signature_method=xmlsec.Transform.RSA_SHA256,
digest_method=xmlsec.Transform.SHA256), digest_method=xmlsec.Transform.SHA256)
,
) )
return client return client

View File

@@ -9,7 +9,7 @@ module.
""" """
import pytz import pytz
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from lxml import etree from lxml import etree
from lxml.etree import QName from lxml.etree import QName
@@ -70,11 +70,8 @@ class MemorySignature(object):
def apply(self, envelope, headers): def apply(self, envelope, headers):
key = _make_sign_key(self.key_data, self.cert_data, self.password) key = _make_sign_key(self.key_data, self.cert_data, self.password)
_sign_envelope_with_key( _sign_envelope_with_key(
envelope, envelope, key, self.signature_method, self.digest_method, expires_dt=self.expires_dt
key, )
self.signature_method,
self.digest_method,
expires_dt=self.expires_dt)
return envelope, headers return envelope, headers
def verify(self, envelope): def verify(self, envelope):
@@ -84,7 +81,7 @@ class MemorySignature(object):
class Signature(MemorySignature): class Signature(MemorySignature):
"""Sign given SOAP envelope with WSSE sig using given key and cert file.""" """Sign given SOAP envelope with WSSE sig using given key file and cert file."""
def __init__( def __init__(
self, self,
@@ -104,18 +101,15 @@ class Signature(MemorySignature):
class BinarySignature(Signature): class BinarySignature(Signature):
"""Sign given SOAP envelope with WSSE sig using given key and cert file. """Sign given SOAP envelope with WSSE sig using given key file and cert file.
Place the key information into BinarySecurityElement.""" Place the key information into BinarySecurityElement."""
def apply(self, envelope, headers): def apply(self, envelope, headers):
key = _make_sign_key(self.key_data, self.cert_data, self.password) key = _make_sign_key(self.key_data, self.cert_data, self.password)
_sign_envelope_with_key_binary( _sign_envelope_with_key_binary(
envelope, envelope, key, self.signature_method, self.digest_method, expires_dt = self.expires_dt
key, )
self.signature_method,
self.digest_method,
expires_dt=self.expires_dt)
return envelope, headers return envelope, headers
@@ -225,12 +219,10 @@ def sign_envelope(
""" """
# Load the signing key and certificate. # Load the signing key and certificate.
key = _make_sign_key(_read_file(keyfile), _read_file(certfile), password) key = _make_sign_key(_read_file(keyfile), _read_file(certfile), password)
return _sign_envelope_with_key( return _sign_envelope_with_key(envelope, key, signature_method, digest_method)
envelope, key, signature_method, digest_method)
def get_timestamp(timestamp = None, delta=None): def get_timestamp(timestamp = None, delta=None):
timestamp = timestamp or datetime.now(timezone.utc) timestamp = timestamp or datetime.utcnow()
if delta: if delta:
timestamp += delta timestamp += delta
@@ -238,33 +230,25 @@ def get_timestamp(timestamp=None, delta=None):
timestamp = timestamp.replace(tzinfo=pytz.utc, microsecond=0) timestamp = timestamp.replace(tzinfo=pytz.utc, microsecond=0)
return timestamp.strftime(format_) return timestamp.strftime(format_)
def _append_timestamp(security, expires_dt=None): def _append_timestamp(security, expires_dt=None):
if expires_dt is None: if expires_dt is None:
expires_dt = timedelta(seconds=6000) expires_dt = timedelta(seconds=6000)
etimestamp = utils.WSU.Timestamp({ timestamp = datetime.now()
'{http://docs.oasis-open.org/wss/2004/01/' 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()})
'oasis-200401-wss-wssecurity-utility-1.0.xsd}Id': etimestamp.append(utils.WSU.Created(get_timestamp(timestamp=timestamp)))
utils.get_unique_id()}) etimestamp.append(utils.WSU.Expires(get_timestamp(timestamp=timestamp, delta=expires_dt)))
etimestamp.append(utils.WSU.Created(get_timestamp()))
etimestamp.append(utils.WSU.Expires(get_timestamp(delta=expires_dt)))
security.insert(0, etimestamp) security.insert(0, etimestamp)
if etree.LXML_VERSION[:2] >= (3, 5): if etree.LXML_VERSION[:2] >= (3, 5):
etree.cleanup_namespaces(security, etree.cleanup_namespaces(security,
keep_ns_prefixes = security.nsmap, keep_ns_prefixes = security.nsmap,
top_nsmap=utils.NSMAP) top_nsmap=utils.NSMAP)
else: else:
etree.cleanup_namespaces(security) etree.cleanup_namespaces(header)
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.""" """Prepare envelope and sign."""
soap_env = detect_soap_env(envelope)
# Create the Signature node. # Create the Signature node.
signature = xmlsec.template.create( signature = xmlsec.template.create(
@@ -295,7 +279,7 @@ def _signature_prepare(
_append_timestamp(security, expires_dt=expires_dt) _append_timestamp(security, expires_dt=expires_dt)
timestamp = security.find(QName(ns.WSU, "Timestamp")) timestamp = security.find(QName(ns.WSU, "Timestamp"))
if timestamp is not None: if timestamp != None:
_sign_node(ctx, signature, timestamp, digest_method) _sign_node(ctx, signature, timestamp, digest_method)
ctx.sign(signature) ctx.sign(signature)
@@ -303,30 +287,18 @@ def _signature_prepare(
# KeyInfo. The recipient expects this structure, but we can't rearrange # KeyInfo. The recipient expects this structure, but we can't rearrange
# like this until after signing, because otherwise xmlsec won't populate # like this until after signing, because otherwise xmlsec won't populate
# the X509 data (because it doesn't understand WSSE). # the X509 data (because it doesn't understand WSSE).
sec_token_ref = etree.SubElement( sec_token_ref = etree.SubElement(key_info, QName(ns.WSSE, "SecurityTokenReference"))
key_info, QName(
ns.WSSE, "SecurityTokenReference"))
return security, sec_token_ref, x509_data return security, sec_token_ref, x509_data
def _sign_envelope_with_key( def _sign_envelope_with_key(envelope, key, signature_method, digest_method, expires_dt=None):
envelope,
key,
signature_method,
digest_method,
expires_dt=None):
_, sec_token_ref, x509_data = _signature_prepare( _, sec_token_ref, x509_data = _signature_prepare(
envelope, key, signature_method, digest_method, expires_dt=expires_dt envelope, key, signature_method, digest_method, expires_dt=expires_dt
) )
sec_token_ref.append(x509_data) sec_token_ref.append(x509_data)
def _sign_envelope_with_key_binary( def _sign_envelope_with_key_binary(envelope, key, signature_method, digest_method, expires_dt=None):
envelope,
key,
signature_method,
digest_method,
expires_dt=None):
security, sec_token_ref, x509_data = _signature_prepare( security, sec_token_ref, x509_data = _signature_prepare(
envelope, key, signature_method, digest_method, expires_dt=expires_dt envelope, key, signature_method, digest_method, expires_dt=expires_dt
) )
@@ -382,10 +354,7 @@ def _verify_envelope_with_key(envelope, key):
ctx = xmlsec.SignatureContext() ctx = xmlsec.SignatureContext()
# Find each signed element and register its ID with the signing context. # Find each signed element and register its ID with the signing context.
refs = signature.xpath( refs = signature.xpath("ds:SignedInfo/ds:Reference", namespaces={"ds": ns.DS})
"ds:SignedInfo/ds:Reference",
namespaces={
"ds": ns.DS})
for ref in refs: for ref in refs:
# Get the reference URI and cut off the initial '#' # Get the reference URI and cut off the initial '#'
referenced_id = ref.get("URI")[1:] referenced_id = ref.get("URI")[1:]

View File

@@ -7,17 +7,10 @@ def path_for_xsd(dirname, xsdname):
data_dir = os.path.dirname(os.path.abspath(__file__)) data_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(data_dir, dirname, xsdname) return os.path.join(data_dir, dirname, xsdname)
UBLInvoice= xmlschema.XMLSchema(path_for_xsd('maindoc', 'UBL-Invoice-2.1.xsd'))
UBLInvoice = xmlschema.XMLSchema( NominaIndividual = xmlschema.XMLSchema(path_for_xsd('nomina', 'NominaIndividualElectronicaXSDV1.0.6.xsd'))
path_for_xsd( NominaIndividualDeAjuste = xmlschema.XMLSchema(path_for_xsd('nomina', 'NominaIndividualDeAjusteElectronicaXSDV1.0.6.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): def validate(xml, schema):
schema.validate(xml) schema.validate(xml)

View File

@@ -35,8 +35,8 @@ class CodeList:
row = {} row = {}
#construir registro... #construir registro...
for value in list(xmlrow): for value in xmlrow.getchildren():
row[value.attrib['ColumnRef']] = list(value)[0].text row[value.attrib['ColumnRef']] = value.getchildren()[0].text
return row return row
@@ -60,7 +60,6 @@ class CodeList:
# nombres de variables igual a ./Identification/ShortName # nombres de variables igual a ./Identification/ShortName
# TODO: garantizar unica carga en python # TODO: garantizar unica carga en python
__all__ = ['TipoOrganizacion', __all__ = ['TipoOrganizacion',
'TipoResponsabilidad', 'TipoResponsabilidad',
'TipoAmbiente', 'TipoAmbiente',
@@ -73,88 +72,38 @@ __all__ = ['TipoOrganizacion',
'Municipio', 'Municipio',
'Departamento'] 'Departamento']
def path_for_codelist(name): def path_for_codelist(name):
return os.path.join(DATA_DIR, name) return os.path.join(DATA_DIR, name)
TipoOrganizacion = CodeList(path_for_codelist('TipoOrganizacion-2.1.gc'), 'code', 'name')
TipoOrganizacion = CodeList(path_for_codelist( TipoResponsabilidad = CodeList(path_for_codelist('TipoResponsabilidad-2.1.gc'), 'code', 'name')\
'TipoOrganizacion-2.1.gc'), 'code', 'name') .update(CodeList(path_for_codelist('TipoResponsabilidad-2.1.custom.gc'), 'code', 'name'))
TipoResponsabilidad = CodeList( TipoAmbiente = CodeList(path_for_codelist('TipoAmbiente-2.1.gc'), 'code', 'name')
path_for_codelist('TipoResponsabilidad-2.1.gc'), TipoDocumento = CodeList(path_for_codelist('TipoDocumento-2.1.gc'), 'code', 'name')
'code', TipoImpuesto = CodeList(path_for_codelist('TipoImpuesto-2.1.gc'), 'code', 'name')\
'name') .update( .update(CodeList(path_for_codelist('TipoImpuesto-2.1.custom.gc'), 'code', 'name'))
CodeList( TarifaImpuesto = CodeList(path_for_codelist('TarifaImpuestoINC-2.1.gc'), 'code', 'name')\
path_for_codelist('TipoResponsabilidad-2.1.custom.gc'), .update(CodeList(path_for_codelist('TarifaImpuestoIVA-2.1.gc'), 'code', 'name'))\
'code', .update(CodeList(path_for_codelist('TarifaImpuestoReteIVA-2.1.gc'), 'code', 'name'))\
'name')) .update(CodeList(path_for_codelist('TarifaImpuestoReteRenta-2.1.gc'), 'code', 'name'))
TipoAmbiente = CodeList(path_for_codelist( CodigoPrecioReferencia = CodeList(path_for_codelist('CodigoPrecioReferencia-2.1.gc'), 'code', 'name')
'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') MediosPago = CodeList(path_for_codelist('MediosPago-2.1.gc'), 'code', 'name')
FormasPago = CodeList(path_for_codelist('FormasPago-2.1.gc'), 'code', 'name') FormasPago = CodeList(path_for_codelist('FormasPago-2.1.gc'), 'code', 'name')
RegimenFiscal = CodeList(path_for_codelist( RegimenFiscal = CodeList(path_for_codelist('RegimenFiscal-2.1.custom.gc'), 'code', 'name')
'RegimenFiscal-2.1.custom.gc'), 'code', 'name') TipoOperacionNC = CodeList(path_for_codelist('TipoOperacionNC-2.1.gc'), 'code', 'name')
TipoOperacionNC = CodeList(path_for_codelist( TipoOperacionNCDS = CodeList(path_for_codelist('TipoOperacionNCDS-2.1.gc'), 'code', 'name')
'TipoOperacionNC-2.1.gc'), 'code', 'name') TipoOperacionND = CodeList(path_for_codelist('TipoOperacionND-2.1 - copia.gc'), 'code', 'name')
TipoOperacionNCDS = CodeList(path_for_codelist( TipoOperacionF = CodeList(path_for_codelist('TipoOperacionF-2.1.gc'), 'code', 'name')\
'TipoOperacionNCDS-2.1.gc'), 'code', 'name') .update(CodeList(path_for_codelist('TipoOperacionF-2.1.custom.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') Municipio = CodeList(path_for_codelist('Municipio-2.1.gc'), 'code', 'name')
Departamento = CodeList(path_for_codelist( Departamento = CodeList(path_for_codelist('Departamentos-2.1.gc'), 'code', 'name')
'Departamentos-2.1.gc'), 'code', 'name')
Paises = CodeList(path_for_codelist('Paises-2.1.gc'), 'code', 'name') Paises = CodeList(path_for_codelist('Paises-2.1.gc'), 'code', 'name')
TipoIdFiscal = CodeList(path_for_codelist( TipoIdFiscal = CodeList(path_for_codelist('TipoIdFiscal-2.1.gc'), 'code', 'name')
'TipoIdFiscal-2.1.gc'), 'code', 'name') CodigoDescuento = CodeList(path_for_codelist('CodigoDescuento-2.1.gc'), 'code', 'name')
CodigoDescuento = CodeList(path_for_codelist( UnidadesMedida = CodeList(path_for_codelist('UnidadesMedida-2.1.gc'), 'code', 'name')
'CodigoDescuento-2.1.gc'), 'code', 'name') TipoTrabajador = CodeList(path_for_codelist('TipoTrabajador-2.1.gc'), 'code', 'name')
UnidadesMedida = CodeList(path_for_codelist( SubTipoTrabajador = CodeList(path_for_codelist('SubTipoTrabajador-2.1.gc'), 'code', 'name')
'UnidadesMedida-2.1.gc'), 'code', 'name') TipoContrato = CodeList(path_for_codelist('TipoContrato-2.1.gc'), 'code', 'name')
TipoTrabajador = CodeList(path_for_codelist( PeriodoNomina = CodeList(path_for_codelist('PeriodoNomina-2.1.gc'), 'code', 'name')
'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') TipoMoneda = CodeList(path_for_codelist('TipoMoneda-2.1.gc'), 'code', 'name')
IdiomaISO6391 = CodeList(path_for_codelist( IdiomaISO6391 = CodeList(path_for_codelist('Idioma-2.1.gc'), 'iso-639-1', 'name')
'Idioma-2.1.gc'), 'iso-639-1', 'name')

View File

@@ -5,6 +5,7 @@ import uuid
import xmlsig import xmlsig
import xades import xades
from datetime import datetime from datetime import datetime
import OpenSSL
import zipfile import zipfile
# import warnings # import warnings
import hashlib import hashlib
@@ -17,65 +18,36 @@ from dateutil import tz
from cryptography.hazmat.primitives.serialization import pkcs12 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_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
SCHEME_AGENCY_ATTRS = { SCHEME_AGENCY_ATTRS = {
'schemeAgencyName': 'CO, DIAN (Dirección de Impuestos y Aduanas' 'schemeAgencyName': 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)',
' Nacionales)', 'schemeAgencyID': '195'
'schemeAgencyID': '195'} }
# RESOLUCION 0001: pagina 516 # RESOLUCION 0001: pagina 516
POLICY_ID = ( POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf'
'https://facturaelectronica.dian.gov.co/politicadefirma/v2' POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.'
'/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') Bogota = tz.gettz('America/Bogota')
# NAMESPACES = { # NAMESPACES = {
# 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2', # 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
# 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual', # 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual',
# 'nominaajuste': 'dian:gov:co:facturaelectronica:' # 'nominaajuste': 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste',
# 'NominaIndividualDeAjuste',
# 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1', # 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1',
# 'xs': 'http://www.w3.org/2001/XMLSchema-instance', # 'xs': 'http://www.w3.org/2001/XMLSchema-instance',
# 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:' # 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
# 'CommonAggregateComponents-2', # 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
# 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:' # 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1',
# 'CommonBasicComponents-2',
# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:'
# 'schema:xsd:DocumentInformationAggregateComponents-1',
# 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001', # 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001',
# 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:' # 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003',
# 'IANAMIMEMediaType:2003', # 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2',
# 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:' # 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2',
# 'CommonExtensionComponents-2',
# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:'
# 'QualifiedDatatypes-2',
# 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', # 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1',
# 'udt': 'urn:un:unece:uncefact:data:specification:' # 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
# 'UnqualifiedDataTypesSchemaModule:2',
# 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', # 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
# 'xades': 'http://uri.etsi.org/01903/v1.3.2#', # '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#',
@@ -85,20 +57,15 @@ Bogota = tz.gettz('America/Bogota')
NAMESPACES = { 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', 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1',
'cac': ( 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'urn:oasis:names:specification:ubl:schema:xsd' 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
':CommonAggregateComponents-2'), 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-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', 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2',
'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1',
'udt': ( 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
'urn:un:unece:uncefact:data:specification'
':UnqualifiedDataTypesSchemaModule:2'),
'xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'ds': 'http://www.w3.org/2000/09/xmldsig#', 'ds': 'http://www.w3.org/2000/09/xmldsig#',
'xades': 'http://uri.etsi.org/01903/v1.3.2#', 'xades': 'http://uri.etsi.org/01903/v1.3.2#',
@@ -140,21 +107,22 @@ class FeXML(FachoXML):
return super().from_string(document, namespaces=NAMESPACES) return super().from_string(document, namespaces=NAMESPACES)
def tostring(self, **kw): def tostring(self, **kw):
# MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un # MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un namespace
# 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'
}
root_namespace = self.root_namespace() root_namespace = self.root_namespace()
root_localname = self.root_localname() root_localname = self.root_localname()
xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace] 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)\ return super().tostring(**kw)\
.replace(xmlns_name + ':', '')\ .replace(xmlns_name + ':', '')\
.replace('xmlns:'+xmlns_name, 'xmlns')\ .replace('xmlns:'+xmlns_name, 'xmlns')\
.replace(root_namespace, urn_oasis) .replace(root_namespace, urn_oasis[root_localname])
class DianXMLExtensionCUDFE(FachoXMLExtension): class DianXMLExtensionCUDFE(FachoXMLExtension):
@@ -173,12 +141,9 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
def _get_qrcode(self, cufe): def _get_qrcode(self, cufe):
url_for = { url_for = {
AMBIENTE_PRUEBAS: ( AMBIENTE_PRUEBAS: 'https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey=',
'https://catalogo-vpfe-hab.dian.gov.co/document/' AMBIENTE_PRODUCCION: 'https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey='
'searchqr?documentkey='), }
AMBIENTE_PRODUCCION: (
'https://catalogo-vpfe.dian.gov.co/document/'
'searchqr?documentkey=')}
return url_for[self.tipo_ambiente] + cufe return url_for[self.tipo_ambiente] + cufe
def build(self, fachoxml): def build(self, fachoxml):
@@ -189,29 +154,21 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
if self.schemeName() == "CUDS-SHA384": if self.schemeName() == "CUDS-SHA384":
if fachoxml.tag_document() == 'Invoice': if fachoxml.tag_document() == 'Invoice':
fachoxml.set_element( fachoxml.set_element('./cbc:ProfileID',
'./cbc:ProfileID', 'DIAN 2.1: documento soporte en adquisiciones efectuadas a no obligados a facturar.')
'DIAN 2.1: documento soporte en adquisiciones '
'efectuadas a no obligados a facturar.')
else: else:
fachoxml.set_element( fachoxml.set_element('./cbc:ProfileID',
'./cbc:ProfileID', 'DIAN 2.1: Nota de ajuste al documento soporte en adquisiciones efectuadas a sujetos no obligados a expedir factura o documento equivalente')
'DIAN 2.1: Nota de ajuste al documento soporte en '
'adquisiciones efectuadas a sujetos no obligados a '
'expedir factura o documento equivalente')
else: else:
fachoxml.set_element( fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta')
'./cbc:ProfileID',
'DIAN 2.1: Factura Electrónica de Venta')
# fachoxml.set_element('./cbc:ProfileID', # #DIAN 1.8.-2021: FAD03
# 'DIAN 2.1: Factura Electrónica de Venta') # fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta')
fachoxml.set_element( fachoxml.set_element(
'./cbc:ProfileExecutionID', self._tipo_ambiente_int()) './cbc:ProfileExecutionID', self._tipo_ambiente_int())
#DIAN 1.7.-2020: FAB36 #DIAN 1.7.-2020: FAB36
fachoxml.set_element( fachoxml.set_element(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode',
':DianExtensions/sts:QRCode',
self._get_qrcode(cufe)) self._get_qrcode(cufe))
def issue_time(self, datetime_): def issue_time(self, datetime_):
@@ -227,8 +184,7 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
build_vars['FecFac'] = self.issue_date(invoice.invoice_issue) build_vars['FecFac'] = self.issue_date(invoice.invoice_issue)
build_vars['HoraFac'] = self.issue_time(invoice.invoice_issue) build_vars['HoraFac'] = self.issue_time(invoice.invoice_issue)
# PAG 601 # PAG 601
build_vars['ValorBruto'] = ( build_vars['ValorBruto'] = invoice.invoice_legal_monetary_total.line_extension_amount
invoice.invoice_legal_monetary_total.line_extension_amount)
build_vars['ValorTotalPagar' build_vars['ValorTotalPagar'
] = invoice.invoice_legal_monetary_total.payable_amount ] = invoice.invoice_legal_monetary_total.payable_amount
ValorImpuestoPara = defaultdict(lambda: form.Amount(0.0)) ValorImpuestoPara = defaultdict(lambda: form.Amount(0.0))
@@ -240,8 +196,7 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
if subtotal.scheme is not None: if subtotal.scheme is not None:
# TODO cual es la naturaleza de tax_scheme_ident? # TODO cual es la naturaleza de tax_scheme_ident?
codigo_impuesto = subtotal.scheme.code codigo_impuesto = subtotal.scheme.code
ValorImpuestoPara.setdefault( ValorImpuestoPara.setdefault(codigo_impuesto, form.Amount(0.0))
codigo_impuesto, form.Amount(0.0))
ValorImpuestoPara[codigo_impuesto] += subtotal.tax_amount ValorImpuestoPara[codigo_impuesto] += subtotal.tax_amount
build_vars['ValorImpuestoPara'] = ValorImpuestoPara build_vars['ValorImpuestoPara'] = ValorImpuestoPara
@@ -281,35 +236,21 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
CodImpuesto2 = build_vars['CodImpuesto2'] CodImpuesto2 = build_vars['CodImpuesto2']
CodImpuesto3 = build_vars['CodImpuesto3'] CodImpuesto3 = build_vars['CodImpuesto3']
return [ return [
'%s' % '%s' % build_vars['NumFac'],
build_vars['NumFac'], '%s' % build_vars['FecFac'],
'%s' % '%s' % build_vars['HoraFac'],
build_vars['FecFac'], form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
'%s' %
build_vars['HoraFac'],
form.Amount(
build_vars['ValorBruto']).truncate_as_string(2),
CodImpuesto1, CodImpuesto1,
build_vars['ValorImpuestoPara'].get( build_vars['ValorImpuestoPara'].get(CodImpuesto1, form.Amount(0.0)).truncate_as_string(2),
CodImpuesto1,
form.Amount(0.0)).truncate_as_string(2),
CodImpuesto2, CodImpuesto2,
build_vars['ValorImpuestoPara'].get( build_vars['ValorImpuestoPara'].get(CodImpuesto2, form.Amount(0.0)).truncate_as_string(2),
CodImpuesto2,
form.Amount(0.0)).truncate_as_string(2),
CodImpuesto3, CodImpuesto3,
build_vars['ValorImpuestoPara'].get( build_vars['ValorImpuestoPara'].get(CodImpuesto3, form.Amount(0.0)).truncate_as_string(2),
CodImpuesto3,
form.Amount(0.0)).truncate_as_string(2),
build_vars['ValorTotalPagar'].truncate_as_string(2), build_vars['ValorTotalPagar'].truncate_as_string(2),
'%s' % '%s' % build_vars['NitOFE'],
build_vars['NitOFE'], '%s' % build_vars['NumAdq'],
'%s' % '%s' % build_vars['ClTec'],
build_vars['NumAdq'], '%d' % build_vars['TipoAmb'],
'%s' %
build_vars['ClTec'],
'%d' %
build_vars['TipoAmb'],
] ]
@@ -333,39 +274,21 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
CodImpuesto2 = build_vars['CodImpuesto2'] CodImpuesto2 = build_vars['CodImpuesto2']
CodImpuesto3 = build_vars['CodImpuesto3'] CodImpuesto3 = build_vars['CodImpuesto3']
return [ return [
'%s' % '%s' % build_vars['NumFac'],
build_vars['NumFac'], '%s' % build_vars['FecFac'],
'%s' % '%s' % build_vars['HoraFac'],
build_vars['FecFac'], form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
'%s' %
build_vars['HoraFac'],
form.Amount(
build_vars['ValorBruto']).truncate_as_string(2),
CodImpuesto1, CodImpuesto1,
form.Amount( form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2),
build_vars['ValorImpuestoPara'].get(
CodImpuesto1,
0.0)).truncate_as_string(2),
CodImpuesto2, CodImpuesto2,
form.Amount( form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).truncate_as_string(2),
build_vars['ValorImpuestoPara'].get(
CodImpuesto2,
0.0)).truncate_as_string(2),
CodImpuesto3, CodImpuesto3,
form.Amount( form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).truncate_as_string(2),
build_vars['ValorImpuestoPara'].get( form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2),
CodImpuesto3, '%s' % build_vars['NitOFE'],
0.0)).truncate_as_string(2), '%s' % build_vars['NumAdq'],
form.Amount( '%s' % build_vars['Software-PIN'],
build_vars['ValorTotalPagar']).truncate_as_string(2), '%d' % build_vars['TipoAmb'],
'%s' %
build_vars['NitOFE'],
'%s' %
build_vars['NumAdq'],
'%s' %
build_vars['Software-PIN'],
'%d' %
build_vars['TipoAmb'],
] ]
@@ -386,30 +309,20 @@ class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
def formatVars(self): def formatVars(self):
build_vars = self.buildVars() build_vars = self.buildVars()
CodImpuesto1 = build_vars['CodImpuesto1'] CodImpuesto1 = build_vars['CodImpuesto1']
CodImpuesto2 = build_vars['CodImpuesto2']
CodImpuesto3 = build_vars['CodImpuesto3']
return [ return [
'%s' % '%s' % build_vars['NumFac'],
build_vars['NumFac'], '%s' % build_vars['FecFac'],
'%s' % '%s' % build_vars['HoraFac'],
build_vars['FecFac'], form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
'%s' %
build_vars['HoraFac'],
form.Amount(
build_vars['ValorBruto']).truncate_as_string(2),
CodImpuesto1, CodImpuesto1,
form.Amount( form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2),
build_vars['ValorImpuestoPara'].get( form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2),
CodImpuesto1, '%s' % build_vars['NitOFE'],
0.0)).truncate_as_string(2), '%s' % build_vars['NumAdq'],
form.Amount( '%s' % build_vars['Software-PIN'],
build_vars['ValorTotalPagar']).truncate_as_string(2), '%d' % build_vars['TipoAmb'],
'%s' %
build_vars['NitOFE'],
'%s' %
build_vars['NumAdq'],
'%s' %
build_vars['Software-PIN'],
'%d' %
build_vars['TipoAmb'],
] ]
@@ -423,19 +336,14 @@ class DianXMLExtensionSoftwareProvider(FachoXMLExtension):
def build(self, fexml): def build(self, fexml):
software_provider = fexml.fragment( software_provider = fexml.fragment(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider')
':DianExtensions/sts:SoftwareProvider')
provider_id_attrs = SCHEME_AGENCY_ATTRS.copy() provider_id_attrs = SCHEME_AGENCY_ATTRS.copy()
provider_id_attrs.update({'schemeID': self.dv}) provider_id_attrs.update({'schemeID': self.dv})
#DIAN 1.7.-2020: FAB23 #DIAN 1.7.-2020: FAB23
provider_id_attrs.update({'schemeName': '31'}) provider_id_attrs.update({'schemeName': '31'})
software_provider.set_element( software_provider.set_element('/sts:SoftwareProvider/sts:ProviderID', self.nit,
'/sts:SoftwareProvider/sts:ProviderID',
self.nit,
**provider_id_attrs) **provider_id_attrs)
software_provider.set_element( software_provider.set_element('/sts:SoftwareProvider/sts:SoftwareID', self.id_software,
'/sts:SoftwareProvider/sts:SoftwareID',
self.id_software,
**SCHEME_AGENCY_ATTRS) **SCHEME_AGENCY_ATTRS)
@@ -448,10 +356,7 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
self.invoice_ident = invoice_ident self.invoice_ident = invoice_ident
def build(self, fexml): def build(self, fexml):
dian_path = ( dian_path = './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode'
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:SoftwareSecurityCode'
)
code = str(self.id_software) + str(self.pin) + str(self.invoice_ident) code = str(self.id_software) + str(self.pin) + str(self.invoice_ident)
m = hashlib.sha384() m = hashlib.sha384()
m.update(code.encode('utf-8')) m.update(code.encode('utf-8'))
@@ -505,11 +410,8 @@ class DianXMLExtensionSigner:
xml.append(signature) xml.append(signature)
ref = xmlsig.template.add_reference( ref = xmlsig.template.add_reference(
signature, signature, xmlsig.constants.TransformSha256, uri="", name="xmldsig-%s-ref0" % (id_uuid)
xmlsig.constants.TransformSha256, )
uri="",
name="xmldsig-%s-ref0" %
(id_uuid))
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped) xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid) id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid)
@@ -528,8 +430,9 @@ class DianXMLExtensionSigner:
id_props = "xmldsig-%s-signedprops" % (id_uuid) id_props = "xmldsig-%s-signedprops" % (id_uuid)
props_ref = xmlsig.template.add_reference( props_ref = xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="#%s" % signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_props),
(id_props), uri_type="http://uri.etsi.org/01903#SignedProperties") uri_type="http://uri.etsi.org/01903#SignedProperties"
)
xmlsig.template.add_transform( xmlsig.template.add_transform(
props_ref, xmlsig.constants.TransformInclC14N) props_ref, xmlsig.constants.TransformInclC14N)
@@ -547,6 +450,9 @@ class DianXMLExtensionSigner:
self._pkcs12_data, self._pkcs12_data,
self._passphrase)) self._passphrase))
# ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(
# self._pkcs12_data,
# self._passphrase))
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():
ctx.sign(signature) ctx.sign(signature)
@@ -570,9 +476,7 @@ class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
def build(self, fexml): def build(self, fexml):
attrs = {'schemeID': '4', 'schemeName': '31'} attrs = {'schemeID': '4', 'schemeName': '31'}
attrs.update(SCHEME_AGENCY_ATTRS) attrs.update(SCHEME_AGENCY_ATTRS)
authorization_provider = fexml.fragment( authorization_provider = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider')
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:AuthorizationProvider')
authorization_provider.set_element('./sts:AuthorizationProviderID', authorization_provider.set_element('./sts:AuthorizationProviderID',
'800197268', '800197268',
**attrs) **attrs)
@@ -581,17 +485,12 @@ class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
class DianXMLExtensionInvoiceSource(FachoXMLExtension): class DianXMLExtensionInvoiceSource(FachoXMLExtension):
# CAB13 # CAB13
def build(self, fexml): def build(self, fexml):
dian_path = ( dian_path = '/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode'
'/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionCo'
'ntent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode'
)
fexml.set_element( fexml.set_element(
dian_path, 'CO', dian_path, 'CO',
listAgencyID="6", listAgencyID="6",
listAgencyName="United Nations Economic Commission for Europe", listAgencyName="United Nations Economic Commission for Europe",
listSchemeURI=( listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1")
"urn:oasis:names:specification:ubl:codelist:gc:"
"CountryIdentificationCode-2.1"))
class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension): class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
@@ -608,38 +507,28 @@ class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
self.to = to self.to = to
def build(self, fexml): def build(self, fexml):
invoice_control = fexml.fragment( invoice_control = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl')
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' invoice_control.set_element('/sts:InvoiceControl/sts:InvoiceAuthorization', self.authorization)
':DianExtensions/sts:InvoiceControl') invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:StartDate',
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')) self.period_startdate.strftime('%Y-%m-%d'))
invoice_control.set_element( invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:EndDate',
'/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:EndDate',
self.period_enddate.strftime('%Y-%m-%d')) self.period_enddate.strftime('%Y-%m-%d'))
invoice_control.set_element( invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:Prefix',
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:Prefix',
self.prefix) self.prefix)
invoice_control.set_element( invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:From',
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:From', self.from_) self.from_)
invoice_control.set_element( invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To',
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To', self.to) self.to)
fexml.set_element( fexml.set_element(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
':DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
'CO', 'CO',
# DIAN 1.7.-2020: FAB15 # DIAN 1.7.-2020: FAB15
listAgencyID="6", listAgencyID="6",
# DIAN 1.7.-2020: FAB16 # DIAN 1.7.-2020: FAB16
listAgencyName="United Nations Economic Commission for Europe", listAgencyName="United Nations Economic Commission for Europe",
# DIAN 1.7.-2020: FAB17 # DIAN 1.7.-2020: FAB17
listSchemeURI=( listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1")
"urn:oasis:names:specification:ubl:codelist:gc:"
"CountryIdentificationCode-2.1"))
class DianZIP: class DianZIP:
@@ -682,11 +571,7 @@ class DianZIP:
class DianXMLExtensionSignerVerifier: class DianXMLExtensionSignerVerifier:
def __init__( def __init__(self, pkcs12_path_or_bytes, passphrase=None, localpolicy=True):
self,
pkcs12_path_or_bytes,
passphrase=None,
localpolicy=True):
self._pkcs12_path_or_bytes = pkcs12_path_or_bytes self._pkcs12_path_or_bytes = pkcs12_path_or_bytes
self._passphrase = None self._passphrase = None
self._localpolicy = localpolicy self._localpolicy = localpolicy
@@ -711,8 +596,8 @@ class DianXMLExtensionSignerVerifier:
if isinstance(self._pkcs12_path_or_bytes, str): if isinstance(self._pkcs12_path_or_bytes, str):
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read() pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
ctx = xades.XAdESContext() ctx = xades.XAdESContext()
ctx.load_pkcs12(pkcs12.load_key_and_certificates( ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data,
pkcs12_data, self._passphrase)) self._passphrase))
try: try:
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():
@@ -720,5 +605,5 @@ class DianXMLExtensionSignerVerifier:
else: else:
ctx.verify(signature) ctx.verify(signature)
return True return True
except BaseException: except:
return False return False

View File

@@ -5,14 +5,13 @@
# from functools import reduce # from functools import reduce
# import copy # import copy
from __future__ import annotations
import dataclasses import dataclasses
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime, date
# from collections import defaultdict # from collections import defaultdict
import decimal import decimal
from decimal import Decimal from decimal import Decimal
import typing
from ..data.dian import codelist from ..data.dian import codelist
DECIMAL_PRECISION = 6 DECIMAL_PRECISION = 6
@@ -61,9 +60,10 @@ class AmountCollection(Collection):
class Amount: class Amount:
def __init__( def __init__(
self, amount: int | float | str | "Amount", self, amount: int or float or str,
currency: Currency = Currency('COP')): currency: Currency = Currency('COP'),
precision=DECIMAL_PRECISION):
self.precision = precision
# DIAN 1.7.-2020: 1.2.3.1 # DIAN 1.7.-2020: 1.2.3.1
if isinstance(amount, Amount): if isinstance(amount, Amount):
if amount < Amount(0.0): if amount < Amount(0.0):
@@ -77,9 +77,10 @@ class Amount:
self.amount = Decimal( self.amount = Decimal(
amount, decimal.Context( amount, decimal.Context(
prec=DECIMAL_PRECISION, prec=self.precision,
# DIAN 1.7.-2020: 1.2.1.1 # DIAN 1.7.-2020: 1.2.1.1
rounding=decimal.ROUND_HALF_EVEN)) rounding=decimal.ROUND_HALF_EVEN)
)
self.currency = currency self.currency = currency
def fromNumber(self, val): def fromNumber(self, val):
@@ -97,20 +98,24 @@ class Amount:
def __lt__(self, other): def __lt__(self, other):
if not self.is_same_currency(other): if not self.is_same_currency(other):
raise AmountCurrencyError() raise AmountCurrencyError()
return round(self.amount, DECIMAL_PRECISION) < round(other, 2) return round(self.amount, self.precision) < round(other, 2)
def __eq__(self, other): def __eq__(self, other):
if not self.is_same_currency(other): if not self.is_same_currency(other):
raise AmountCurrencyError() raise AmountCurrencyError()
return round(self.amount, DECIMAL_PRECISION) == round( return round(self.amount, self.precision) == round(
other.amount, DECIMAL_PRECISION) other.amount, self.precision)
def _cast(self, val): def _cast(self, val):
if type(val) in [int, float]: if type(val) in [int, float]:
return self.fromNumber(val) return self.fromNumber(val)
if isinstance(val, Amount): if isinstance(val, Amount):
return val return val
raise TypeError("cant cast to amount")
if isinstance(val, Decimal):
return self.fromNumber(float(val))
raise TypeError("cant cast %s to amount" % (type(val)))
def __add__(self, rother): def __add__(self, rother):
other = self._cast(rother) other = self._cast(rother)
@@ -280,11 +285,22 @@ class Responsability:
raise ValueError("code %s not found" % (code)) 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 @dataclass
class Party: class Party:
name: str name: str
ident: str ident: str
responsability_code: list[Responsability] responsability_code: typing.List[Responsability]
responsability_regime_code: str responsability_regime_code: str
organization_code: str organization_code: str
tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01')) tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01'))
@@ -317,7 +333,7 @@ class TaxScheme:
@dataclass @dataclass
class TaxSubTotal: class TaxSubTotal:
percent: float percent: float
scheme: TaxScheme | None = None scheme: typing.Optional[TaxScheme] = None
tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline): def calculate(self, invline):
@@ -349,7 +365,7 @@ class TaxTotalOmit(TaxTotal):
@dataclass @dataclass
class WithholdingTaxSubTotal: class WithholdingTaxSubTotal:
percent: float percent: float
scheme: TaxScheme | None = None scheme: typing.Optional[TaxScheme] = None
tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline): def calculate(self, invline):
@@ -481,7 +497,7 @@ class AllowanceCharge:
reason: AllowanceChargeReason = None reason: AllowanceChargeReason = None
# Valor Base para calcular el descuento o el cargo # Valor Base para calcular el descuento o el cargo
base_amount: Amount | None = field( base_amount: typing.Optional[Amount] = field(
default_factory=lambda: Amount(0.0)) default_factory=lambda: Amount(0.0))
# Porcentaje: Porcentaje que aplicar. # Porcentaje: Porcentaje que aplicar.
@@ -526,9 +542,9 @@ class InvoiceLine:
# ya que al reportar los totales es sobre # ya que al reportar los totales es sobre
# la factura y el percent es unico por type_code # la factura y el percent es unico por type_code
# de subtotal # de subtotal
tax: TaxTotal | None tax: typing.Optional[TaxTotal]
withholding: WithholdingTaxTotal | None withholding: typing.Optional[WithholdingTaxTotal]
allowance_charge: list[AllowanceCharge] = dataclasses.field( allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(
default_factory=list) default_factory=list)
def add_allowance_charge(self, charge): def add_allowance_charge(self, charge):

View File

@@ -6,10 +6,7 @@ from .. import form
from ..fe import fe_from_string from ..fe import fe_from_string
from datetime import datetime 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 construye BillingReference desde XMLDOCUMENT
usando KLASS como clase. usando KLASS como clase.

View File

@@ -1,18 +1,8 @@
from .invoice import DIANInvoiceXML from .invoice import *
from .credit_note import DIANCreditNoteXML from .credit_note import *
from .debit_note import DIANDebitNoteXML from .debit_note import *
from .utils import DIANWrite, DIANWriteSigned from .utils import *
from .attached_document import AttachedDocument from .support_document import *
from .support_document import DIANSupportDocumentXML from .support_document_credit_note import *
from .support_document_credit_note import DIANSupportDocumentCreditNoteXML from .attached_document import *
from .application_response import *
__all__ = [
'DIANInvoiceXML',
'DIANCreditNoteXML',
'DIANDebitNoteXML',
'DIANWrite',
'DIANWriteSigned',
'AttachedDocument',
'DIANSupportDocumentXML',
'DIANSupportDocumentCreditNoteXML',
]

View File

@@ -0,0 +1,177 @@
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

View File

@@ -1,15 +1,227 @@
from .. import fe from .. import fe
from .application_response import ApplicationResponse
__all__ = ['AttachedDocument'] __all__ = ['AttachedDocument']
class AttachedDocument(): class AttachedDocument():
def __init__(self, id): def __init__(self, invoice, DIANInvoiceXML, id):
schema =\ self.schema =\
'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2' 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2'
self.fexml = fe.FeXML('AttachedDocument', schema) self.id = id
self.fexml.set_element('./cbc: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
def toFachoXML(self): def toFachoXML(self):
return self.fexml return self.fexml

View File

@@ -1,18 +1,10 @@
from collections import defaultdict
from .. import fe from .. import fe
from ..form import ( from ..form import *
Amount, from collections import defaultdict
CreditNoteDocumentReference, from .attached_document import AttachedDocument
DebitNoteDocumentReference,
InvoiceDocumentReference,
TaxTotalOmit,
WithholdingTaxTotalOmit,
)
__all__ = ['DIANInvoiceXML'] __all__ = ['DIANInvoiceXML']
class DIANInvoiceXML(fe.FeXML): class DIANInvoiceXML(fe.FeXML):
""" """
DianInvoiceXML mapea objeto form.Invoice a XML segun DianInvoiceXML mapea objeto form.Invoice a XML segun
@@ -20,32 +12,69 @@ class DIANInvoiceXML(fe.FeXML):
""" """
def __init__(self, invoice, tag_document='Invoice'): def __init__(self, invoice, tag_document='Invoice'):
super().__init__( super().__init__(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
tag_document, self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl')
'http://www.dian.gov.co/contratos/facturaelectronica/v1') self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource')
self.placeholder_for( self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider')
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode')
':DianExtensions/sts:InvoiceControl') self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:InvoiceSource')
self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:SoftwareProvider')
self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:SoftwareSecurityCode')
self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/'
'sts:DianExtensions/sts:AuthorizationProvider/'
'sts:AuthorizationProviderID')
# ZE02 se requiere existencia para firmar # ZE02 se requiere existencia para firmar
ublextension = self.fragment( ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True)
'./ext:UBLExtensions/ext:UBLExtension', append=True) extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
ublextension.find_or_create_element( self.attach_invoice = self.attach_invoice(invoice)
'/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice(invoice) # self.attach_document = self.attached_document_invoice(attach_invoice)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None"""
fexml.placeholder_for('./ext:UBLExtensions')
fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1')
fexml.set_element(
'./cbc:CustomizationID', invoice.invoice_operation_type)
fexml.placeholder_for('./cbc:ProfileID')
fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
# DIAN 1.7.-2020: FAD10
fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00'))
fexml.set_element(
'./cbc:%sTypeCode' % (fexml.tag_document()),
invoice.invoice_type_code,
listAgencyID='195',
listAgencyName='No matching global declaration available for the validation root',
listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
if fexml.tag_document() == 'Invoice':
fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (
fexml.tag_document()),
invoice.invoice_period_start.strftime(
'%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (
fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_billing_reference(invoice)
fexml.customize(invoice)
fexml.set_supplier(invoice)
fexml.set_customer(invoice)
fexml.set_payment_mean(invoice)
fexml.set_invoice_totals(invoice)
fexml.set_legal_monetary(invoice)
fexml.set_invoice_lines(invoice)
fexml.set_allowance_charge(invoice)
return fexml
def attached_document_invoice(fexml, invoice):
attach_invoice = fexml.attach_invoice(invoice)
attached_document = AttachedDocument(
invoice, '123', attach_invoice)
return attached_document
def set_supplier(fexml, invoice): def set_supplier(fexml, invoice):
fexml.placeholder_for('./cac:AccountingSupplierParty') fexml.placeholder_for('./cac:AccountingSupplierParty')
@@ -65,62 +94,54 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: CAJ07, CAJ08 # DIAN 1.7.-2020: CAJ07, CAJ08
# DIAN 1.7.-2020: FAJ07 # DIAN 1.7.-2020: FAJ07
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address')
':Address')
# DIAN 1.7.-2020: FAJ08 # DIAN 1.7.-2020: FAJ08
# DIAN 1.7.-2020: CAJ09 # DIAN 1.7.-2020: CAJ09
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
':Address/cbc:ID',
invoice.invoice_supplier.address.city.code) invoice.invoice_supplier.address.city.code)
# DIAN 1.7.-2020: FAJ09 # DIAN 1.7.-2020: FAJ09
# DIAN 1.7.-2020: CAJ10 # DIAN 1.7.-2020: CAJ10
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName',
':Address/cbc:CityName',
invoice.invoice_supplier.address.city.name) invoice.invoice_supplier.address.city.name)
# DIAN 1.7.-2020: FAJ11 # DIAN 1.7.-2020: FAJ11
# DIAN 1.7.-2020: CAJ11 # DIAN 1.7.-2020: CAJ11
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
':Address/cbc:CountrySubentity',
invoice.invoice_supplier.address.countrysubentity.name) invoice.invoice_supplier.address.countrysubentity.name)
# DIAN 1.7.-2020: FAJ12 # DIAN 1.7.-2020: FAJ12
# DIAN 1.7.-2020: CAJ12 # DIAN 1.7.-2020: CAJ12
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
':Address/cbc:CountrySubentityCode',
invoice.invoice_supplier.address.countrysubentity.code) invoice.invoice_supplier.address.countrysubentity.code)
# DIAN 1.7.-2020: FAJ14 # DIAN 1.7.-2020: FAJ14
# DIAN 1.7.-2020: CAJ13, CAJ14 # DIAN 1.7.-2020: CAJ13, CAJ14
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
':Address/cac:AddressLine/cbc:Line',
invoice.invoice_supplier.address.street) invoice.invoice_supplier.address.street)
# DIAN 1.7.-2020: FAJ16 # DIAN 1.7.-2020: FAJ16
# DIAN 1.7.-2020: CAJ16, CAJ16 # DIAN 1.7.-2020: CAJ16, CAJ16
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
':Address/cac:Country/cbc:IdentificationCode',
invoice.invoice_supplier.address.country.code) invoice.invoice_supplier.address.country.code)
# DIAN 1.7.-2020: FAJ17 # DIAN 1.7.-2020: FAJ17
# DIAN 1.7.-2020: CAJ17 # DIAN 1.7.-2020: CAJ17
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
':Address/cac:Country/cbc:Name',
invoice.invoice_supplier.address.country.name, invoice.invoice_supplier.address.country.name,
# DIAN 1.7.-2020: FAJ18 # DIAN 1.7.-2020: FAJ18
languageID='es') languageID='es')
supplier_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() supplier_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
supplier_company_id_attrs.update( supplier_company_id_attrs.update({
{
'schemeID': invoice.invoice_supplier.ident.dv, 'schemeID': invoice.invoice_supplier.ident.dv,
'schemeName': invoice.invoice_supplier.ident.type_fiscal}) 'schemeName': invoice.invoice_supplier.ident.type_fiscal
})
# DIAN 1.7.-2020: FAJ19 # DIAN 1.7.-2020: FAJ19
# DIAN 1.7.-2020: CAJ19 # DIAN 1.7.-2020: CAJ19
@@ -130,21 +151,18 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAJ20 # DIAN 1.7.-2020: FAJ20
# DIAN 1.7.-2020: CAJ20 # DIAN 1.7.-2020: CAJ20
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
':RegistrationName',
invoice.invoice_supplier.legal_name) invoice.invoice_supplier.legal_name)
# DIAN 1.7.-2020: FAJ21 # DIAN 1.7.-2020: FAJ21
# DIAN 1.7.-2020: CAJ21 # DIAN 1.7.-2020: CAJ21
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
':CompanyID',
invoice.invoice_supplier.ident, invoice.invoice_supplier.ident,
# DIAN 1.7.-2020: FAJ22,FAJ23,FAJ24,FAJ25 # DIAN 1.7.-2020: FAJ22,FAJ23,FAJ24,FAJ25
**supplier_company_id_attrs) **supplier_company_id_attrs)
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
':TaxLevelCode',
# DIAN 1.7.-2020: FAJ26 # DIAN 1.7.-2020: FAJ26
# DIAN 1.7.-2020: CAJ26 # DIAN 1.7.-2020: CAJ26
invoice.invoice_supplier.responsability_code, invoice.invoice_supplier.responsability_code,
@@ -154,76 +172,63 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAJ28 # DIAN 1.7.-2020: FAJ28
# DIAN 1.7.-2020: CAJ28 # DIAN 1.7.-2020: CAJ28
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress')
':RegistrationAddress')
# DIAN 1.7.-2020: FAJ29 # DIAN 1.7.-2020: FAJ29
# DIAN 1.7.-2020: CAJ29 # DIAN 1.7.-2020: CAJ29
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID',
':RegistrationAddress/cbc:ID',
invoice.invoice_supplier.address.city.code) invoice.invoice_supplier.address.city.code)
# DIAN 1.7.-2020: FAJ30 # DIAN 1.7.-2020: FAJ30
# DIAN 1.7.-2020: CAJ30 # DIAN 1.7.-2020: CAJ30
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName', invoice.invoice_supplier.address.city.name)
':RegistrationAddress/cbc:CityName',
invoice.invoice_supplier.address.city.name)
# DIAN 1.7.-2020: FAJ31 # DIAN 1.7.-2020: FAJ31
# DIAN 1.7.-2020: CAJ31 # DIAN 1.7.-2020: CAJ31
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity',
':RegistrationAddress/cbc:CountrySubentity',
invoice.invoice_supplier.address.countrysubentity.name) invoice.invoice_supplier.address.countrysubentity.name)
# DIAN 1.7.-2020: FAJ32 # DIAN 1.7.-2020: FAJ32
# DIAN 1.7.-2020: CAJ32 # DIAN 1.7.-2020: CAJ32
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode',
':RegistrationAddress/cbc:CountrySubentityCode',
invoice.invoice_supplier.address.countrysubentity.code) invoice.invoice_supplier.address.countrysubentity.code)
# DIAN 1.7.-2020: FAJ33,FAJ34 # DIAN 1.7.-2020: FAJ33,FAJ34
# DIAN 1.7.-2020: CAJ33,CAJ34 # DIAN 1.7.-2020: CAJ33,CAJ34
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line',
':RegistrationAddress/cac:AddressLine/cbc:Line',
invoice.invoice_supplier.address.street) invoice.invoice_supplier.address.street)
# DIAN 1.7.-2020: FAJ35,FAJ36 # DIAN 1.7.-2020: FAJ35,FAJ36
# DIAN 1.7.-2020: CAJ35,CAJ36 # DIAN 1.7.-2020: CAJ35,CAJ36
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':RegistrationAddress/cac:Country/cbc:IdentificationCode',
invoice.invoice_supplier.address.country.code) invoice.invoice_supplier.address.country.code)
# DIAN 1.7.-2020: FAJ37,FAJ38 # DIAN 1.7.-2020: FAJ37,FAJ38
# DIAN 1.7.-2020: CAJ37,CAJ38 # DIAN 1.7.-2020: CAJ37,CAJ38
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name',
':RegistrationAddress/cac:Country/cbc:Name',
invoice.invoice_supplier.address.country.name, invoice.invoice_supplier.address.country.name,
languageID='es') languageID='es')
# DIAN 1.7.-2020: FAJ39 # DIAN 1.7.-2020: FAJ39
# DIAN 1.7.-2020: CAJ39 # DIAN 1.7.-2020: CAJ39
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
':TaxScheme')
# DIAN 1.7.-2020: CAJ40 # DIAN 1.7.-2020: CAJ40
# DIAN 1.7.-2020: FAJ40 # DIAN 1.7.-2020: FAJ40
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
':TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code) invoice.invoice_customer.tax_scheme.code)
# DIAN 1.7.-2020: CAJ41 # DIAN 1.7.-2020: CAJ41
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
':TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name) invoice.invoice_customer.tax_scheme.name)
# DIAN 1.7.-2020: FAJ42 # DIAN 1.7.-2020: FAJ42
@@ -234,29 +239,25 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAJ43 # DIAN 1.7.-2020: FAJ43
# DIAN 1.7.-2020: CAJ43 # DIAN 1.7.-2020: CAJ43
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName',
':RegistrationName',
invoice.invoice_supplier.legal_name) invoice.invoice_supplier.legal_name)
# DIAN 1.7.-2020: FAJ44,FAJ45,FAJ46,FAJ47,FAJ48 # DIAN 1.7.-2020: FAJ44,FAJ45,FAJ46,FAJ47,FAJ48
# DIAN 1.7.-2020: CAJ44,CAJ45,CAJ46,CAJ47,CAJ48 # DIAN 1.7.-2020: CAJ44,CAJ45,CAJ46,CAJ47,CAJ48
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
':CompanyID',
invoice.invoice_supplier.ident, invoice.invoice_supplier.ident,
**supplier_company_id_attrs) **supplier_company_id_attrs)
# DIAN 1.7.-2020: FAJ49 # DIAN 1.7.-2020: FAJ49
# DIAN 1.7.-2020: CAJ49 # DIAN 1.7.-2020: CAJ49
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme')
':CorporateRegistrationScheme')
# DIAN 1.7.-2020: FAJ50 # DIAN 1.7.-2020: FAJ50
# DIAN 1.7.-2020: CAJ50 # DIAN 1.7.-2020: CAJ50
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme/cbc:ID',
':CorporateRegistrationScheme/cbc:ID',
invoice.invoice_ident_prefix) invoice.invoice_ident_prefix)
# DIAN 1.7.-2020: CAJ67 # DIAN 1.7.-2020: CAJ67
@@ -266,8 +267,7 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAJ71 # DIAN 1.7.-2020: FAJ71
# DIAN 1.7.-2020: CAJ71 # DIAN 1.7.-2020: CAJ71
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc' './cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicMail',
':ElectronicMail',
invoice.invoice_supplier.email) invoice.invoice_supplier.email)
def set_customer(fexml, invoice): def set_customer(fexml, invoice):
@@ -276,8 +276,7 @@ class DIANInvoiceXML(fe.FeXML):
'./cac:AccountingCustomerParty/cbc:AdditionalAccountID', './cac:AccountingCustomerParty/cbc:AdditionalAccountID',
invoice.invoice_customer.organization_code) invoice.invoice_customer.organization_code)
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification' './cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID',
'/cbc:ID',
invoice.invoice_customer.ident) invoice.invoice_customer.ident)
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name', './cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name',
@@ -288,62 +287,52 @@ class DIANInvoiceXML(fe.FeXML):
customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
# DIAN 1.7.-2020: FAK25 # DIAN 1.7.-2020: FAK25
# DIAN 1.7.-2020: CAK25 # DIAN 1.7.-2020: CAK25
customer_company_id_attrs.update( customer_company_id_attrs.update({
{
'schemeID': invoice.invoice_customer.ident.dv, 'schemeID': invoice.invoice_customer.ident.dv,
'schemeName': invoice.invoice_customer.ident.type_fiscal}) 'schemeName': invoice.invoice_customer.ident.type_fiscal})
# DIAN 1.7.-2020: FAK07 # DIAN 1.7.-2020: FAK07
# DIAN 1.7.-2020: CAK07 # DIAN 1.7.-2020: CAK07
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address')
':Address')
# DIAN 1.7.-2020: FAK08 # DIAN 1.7.-2020: FAK08
# DIAN 1.7.-2020: CAK08 # DIAN 1.7.-2020: CAK08
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
':Address/cbc:ID',
invoice.invoice_customer.address.city.code) invoice.invoice_customer.address.city.code)
# DIAN 1.7.-2020: FAK09 # DIAN 1.7.-2020: FAK09
# DIAN 1.7.-2020: CAK09 # DIAN 1.7.-2020: CAK09
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', invoice.invoice_customer.address.city.name)
':Address/cbc:CityName',
invoice.invoice_customer.address.city.name)
# DIAN 1.7.-2020: FAK11 # DIAN 1.7.-2020: FAK11
# DIAN 1.7.-2020: CAK11 # DIAN 1.7.-2020: CAK11
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
':Address/cbc:CountrySubentity',
invoice.invoice_customer.address.countrysubentity.name) invoice.invoice_customer.address.countrysubentity.name)
# DIAN 1.7.-2020: FAK12 # DIAN 1.7.-2020: FAK12
# DIAN 1.7.-2020: CAK12 # DIAN 1.7.-2020: CAK12
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
':Address/cbc:CountrySubentityCode',
invoice.invoice_customer.address.countrysubentity.code) invoice.invoice_customer.address.countrysubentity.code)
# DIAN 1.7.-2020: CAK13, CAK14 # DIAN 1.7.-2020: CAK13, CAK14
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
':Address/cac:AddressLine/cbc:Line',
invoice.invoice_customer.address.street) invoice.invoice_customer.address.street)
# DIAN 1.7.-2020: CAK16 # DIAN 1.7.-2020: CAK16
# DIAN 1.7.-2020: FAK16 # DIAN 1.7.-2020: FAK16
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
':Address/cac:Country/cbc:IdentificationCode',
invoice.invoice_customer.address.country.code) invoice.invoice_customer.address.country.code)
# DIAN 1.7.-2020: FAK17 # DIAN 1.7.-2020: FAK17
# DIAN 1.7.-2020: CAK17 # DIAN 1.7.-2020: CAK17
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
':Address/cac:Country/cbc:Name',
invoice.invoice_customer.address.country.name, invoice.invoice_customer.address.country.name,
# DIAN 1.7.-2020: FAK18 # DIAN 1.7.-2020: FAK18
# DIAN 1.7.-2020: CAK18 # DIAN 1.7.-2020: CAK18
@@ -357,23 +346,20 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAK17,FAK20 # DIAN 1.7.-2020: FAK17,FAK20
# DIAN 1.7.-2020: CAK20 # DIAN 1.7.-2020: CAK20
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
':RegistrationName',
invoice.invoice_customer.legal_name) invoice.invoice_customer.legal_name)
# DIAN 1.7.-2020: CAK21 # DIAN 1.7.-2020: CAK21
# DIAN 1.7.-2020: FAK21 # DIAN 1.7.-2020: FAK21
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
':CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
# DIAN 1.7.-2020: CAK22, CAK23, CAK24, CAK25 # DIAN 1.7.-2020: CAK22, CAK23, CAK24, CAK25
**customer_company_id_attrs) **customer_company_id_attrs)
# DIAN 1.7.-2020: CAK26 # DIAN 1.7.-2020: CAK26
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
':TaxLevelCode',
# DIAN 1.7.-2020: FAK26 # DIAN 1.7.-2020: FAK26
invoice.invoice_customer.responsability_code, invoice.invoice_customer.responsability_code,
# DIAN 1.7.-2020: FAK27 # DIAN 1.7.-2020: FAK27
@@ -383,97 +369,83 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAK28 # DIAN 1.7.-2020: FAK28
# DIAN 1.7.-2020: CAK28 # DIAN 1.7.-2020: CAK28
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress')
':RegistrationAddress')
# DIAN 1.7.-2020: FAK29 # DIAN 1.7.-2020: FAK29
# DIAN 1.7.-2020: CAK29 # DIAN 1.7.-2020: CAK29
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID',
':RegistrationAddress/cbc:ID',
invoice.invoice_customer.address.city.code) invoice.invoice_customer.address.city.code)
# DIAN 1.7.-2020: FAK30 # DIAN 1.7.-2020: FAK30
# DIAN 1.7.-2020: CAK30 # DIAN 1.7.-2020: CAK30
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName',
':RegistrationAddress/cbc:CityName',
invoice.invoice_customer.address.city.name) invoice.invoice_customer.address.city.name)
# DIAN 1.7.-2020: FAK31 # DIAN 1.7.-2020: FAK31
# DIAN 1.7.-2020: CAK31 # DIAN 1.7.-2020: CAK31
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity',
':RegistrationAddress/cbc:CountrySubentity',
invoice.invoice_customer.address.countrysubentity.name) invoice.invoice_customer.address.countrysubentity.name)
# DIAN 1.7.-2020: FAK32 # DIAN 1.7.-2020: FAK32
# DIAN 1.7.-2020: CAK32 # DIAN 1.7.-2020: CAK32
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode',
':RegistrationAddress/cbc:CountrySubentityCode',
invoice.invoice_customer.address.countrysubentity.code) invoice.invoice_customer.address.countrysubentity.code)
# DIAN 1.7.-2020: FAK33 # DIAN 1.7.-2020: FAK33
# DIAN 1.7.-2020: CAK33 # DIAN 1.7.-2020: CAK33
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine')
':RegistrationAddress/cac:AddressLine')
# DIAN 1.7.-2020: FAK34 # DIAN 1.7.-2020: FAK34
# DIAN 1.7.-2020: CAK34 # DIAN 1.7.-2020: CAK34
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line',
':RegistrationAddress/cac:AddressLine/cbc:Line',
invoice.invoice_customer.address.street) invoice.invoice_customer.address.street)
# DIAN 1.7.-2020: CAK35 # DIAN 1.7.-2020: CAK35
# DIAN 1.7.-2020: FAK35 # DIAN 1.7.-2020: FAK35
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country')
':RegistrationAddress/cac:Country')
# DIAN 1.7.-2020: CAK36 # DIAN 1.7.-2020: CAK36
# DIAN 1.7.-2020: FAK36 # DIAN 1.7.-2020: FAK36
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
':RegistrationAddress/cac:Country/cbc:IdentificationCode',
invoice.invoice_customer.address.country.code) invoice.invoice_customer.address.country.code)
# DIAN 1.7.-2020: CAK37 # DIAN 1.7.-2020: CAK37
# DIAN 1.7.-2020: FAK37 # DIAN 1.7.-2020: FAK37
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name', invoice.invoice_customer.address.country.name)
':RegistrationAddress/cac:Country/cbc:Name',
invoice.invoice_customer.address.country.name)
# DIAN 1.7.-2020: FAK38 # DIAN 1.7.-2020: FAK38
# DIAN 1.7.-2020: CAK38 # DIAN 1.7.-2020: CAK38
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
':RegistrationAddress/cac:Country/cbc:IdentificationCode',
invoice.invoice_customer.address.country.code, invoice.invoice_customer.address.country.code,
languageID='es') languageID='es')
# DIAN 1.7.-2020: CAK39 # DIAN 1.7.-2020: CAK39
# DIAN 1.7.-2020: FAK39 # DIAN 1.7.-2020: FAK39
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
':TaxScheme')
# DIAN 1.7.-2020: CAK40 Machete Construir Validación # DIAN 1.7.-2020: CAK40 Machete Construir Validación
# DIAN 1.7.-2020: FAK40 # DIAN 1.7.-2020: FAK40
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
':TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code) invoice.invoice_customer.tax_scheme.code)
# DIAN 1.7.-2020: FAK41 # DIAN 1.7.-2020: FAK41
# DIAN 1.7.-2020: CAK41 Machete Construir Validación # DIAN 1.7.-2020: CAK41 Machete Construir Validación
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
':TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name) invoice.invoice_customer.tax_scheme.name)
# DIAN 1.7.-2020: FAK42 # DIAN 1.7.-2020: FAK42
# DIAN 1.7.-2020: CAK42 # DIAN 1.7.-2020: CAK42
fexml.placeholder_for( fexml.placeholder_for(
@@ -482,15 +454,13 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAK43 # DIAN 1.7.-2020: FAK43
# DIAN 1.7.-2020: CAK43 # DIAN 1.7.-2020: CAK43
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName',
':RegistrationName',
invoice.invoice_customer.legal_name) invoice.invoice_customer.legal_name)
# DIAN 1.7.-2020: CAK44 # DIAN 1.7.-2020: CAK44
# DIAN 1.7.-2020: FAK44,FAK45,FAK46,FAK47,FAK48 # DIAN 1.7.-2020: FAK44,FAK45,FAK46,FAK47,FAK48
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
':CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
**customer_company_id_attrs) **customer_company_id_attrs)
@@ -500,22 +470,19 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAK55 # DIAN 1.7.-2020: FAK55
# DIAN 1.7.-2020: CAK51, CAK55 # DIAN 1.7.-2020: CAK51, CAK55
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc' './cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicMail',
':ElectronicMail',
invoice.invoice_customer.email) invoice.invoice_customer.email)
def set_payment_mean(fexml, invoice): def set_payment_mean(fexml, invoice):
payment_mean = invoice.invoice_payment_mean payment_mean = invoice.invoice_payment_mean
fexml.set_element('./cac:PaymentMeans/cbc:ID', payment_mean.id)
fexml.set_element( fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentMeansCode', './cac:PaymentMeans/cbc:ID', payment_mean.id)
payment_mean.code)
fexml.set_element( fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentDueDate', './cac:PaymentMeans/cbc:PaymentMeansCode', payment_mean.code)
payment_mean.due_at.strftime('%Y-%m-%d'))
fexml.set_element( fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentID', './cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d'))
payment_mean.payment_id) fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentID', payment_mean.payment_id)
def set_element_amount_for(fexml, xml, xpath, amount): def set_element_amount_for(fexml, xml, xpath, amount):
if not isinstance(amount, Amount): if not isinstance(amount, Amount):
@@ -530,48 +497,38 @@ class DIANInvoiceXML(fe.FeXML):
fexml.set_element(xpath, amount, currencyID=amount.currency.code) fexml.set_element(xpath, amount, currencyID=amount.currency.code)
def set_legal_monetary(fexml, invoice): def set_legal_monetary(fexml, invoice):
fexml.set_element_amount( fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:LineExtensionAmount',
'./cac:LegalMonetaryTotal/cbc:LineExtensionAmount',
invoice.invoice_legal_monetary_total.line_extension_amount) invoice.invoice_legal_monetary_total.line_extension_amount)
fexml.set_element_amount( fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
'./cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
invoice.invoice_legal_monetary_total.tax_exclusive_amount) invoice.invoice_legal_monetary_total.tax_exclusive_amount)
fexml.set_element_amount( fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
'./cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
invoice.invoice_legal_monetary_total.tax_inclusive_amount) invoice.invoice_legal_monetary_total.tax_inclusive_amount)
fexml.set_element_amount( fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:ChargeTotalAmount',
'./cac:LegalMonetaryTotal/cbc:ChargeTotalAmount',
invoice.invoice_legal_monetary_total.charge_total_amount) invoice.invoice_legal_monetary_total.charge_total_amount)
fexml.set_element_amount( fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:PayableAmount',
'./cac:LegalMonetaryTotal/cbc:PayableAmount',
invoice.invoice_legal_monetary_total.payable_amount) invoice.invoice_legal_monetary_total.payable_amount)
def _set_invoice_document_reference(fexml, reference): def _set_invoice_document_reference(fexml, reference):
fexml._do_set_billing_reference( fexml._do_set_billing_reference(reference, 'cac:InvoiceDocumentReference')
reference, 'cac:InvoiceDocumentReference')
def _set_credit_note_document_reference(fexml, reference): def _set_credit_note_document_reference(fexml, reference):
fexml._do_set_billing_reference( fexml._do_set_billing_reference(reference, 'cac:CreditNoteDocumentReference')
reference, 'cac:CreditNoteDocumentReference')
def _set_debit_note_document_reference(fexml, reference): def _set_debit_note_document_reference(fexml, reference):
fexml._do_set_billing_reference( fexml._do_set_billing_reference(reference, 'cac:DebitNoteDocumentReference')
reference, 'cac:DebitNoteDocumentReference')
def _do_set_billing_reference(fexml, reference, tag_document): def _do_set_billing_reference(fexml, reference, tag_document):
fexml.set_element('./cac:BillingReference/%s/cbc:ID' %(tag_document), fexml.set_element('./cac:BillingReference/%s/cbc:ID' %(tag_document),
reference.ident) reference.ident)
fexml.set_element( fexml.set_element('./cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID',
'./cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID',
reference.uuid, reference.uuid,
schemeName='CUFE-SHA384') schemeName='CUFE-SHA384')
fexml.set_element( fexml.set_element('./cac:BillingReference/cac:InvoiceDocumentReference/cbc:IssueDate',
'./cac:BillingReference/cac:InvoiceDocumentReference/'
'cbc:IssueDate',
reference.date.strftime("%Y-%m-%d")) reference.date.strftime("%Y-%m-%d"))
def set_billing_reference(fexml, invoice): def set_billing_reference(fexml, invoice):
@@ -589,8 +546,7 @@ class DIANInvoiceXML(fe.FeXML):
def set_invoice_totals(fexml, invoice): def set_invoice_totals(fexml, invoice):
tax_amount_for = defaultdict(lambda: defaultdict(lambda: Amount(0.0))) tax_amount_for = defaultdict(lambda: defaultdict(lambda: Amount(0.0)))
withholding_amount_for = defaultdict( withholding_amount_for = defaultdict(lambda: defaultdict(lambda: Amount(0.0)))
lambda: defaultdict(lambda: Amount(0.0)))
percent_for = defaultdict(lambda: None) percent_for = defaultdict(lambda: None)
#requeridos para CUFE #requeridos para CUFE
@@ -608,12 +564,9 @@ class DIANInvoiceXML(fe.FeXML):
for invoice_line in invoice.invoice_lines: for invoice_line in invoice.invoice_lines:
for subtotal in invoice_line.tax.subtotals: for subtotal in invoice_line.tax.subtotals:
if subtotal.scheme is not None: if subtotal.scheme is not None:
scheme = subtotal.scheme.code tax_amount_for[subtotal.scheme.code]['tax_amount'] += subtotal.tax_amount
tax_amount_for[scheme][ tax_amount_for[subtotal.scheme.code]['taxable_amount'] += invoice_line.taxable_amount
'tax_amount'] += subtotal.tax_amount tax_amount_for[subtotal.scheme.code]['name'] = subtotal.scheme.name
tax_amount_for[scheme][
'taxable_amount'] += invoice_line.taxable_amount
tax_amount_for[scheme]['name'] = subtotal.scheme.name
# MACHETE ojo InvoiceLine.tax pasar a Invoice # MACHETE ojo InvoiceLine.tax pasar a Invoice
percent_for[subtotal.scheme.code] = subtotal.percent percent_for[subtotal.scheme.code] = subtotal.percent
@@ -622,16 +575,12 @@ class DIANInvoiceXML(fe.FeXML):
for subtotal_withholding in invoice_line.withholding.subtotals: for subtotal_withholding in invoice_line.withholding.subtotals:
if subtotal_withholding.scheme is not None: if subtotal_withholding.scheme is not None:
scheme = subtotal_withholding.scheme.code withholding_amount_for[subtotal_withholding.scheme.code]['tax_amount'] += subtotal_withholding.tax_amount
withholding_amount_for[scheme][ withholding_amount_for[subtotal_withholding.scheme.code]['taxable_amount'] += invoice_line.withholding_taxable_amount
'tax_amount'] += subtotal_withholding.tax_amount
withholding_amount_for[scheme][
'taxable_amount'] += (
invoice_line.withholding_taxable_amount)
# MACHETE ojo InvoiceLine.tax pasar a Invoice # MACHETE ojo InvoiceLine.tax pasar a Invoice
percent_for[scheme] = subtotal_withholding.percent percent_for[subtotal_withholding.scheme.code] = subtotal_withholding.percent
total_withholding_amount += subtotal_withholding.tax_amount total_withholding_amount += subtotal_withholding.tax_amount
@@ -653,14 +602,12 @@ class DIANInvoiceXML(fe.FeXML):
tax_amount) tax_amount)
#DIAN 1.7.-2020: FAS05 #DIAN 1.7.-2020: FAS05
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', '/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
amount_of['taxable_amount']) amount_of['taxable_amount'])
#DIAN 1.7.-2020: FAU06 #DIAN 1.7.-2020: FAU06
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', '/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
amount_of['tax_amount']) amount_of['tax_amount'])
@@ -669,19 +616,14 @@ class DIANInvoiceXML(fe.FeXML):
line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cbc:Percent', line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
line.set_element( line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/'
'cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element( 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) cod_impuesto)
line.set_element( line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
'/cbc:Name',
amount_of['name']) amount_of['name'])
for index, item in enumerate(withholding_amount_for.items()): for index, item in enumerate(withholding_amount_for.items()):
@@ -690,12 +632,13 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAS01 # DIAN 1.7.-2020: FAS01
line = fexml.fragment( line = fexml.fragment(
'./cac:WithholdingTaxTotal', './cac:WithholdingTaxTotal', append=next_append)
append=next_append)
# DIAN 1.7.-2020: FAU06 # DIAN 1.7.-2020: FAU06
tax_amount = amount_of['tax_amount'] tax_amount = amount_of['tax_amount']
fexml.set_element_amount_for( fexml.set_element_amount_for(
line, '/cac:WithholdingTaxTotal/cbc:TaxAmount', tax_amount) line,
'/cac:WithholdingTaxTotal/cbc:TaxAmount',
tax_amount)
# DIAN 1.7.-2020: FAS05 # DIAN 1.7.-2020: FAS05
fexml.set_element_amount_for( fexml.set_element_amount_for(
@@ -717,17 +660,14 @@ class DIANInvoiceXML(fe.FeXML):
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
line.set_element( line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory' '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
'/cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element( line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac' '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
':TaxScheme/cbc:ID',
cod_impuesto) cod_impuesto)
line.set_element( line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac' '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
':TaxScheme/cbc:Name',
'ReteRenta') 'ReteRenta')
# abstract method # abstract method
@@ -739,7 +679,8 @@ class DIANInvoiceXML(fe.FeXML):
return 'Invoiced' return 'Invoiced'
def set_invoice_line_tax(fexml, line, invoice_line): def set_invoice_line_tax(fexml, line, invoice_line):
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
line,
'./cac:TaxTotal/cbc:TaxAmount', './cac:TaxTotal/cbc:TaxAmount',
invoice_line.tax_amount) invoice_line.tax_amount)
@@ -748,6 +689,7 @@ class DIANInvoiceXML(fe.FeXML):
line, line,
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', './cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.taxable_amount) invoice_line.taxable_amount)
for subtotal in invoice_line.tax.subtotals: for subtotal in invoice_line.tax.subtotals:
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', './cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
@@ -756,28 +698,21 @@ class DIANInvoiceXML(fe.FeXML):
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc' './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2))
':Percent',
'%0.2f' %
round(
subtotal.percent,
2))
if subtotal.scheme is not None: if subtotal.scheme is not None:
# DIAN 1.7.-2020: FAX15 # DIAN 1.7.-2020: FAX15
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac' './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code)
':TaxScheme/cbc:ID',
subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac' './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', subtotal.scheme.name)
':TaxScheme/cbc:Name',
subtotal.scheme.name)
def set_invoice_line_withholding(fexml, line, invoice_line): def set_invoice_line_withholding(fexml, line, invoice_line):
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
line,
'./cac:WithholdingTaxTotal/cbc:TaxAmount', './cac:WithholdingTaxTotal/cbc:TaxAmount',
invoice_line.withholding_amount) invoice_line.withholding_amount)
# DIAN 1.7.-2020: FAX05 # DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for( fexml.set_element_amount_for(
line, line,
@@ -792,54 +727,44 @@ class DIANInvoiceXML(fe.FeXML):
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac' './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2))
':TaxCategory/cbc:Percent',
'%0.2f' %
round(
subtotal.percent,
2))
if subtotal.scheme is not None: if subtotal.scheme is not None:
# DIAN 1.7.-2020: FAX15 # DIAN 1.7.-2020: FAX15
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac' './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code)
':TaxCategory/cac:TaxScheme/cbc:ID',
subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac' './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', subtotal.scheme.name)
':TaxCategory/cac:TaxScheme/cbc:Name',
subtotal.scheme.name)
def set_invoice_lines(fexml, invoice): def set_invoice_lines(fexml, invoice):
next_append = False next_append = False
for index, invoice_line in enumerate(invoice.invoice_lines): for index, invoice_line in enumerate(invoice.invoice_lines):
line = fexml.fragment( line = fexml.fragment(
'./cac:%sLine' % './cac:%sLine' % (fexml.tag_document()),
(fexml.tag_document()),
append=next_append) append=next_append)
next_append = True next_append = True
line.set_element('./cbc:ID', index + 1) line.set_element('./cbc:ID', index + 1)
line.set_element( line.set_element(
'./cbc:%sQuantity' % './cbc:%sQuantity' % (
(fexml.tag_document_concilied()), fexml.tag_document_concilied()),
invoice_line.quantity, invoice_line.quantity,
unitCode='NAR') unitCode='NAR')
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
line,
'./cbc:LineExtensionAmount', './cbc:LineExtensionAmount',
invoice_line.total_amount) invoice_line.total_amount)
if not isinstance(invoice_line.tax, TaxTotalOmit): if not isinstance(
invoice_line.tax, TaxTotalOmit):
fexml.set_invoice_line_tax(line, invoice_line) fexml.set_invoice_line_tax(line, invoice_line)
if not isinstance( if not isinstance(
invoice_line.withholding, invoice_line.withholding, WithholdingTaxTotalOmit):
WithholdingTaxTotalOmit):
fexml.set_invoice_line_withholding(line, invoice_line) fexml.set_invoice_line_withholding(line, invoice_line)
line.set_element( line.set_element(
'./cac:Item/cbc:Description', './cac:Item/cbc:Description', invoice_line.item.description)
invoice_line.item.description)
line.set_element( line.set_element(
'./cac:Item/cac:StandardItemIdentification/cbc:ID', './cac:Item/cac:StandardItemIdentification/cbc:ID',
@@ -852,8 +777,10 @@ class DIANInvoiceXML(fe.FeXML):
'./cac:Price/cbc:PriceAmount', './cac:Price/cbc:PriceAmount',
invoice_line.price.amount, invoice_line.price.amount,
currencyID=invoice_line.price.amount.currency.code) currencyID=invoice_line.price.amount.currency.code)
# DIAN 1.7.-2020: FBB04 # DIAN 1.7.-2020: FBB04
line.set_element('./cac:Price/cbc:BaseQuantity', line.set_element(
'./cac:Price/cbc:BaseQuantity',
invoice_line.price.quantity, invoice_line.price.quantity,
unitCode=invoice_line.quantity.code) unitCode=invoice_line.quantity.code)
@@ -888,56 +815,6 @@ class DIANInvoiceXML(fe.FeXML):
fexml.set_element_amount_for( fexml.set_element_amount_for(
line, './cbc:BaseAmount', charge.base_amount) line, './cbc:BaseAmount', charge.base_amount)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None"""
fexml.placeholder_for('./ext:UBLExtensions')
fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1')
fexml.set_element(
'./cbc:CustomizationID',
invoice.invoice_operation_type)
fexml.placeholder_for('./cbc:ProfileID')
fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID')
fexml.set_element(
'./cbc:IssueDate',
invoice.invoice_issue.strftime('%Y-%m-%d'))
# DIAN 1.7.-2020: FAD10
fexml.set_element(
'./cbc:IssueTime',
invoice.invoice_issue.strftime('%H:%M:%S-05:00'))
fexml.set_element(
'./cbc:%sTypeCode' %
(fexml.tag_document()),
invoice.invoice_type_code,
listAgencyID='195',
listAgencyName=(
'No matching global declaration available for the '
'validation root'),
listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
if fexml.tag_document() == 'Invoice':
fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (
fexml.tag_document()),
invoice.invoice_period_start.strftime('%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (
fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_billing_reference(invoice)
fexml.customize(invoice)
fexml.set_supplier(invoice)
fexml.set_customer(invoice)
fexml.set_payment_mean(invoice)
fexml.set_invoice_totals(invoice)
fexml.set_legal_monetary(invoice)
fexml.set_invoice_lines(invoice)
fexml.set_allowance_charge(invoice)
return fexml
def customize(fexml, invoice): def customize(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML """adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None""" en caso de fallar validacion retorna None"""

View File

@@ -14,45 +14,36 @@ __all__ = ['DIANSupportDocumentXML']
class DIANSupportDocumentXML(fe.FeXML): class DIANSupportDocumentXML(fe.FeXML):
""" """
DianSupportDocumentXML mapea objeto form.Invoice a XML segun DianSupportDocumentXML mapea objeto form.Invoice a XML segun
lo indicado para él Documento soporte en adquisiciones efectuadas con lo indicado para él Documento soporte en adquisiciones efectuadas con sujetos no obligados a expedir factura de venta o documento equivalente.
sujetos no obligados a expedir factura de venta o documento equivalente.
""" """
def __init__(self, invoice, tag_document='Invoice'): def __init__(self, invoice, tag_document='Invoice'):
super().__init__( super().__init__(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
tag_document,
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
# DIAN 1.1.-2021: DSAB03 # DIAN 1.1.-2021: DSAB03
# DIAN 1.1.-2021: NSAB03 # DIAN 1.1.-2021: NSAB03
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl')
':DianExtensions/sts:InvoiceControl')
# DIAN 1.1.-2021: DSAB13 # DIAN 1.1.-2021: DSAB13
# DIAN 1.1.-2021: NSAB13 # DIAN 1.1.-2021: NSAB13
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource')
':DianExtensions/sts:InvoiceSource')
# DIAN 1.1.-2021: DSAB18 # DIAN 1.1.-2021: DSAB18
# DIAN 1.1.-2021: NSAB18 # DIAN 1.1.-2021: NSAB18
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider')
':DianExtensions/sts:SoftwareProvider')
# DIAN 1.1.-2021: DSAB27 # DIAN 1.1.-2021: DSAB27
# DIAN 1.1.-2021: NSAB27 # DIAN 1.1.-2021: NSAB27
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode')
':DianExtensions/sts:SoftwareSecurityCode')
# DIAN 1.1.-2021: DSAB30 DSAB31 # DIAN 1.1.-2021: DSAB30 DSAB31
# DIAN 1.1.-2021: NSAB30 NSAB31 # DIAN 1.1.-2021: NSAB30 NSAB31
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/' './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
'sts:DianExtensions/sts:AuthorizationProvider/'
'sts:AuthorizationProviderID')
# ZE02 se requiere existencia para firmar # ZE02 se requiere existencia para firmar
# DIAN 1.1.-2021: DSAA02 DSAB01 # DIAN 1.1.-2021: DSAA02 DSAB01
@@ -61,7 +52,7 @@ class DIANSupportDocumentXML(fe.FeXML):
'./ext:UBLExtensions/ext:UBLExtension', append=True) './ext:UBLExtensions/ext:UBLExtension', append=True)
# DIAN 1.1.-2021: DSAB02 # DIAN 1.1.-2021: DSAB02
# DIAN 1.1.-2021: NSAB02 # DIAN 1.1.-2021: NSAB02
ublextension.find_or_create_element( extcontent = ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent') '/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice(invoice) self.attach_invoice(invoice)
@@ -79,61 +70,51 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAJ07 DSAJ08 # DIAN 1.1.-2021: DSAJ07 DSAJ08
# DIAN 1.1.-2021: NSAJ07 NSAJ08 # DIAN 1.1.-2021: NSAJ07 NSAJ08
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address')
':Address')
# DIAN 1.1.-2021: DSAJ09 # DIAN 1.1.-2021: DSAJ09
# DIAN 1.1.-2021: NSAJ09 # DIAN 1.1.-2021: NSAJ09
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
':Address/cbc:ID',
invoice.invoice_supplier.address.city.code) invoice.invoice_supplier.address.city.code)
# DIAN 1.1.-2021: DSAJ10 # DIAN 1.1.-2021: DSAJ10
# DIAN 1.1.-2021: NSAJ10 # DIAN 1.1.-2021: NSAJ10
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName',
':Address/cbc:CityName',
invoice.invoice_supplier.address.city.name) invoice.invoice_supplier.address.city.name)
# DIAN 1.1.-2021: DSAJ73 # DIAN 1.1.-2021: DSAJ73
# DIAN 1.1.-2021: NSAJ73 # DIAN 1.1.-2021: NSAJ73
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:PostalZone',
':Address/cbc:PostalZone',
invoice.invoice_supplier.address.postalzone.code) invoice.invoice_supplier.address.postalzone.code)
# DIAN 1.1.-2021: DSAJ11 # DIAN 1.1.-2021: DSAJ11
# DIAN 1.1.-2021: NSAJ11 # DIAN 1.1.-2021: NSAJ11
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
':Address/cbc:CountrySubentity',
invoice.invoice_supplier.address.countrysubentity.name) invoice.invoice_supplier.address.countrysubentity.name)
# DIAN 1.1.-2021: DSAJ12 # DIAN 1.1.-2021: DSAJ12
# DIAN 1.1.-2021: NSAJ12 # DIAN 1.1.-2021: NSAJ12
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
':Address/cbc:CountrySubentityCode',
invoice.invoice_supplier.address.countrysubentity.code) invoice.invoice_supplier.address.countrysubentity.code)
# DIAN 1.1.-2021: NSAJ13 NSAJ14 # DIAN 1.1.-2021: NSAJ13 NSAJ14
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
':Address/cac:AddressLine/cbc:Line',
invoice.invoice_supplier.address.street) invoice.invoice_supplier.address.street)
# DIAN 1.1.-2021: DSAJ15 DSAJ16 # DIAN 1.1.-2021: DSAJ15 DSAJ16
# DIAN 1.1.-2021: NSAJ15 NSAJ16 # DIAN 1.1.-2021: NSAJ15 NSAJ16
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac' './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
':Address/cac:Country/cbc:IdentificationCode',
invoice.invoice_supplier.address.country.code) invoice.invoice_supplier.address.country.code)
# DIAN 1.1.-2021: DSAJ17 # DIAN 1.1.-2021: DSAJ17
# DIAN 1.1.-2021: NSAJ17 # DIAN 1.1.-2021: NSAJ17
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':Address/cac:Country/cbc:Name',
invoice.invoice_supplier.address.country.name, invoice.invoice_supplier.address.country.name,
# DIAN 1.1.-2021: DSAJ18 # DIAN 1.1.-2021: DSAJ18
# # DIAN 1.1.-2021: NSAJ18 # # DIAN 1.1.-2021: NSAJ18
@@ -153,15 +134,13 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAJ20 # DIAN 1.1.-2021: DSAJ20
# DIAN 1.1.-2021: NSAJ20 # DIAN 1.1.-2021: NSAJ20
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
':RegistrationName',
invoice.invoice_supplier.legal_name) invoice.invoice_supplier.legal_name)
# DIAN 1.1.-2021: DSAJ21 # DIAN 1.1.-2021: DSAJ21
# DIAN 1.1.-2021: NSAJ21 # DIAN 1.1.-2021: NSAJ21
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
':CompanyID',
invoice.invoice_supplier.ident, invoice.invoice_supplier.ident,
# DIAN 1.1.-2021: DSAJ22 DSAJ23 DSAJ24 DSAJ25 # DIAN 1.1.-2021: DSAJ22 DSAJ23 DSAJ24 DSAJ25
# DIAN 1.1.-2021: NSAJ22 NSAJ23 NSAJ24 NSAJ25 # DIAN 1.1.-2021: NSAJ22 NSAJ23 NSAJ24 NSAJ25
@@ -170,29 +149,25 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAJ26 # DIAN 1.1.-2021: DSAJ26
# DIAN 1.1.-2021: NSAJ26 # DIAN 1.1.-2021: NSAJ26
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
':TaxLevelCode',
invoice.invoice_supplier.responsability_code, invoice.invoice_supplier.responsability_code,
listName=invoice.invoice_supplier.responsability_regime_code) listName=invoice.invoice_supplier.responsability_regime_code)
# DIAN 1.1.-2021: DSAJ39 # DIAN 1.1.-2021: DSAJ39
# DIAN 1.1.-2021: NSAJ39 # DIAN 1.1.-2021: NSAJ39
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
':TaxScheme')
# DIAN 1.1.-2021: DSAJ40 # DIAN 1.1.-2021: DSAJ40
# DIAN 1.1.-2021: NSAJ40 # DIAN 1.1.-2021: NSAJ40
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
':TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code) invoice.invoice_customer.tax_scheme.code)
# DIAN 1.1.-2021: DSAJ41 # DIAN 1.1.-2021: DSAJ41
# DIAN 1.1.-2021: NSAJ41 # DIAN 1.1.-2021: NSAJ41
fexml.set_element( fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
':TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name) invoice.invoice_customer.tax_scheme.name)
def set_customer(fexml, invoice): def set_customer(fexml, invoice):
@@ -218,8 +193,7 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAK20 # DIAN 1.1.-2021: DSAK20
# DIAN 1.1.-2021: NSAK20 # DIAN 1.1.-2021: NSAK20
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
':RegistrationName',
invoice.invoice_customer.legal_name) invoice.invoice_customer.legal_name)
customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
@@ -231,8 +205,7 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAK21 # DIAN 1.1.-2021: DSAK21
# DIAN 1.1.-2021: NSAK21 # DIAN 1.1.-2021: NSAK21
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
':CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
# DIAN 1.1.-2021: DSAK22 DSAK23 DSAK24 DSAK25 # DIAN 1.1.-2021: DSAK22 DSAK23 DSAK24 DSAK25
# DIAN 1.1.-2021: NSAK22 NSAK23 NSAK24 NSAK25 # DIAN 1.1.-2021: NSAK22 NSAK23 NSAK24 NSAK25
@@ -241,28 +214,24 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAK26 # DIAN 1.1.-2021: DSAK26
# DIAN 1.1.-2021: NSAK26 # DIAN 1.1.-2021: NSAK26
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
':TaxLevelCode',
invoice.invoice_customer.responsability_code) invoice.invoice_customer.responsability_code)
# DIAN 1.1.-2021: DSAK39 # DIAN 1.1.-2021: DSAK39
# DIAN 1.1.-2021: NSAK39 # DIAN 1.1.-2021: NSAK39
fexml.placeholder_for( fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
':TaxScheme')
# DIAN 1.1.-2021: DSAK40 # DIAN 1.1.-2021: DSAK40
# DIAN 1.1.-2021: NSAK40 # DIAN 1.1.-2021: NSAK40
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
':TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code) invoice.invoice_customer.tax_scheme.code)
# DIAN 1.1.-2021: DSAK41 # DIAN 1.1.-2021: DSAK41
# DIAN 1.1.-2021: NSAK41 # DIAN 1.1.-2021: NSAK41
fexml.set_element( fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac' './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
':TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name) invoice.invoice_customer.tax_scheme.name)
def set_payment_mean(fexml, invoice): def set_payment_mean(fexml, invoice):
@@ -359,8 +328,7 @@ class DIANSupportDocumentXML(fe.FeXML):
reference.uuid, reference.uuid,
schemeName=schemeName) schemeName=schemeName)
fexml.set_element( fexml.set_element(
'./cac:BillingReference/cac:InvoiceDocumentReference/' './cac:BillingReference/cac:InvoiceDocumentReference/cbc:IssueDate',
'cbc:IssueDate',
reference.date.strftime("%Y-%m-%d")) reference.date.strftime("%Y-%m-%d"))
def set_billing_reference(fexml, invoice): def set_billing_reference(fexml, invoice):
@@ -451,17 +419,14 @@ class DIANSupportDocumentXML(fe.FeXML):
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
line.set_element( line.set_element(
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/' '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
'cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element( line.set_element(
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme' '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
'/cbc:ID',
cod_impuesto) cod_impuesto)
line.set_element( line.set_element(
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme' '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', 'IVA')
'/cbc:Name', 'IVA')
# abstract method # abstract method
@@ -490,8 +455,7 @@ class DIANSupportDocumentXML(fe.FeXML):
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac' './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
':TaxCategory/cbc:Percent',
'%0.2f' % '%0.2f' %
round( round(
subtotal.percent, subtotal.percent,
@@ -500,12 +464,10 @@ class DIANSupportDocumentXML(fe.FeXML):
if subtotal.scheme is not None: if subtotal.scheme is not None:
# DIAN 1.7.-2020: FAX15 # DIAN 1.7.-2020: FAX15
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac' './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
':TaxCategory/cac:TaxScheme/cbc:ID',
subtotal.scheme.code) subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac' './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
':TaxCategory/cac:TaxScheme/cbc:Name',
subtotal.scheme.name) subtotal.scheme.name)
def set_invoice_line_tax(fexml, line, invoice_line): def set_invoice_line_tax(fexml, line, invoice_line):
@@ -526,8 +488,7 @@ class DIANSupportDocumentXML(fe.FeXML):
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc' './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
':Percent',
'%0.2f' % '%0.2f' %
round( round(
subtotal.percent, subtotal.percent,
@@ -536,12 +497,10 @@ class DIANSupportDocumentXML(fe.FeXML):
if subtotal.scheme is not None: if subtotal.scheme is not None:
# DIAN 1.7.-2020: FAX15 # DIAN 1.7.-2020: FAX15
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac' './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
':TaxScheme/cbc:ID',
subtotal.scheme.code) subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac' './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
':TaxScheme/cbc:Name',
subtotal.scheme.name) subtotal.scheme.name)
def set_invoice_lines(fexml, invoice): def set_invoice_lines(fexml, invoice):
@@ -656,9 +615,7 @@ class DIANSupportDocumentXML(fe.FeXML):
(fexml.tag_document()), (fexml.tag_document()),
invoice.invoice_type_code, invoice.invoice_type_code,
listAgencyID='195', listAgencyID='195',
listAgencyName=( listAgencyName='No matching global declaration available for the validation root',
'No matching global declaration available for the '
'validation root'),
listURI='http://www.dian.gov.co') listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines)) fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
fexml.set_element( fexml.set_element(

367
facho/fe/model/__init__.py Normal file
View File

@@ -0,0 +1,367 @@
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")

90
facho/fe/model/common.py Normal file
View File

@@ -0,0 +1,90 @@
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')

58
facho/fe/model/dian.py Normal file
View File

@@ -0,0 +1,58 @@
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')

View File

@@ -8,91 +8,21 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
import hashlib import hashlib
import typing
from .. import fe from .. import fe
from .. import form
from ..data.dian import codelist from ..data.dian import codelist
from .amount import Amount from .devengado import *
from .deduccion import ( from .deduccion import *
Deduccion, from .trabajador import *
DeduccionFondoPension, from .empleador import *
DeduccionSalud, from .pago import *
)
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 .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',
]
from .amount import Amount
from .exception import *
class Fecha: class Fecha:
def __init__(self, fecha): def __init__(self, fecha):
@@ -116,7 +46,6 @@ class Fecha:
def __str__(self): def __str__(self):
return self.value return self.value
class FechaPago(Fecha): class FechaPago(Fecha):
def apply(self, fragment): def apply(self, fragment):
fragment.set_element('./FechaPago', self.value) fragment.set_element('./FechaPago', self.value)
@@ -155,21 +84,19 @@ class NumeroSecuencia:
# NIE012 # NIE012
Numero = numero) Numero = numero)
@dataclass @dataclass
class Periodo: class Periodo:
fecha_ingreso: str | Fecha fecha_ingreso: typing.Union[str, Fecha]
fecha_liquidacion_inicio: str | Fecha fecha_liquidacion_inicio: typing.Union[str, Fecha]
fecha_liquidacion_fin: str | Fecha fecha_liquidacion_fin: typing.Union[str, Fecha]
fecha_generacion: str | Fecha fecha_generacion: typing.Union[str, Fecha]
tiempo_laborado: int = 1 tiempo_laborado: int = 1
fecha_retiro: str | Fecha | None = None fecha_retiro: typing.Union[str, Fecha] = None
def __post_init__(self): def __post_init__(self):
self.fecha_ingreso = Fecha.cast(self.fecha_ingreso) self.fecha_ingreso = Fecha.cast(self.fecha_ingreso)
self.fecha_liquidacion_inicio = Fecha.cast( self.fecha_liquidacion_inicio = Fecha.cast(self.fecha_liquidacion_inicio)
self.fecha_liquidacion_inicio)
self.fecha_liquidacion_fin = Fecha.cast(self.fecha_liquidacion_fin) self.fecha_liquidacion_fin = Fecha.cast(self.fecha_liquidacion_fin)
self.fecha_retiro = Fecha.cast(self.fecha_retiro, optional=True) self.fecha_retiro = Fecha.cast(self.fecha_retiro, optional=True)
@@ -180,8 +107,7 @@ class Periodo:
#NIE003 #NIE003
FechaRetiro=self.fecha_retiro, FechaRetiro=self.fecha_retiro,
#NIE004 #NIE004
FechaLiquidacionInicio=( FechaLiquidacionInicio=self.fecha_liquidacion_inicio,
self.fecha_liquidacion_inicio),
#NIE005 #NIE005
FechaLiquidacionFin=self.fecha_liquidacion_fin, FechaLiquidacionFin=self.fecha_liquidacion_fin,
#NIE006 #NIE006
@@ -189,7 +115,6 @@ class Periodo:
#NIE008 #NIE008
FechaGen=self.fecha_generacion) FechaGen=self.fecha_generacion)
@dataclass @dataclass
class Proveedor: class Proveedor:
razon_social: str razon_social: str
@@ -216,16 +141,11 @@ class Proveedor:
cune_xpath = scopexml.xpath_from_root('/InformacionGeneral') cune_xpath = scopexml.xpath_from_root('/InformacionGeneral')
cune = fexml.get_element_attribute(cune_xpath, 'CUNE') cune = fexml.get_element_attribute(cune_xpath, 'CUNE')
ambiente = fexml.get_element_attribute( ambiente = fexml.get_element_attribute(scopexml.xpath_from_root('/InformacionGeneral'), 'Ambiente')
scopexml.xpath_from_root('/InformacionGeneral'), 'Ambiente') codigo_qr = f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={cune}"
codigo_qr = (
f"https://catalogo-vpfe.dian.gov.co/document/"
f"searchqr?documentkey={cune}")
if InformacionGeneral.AMBIENTE_PRUEBAS == ambiente: if InformacionGeneral.AMBIENTE_PRUEBAS == ambiente:
codigo_qr = ( codigo_qr = f"https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey={cune}"
f"https://catalogo-vpfe-hab.dian.gov.co/document/"
f"searchqr?documentkey={cune}")
elif ambiente is None: elif ambiente is None:
raise RuntimeError('fail to get InformacionGeneral/@Ambiente') raise RuntimeError('fail to get InformacionGeneral/@Ambiente')
@@ -233,15 +153,13 @@ class Proveedor:
# NIE020 # NIE020
software_code = self._software_security_code(fexml, scopexml) software_code = self._software_security_code(fexml, scopexml)
fexml.set_attributes( fexml.set_attributes(scopexml.xpath_from_root('/ProveedorXML'), SoftwareSC=software_code)
scopexml.xpath_from_root('/ProveedorXML'),
SoftwareSC=software_code)
def _software_security_code(self, fexml, scopexml): def _software_security_code(self, fexml, scopexml):
# 8.2 # 8.2
numero = fexml.get_element_attribute( numero = fexml.get_element_attribute(scopexml.xpath_from_root('/NumeroSecuenciaXML'), 'Numero')
scopexml.xpath_from_root('/NumeroSecuenciaXML'), 'Numero')
if numero is None: if numero is None:
raise RuntimeError('fallo obtener NumeroSequenciaXML/@Numero') raise RuntimeError('fallo obtener NumeroSequenciaXML/@Numero')
@@ -255,7 +173,6 @@ class Proveedor:
h.update(code.encode('utf-8')) h.update(code.encode('utf-8'))
return h.hexdigest() return h.hexdigest()
@dataclass @dataclass
class Metadata: class Metadata:
novedad: Novedad novedad: Novedad
@@ -264,33 +181,18 @@ class Metadata:
lugar_generacion: Lugar lugar_generacion: Lugar
proveedor: Proveedor proveedor: Proveedor
def apply( def apply(self, novedad, numero_secuencia_xml, lugar_generacion_xml, proveedor_xml):
self,
novedad,
numero_secuencia_xml,
lugar_generacion_xml,
proveedor_xml):
if novedad: if novedad:
self.novedad.apply(novedad) self.novedad.apply(novedad)
self.secuencia.apply(numero_secuencia_xml) self.secuencia.apply(numero_secuencia_xml)
self.lugar_generacion.apply( self.lugar_generacion.apply(lugar_generacion_xml, './LugarGeneracionXML')
lugar_generacion_xml,
'./LugarGeneracionXML')
self.proveedor.apply(proveedor_xml) self.proveedor.apply(proveedor_xml)
def post_apply( def post_apply(self, fexml, scopexml, novedad, numero_secuencia_xml, lugar_generacion_xml, proveedor_xml):
self,
fexml,
scopexml,
novedad,
numero_secuencia_xml,
lugar_generacion_xml,
proveedor_xml):
self.proveedor.post_apply(fexml, scopexml, proveedor_xml) self.proveedor.post_apply(fexml, scopexml, proveedor_xml)
if novedad: if novedad:
self.novedad.post_apply(fexml, scopexml, proveedor_xml) self.novedad.post_apply(fexml, scopexml, proveedor_xml)
@dataclass @dataclass
class PeriodoNomina: class PeriodoNomina:
code: str code: str
@@ -301,7 +203,6 @@ class PeriodoNomina:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.PeriodoNomina[self.code]['name'] self.name = codelist.PeriodoNomina[self.code]['name']
@dataclass @dataclass
class TipoMoneda: class TipoMoneda:
code: str code: str
@@ -312,7 +213,6 @@ class TipoMoneda:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.TipoMoneda[self.code]['name'] self.name = codelist.TipoMoneda[self.code]['name']
@dataclass @dataclass
class InformacionGeneral: class InformacionGeneral:
@dataclass @dataclass
@@ -359,7 +259,7 @@ class InformacionGeneral:
def __str__(self): def __str__(self):
self.valor self.valor
fecha_generacion: str | Fecha fecha_generacion: typing.Union[str, Fecha]
hora_generacion: str hora_generacion: str
periodo_nomina: PeriodoNomina periodo_nomina: PeriodoNomina
tipo_moneda: TipoMoneda tipo_moneda: TipoMoneda
@@ -428,28 +328,17 @@ class InformacionGeneral:
CUNE = cune_hash CUNE = cune_hash
) )
class DianXMLExtensionSigner(fe.DianXMLExtensionSigner): class DianXMLExtensionSigner(fe.DianXMLExtensionSigner):
def __init__(self, pkcs12_path, passphrase=None, localpolicy=True): def __init__(self, pkcs12_path, passphrase=None, localpolicy=True):
super().__init__( super().__init__(pkcs12_path, passphrase=passphrase, localpolicy=localpolicy)
pkcs12_path,
passphrase=passphrase,
localpolicy=localpolicy)
def _element_extension_content(self, fachoxml): def _element_extension_content(self, fachoxml):
return fachoxml.builder.xpath( return fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
fachoxml.root,
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
class DIANNominaXML: class DIANNominaXML:
def __init__( def __init__(self, tag_document, xpath_ajuste=None, schemaLocation=None, namespace_ajuste=None):
self,
tag_document,
xpath_ajuste=None,
schemaLocation=None,
namespace_ajuste=None):
self.informacion_general_version = None self.informacion_general_version = None
self.tag_document = tag_document self.tag_document = tag_document
@@ -457,26 +346,21 @@ class DIANNominaXML:
if namespace_ajuste: if namespace_ajuste:
self.fexml = fe.FeXML(tag_document, namespace_ajuste) self.fexml = fe.FeXML(tag_document, namespace_ajuste)
else: else:
self.fexml = fe.FeXML( self.fexml = fe.FeXML(tag_document, 'dian:gov:co:facturaelectronica:NominaIndividual')
tag_document,
'dian:gov:co:facturaelectronica:NominaIndividual')
self.fexml.root.set("SchemaLocation", "") self.fexml.root.set("SchemaLocation", "")
self.fexml.root.set("schemaLocation", schemaLocation) self.fexml.root.set("schemaLocation", schemaLocation)
# layout, la dian requiere que los elementos # layout, la dian requiere que los elementos
# esten ordenados segun el anexo tecnico # esten ordenados segun el anexo tecnico
self.fexml.placeholder_for( self.fexml.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
self.fexml.placeholder_for('./TipoNota', optional=True) self.fexml.placeholder_for('./TipoNota', optional=True)
self.root_fragment = self.fexml self.root_fragment = self.fexml
if xpath_ajuste is not None: if xpath_ajuste is not None:
self.root_fragment = self.fexml.fragment(xpath_ajuste) self.root_fragment = self.fexml.fragment(xpath_ajuste)
self.root_fragment.placeholder_for( self.root_fragment.placeholder_for('./ReemplazandoPredecesor', optional=True)
'./ReemplazandoPredecesor', optional=True) self.root_fragment.placeholder_for('./EliminandoPredecesor', optional=True)
self.root_fragment.placeholder_for(
'./EliminandoPredecesor', optional=True)
if not namespace_ajuste: if not namespace_ajuste:
self.root_fragment.placeholder_for('./Novedad', optional=False) self.root_fragment.placeholder_for('./Novedad', optional=False)
self.root_fragment.placeholder_for('./Periodo') self.root_fragment.placeholder_for('./Periodo')
@@ -490,20 +374,16 @@ class DIANNominaXML:
self.root_fragment.placeholder_for('./Pago') self.root_fragment.placeholder_for('./Pago')
self.root_fragment.placeholder_for('./FechasPagos') self.root_fragment.placeholder_for('./FechasPagos')
self.root_fragment.placeholder_for('./Devengados/Basico') self.root_fragment.placeholder_for('./Devengados/Basico')
self.root_fragment.placeholder_for( self.root_fragment.placeholder_for('./Devengados/Transporte', optional=True)
'./Devengados/Transporte', optional=True)
if not namespace_ajuste: if not namespace_ajuste:
self.novedad = self.root_fragment.fragment('./Novedad') self.novedad = self.root_fragment.fragment('./Novedad')
else: else:
self.novedad = None self.novedad = None
self.informacion_general_xml = self.root_fragment.fragment( self.informacion_general_xml = self.root_fragment.fragment('./InformacionGeneral')
'./InformacionGeneral')
self.periodo_xml = self.root_fragment.fragment('./Periodo') self.periodo_xml = self.root_fragment.fragment('./Periodo')
self.fecha_pagos_xml = self.root_fragment.fragment('./FechasPagos') self.fecha_pagos_xml = self.root_fragment.fragment('./FechasPagos')
self.numero_secuencia_xml = self.root_fragment.fragment( self.numero_secuencia_xml = self.root_fragment.fragment('./NumeroSecuenciaXML')
'./NumeroSecuenciaXML') self.lugar_generacion_xml = self.root_fragment.fragment('./LugarGeneracionXML')
self.lugar_generacion_xml = self.root_fragment.fragment(
'./LugarGeneracionXML')
self.proveedor_xml = self.root_fragment.fragment('./ProveedorXML') self.proveedor_xml = self.root_fragment.fragment('./ProveedorXML')
self.empleador = self.root_fragment.fragment('./Empleador') self.empleador = self.root_fragment.fragment('./Empleador')
self.trabajador = self.root_fragment.fragment('./Trabajador') self.trabajador = self.root_fragment.fragment('./Trabajador')
@@ -519,19 +399,13 @@ class DIANNominaXML:
raise ValueError('se espera tipo Metadata') raise ValueError('se espera tipo Metadata')
self.metadata = metadata self.metadata = metadata
self.metadata.apply( self.metadata.apply(self.novedad, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml)
self.novedad,
self.numero_secuencia_xml,
self.lugar_generacion_xml,
self.proveedor_xml)
def asignar_informacion_general(self, general): def asignar_informacion_general(self, general):
if not isinstance(general, InformacionGeneral): if not isinstance(general, InformacionGeneral):
raise ValueError('se espera tipo InformacionGeneral') raise ValueError('se espera tipo InformacionGeneral')
self.informacion_general = general self.informacion_general = general
self.informacion_general.apply( self.informacion_general.apply(self.informacion_general_xml, self.informacion_general_version)
self.informacion_general_xml,
self.informacion_general_version)
def asignar_periodo(self, periodo): def asignar_periodo(self, periodo):
if not isinstance(periodo, Periodo): if not isinstance(periodo, Periodo):
@@ -624,13 +498,10 @@ class DIANNominaXML:
def informacion_general(self): def informacion_general(self):
xpath = self.root_fragment.xpath_from_root('/InformacionGeneral') xpath = self.root_fragment.xpath_from_root('/InformacionGeneral')
return { return {
'cune': self.fexml.get_element_attribute( 'cune': self.fexml.get_element_attribute(cune_xpath, 'CUNE'),
xpath, 'CUNE'), 'fecha_generacion': self.fexml.get_element_attribute(xpath, 'FechaGen'),
'fecha_generacion': self.fexml.get_element_attribute( 'numero': self.fexml.get_element_attribute(self.root_fragment('/NumeroSecuenciaXML', 'Numero'))
xpath, 'FechaGen'), }
'numero': self.fexml.get_element_attribute(
self.root_fragment(
'/NumeroSecuenciaXML', 'Numero'))}
def toFachoXML(self): def toFachoXML(self):
self._devengados_total() self._devengados_total()
@@ -641,38 +512,26 @@ class DIANNominaXML:
#TODO(bit4bit) acoplamiento temporal #TODO(bit4bit) acoplamiento temporal
# es importante el orden de ejecucion # es importante el orden de ejecucion
self.informacion_general.post_apply( self.informacion_general.post_apply(self.fexml, self.root_fragment, self.informacion_general_xml)
self.fexml, self.root_fragment, self.informacion_general_xml)
if self.metadata is not None: if self.metadata is not None:
self.metadata.post_apply( self.metadata.post_apply(self.fexml, self.root_fragment, self.novedad, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml)
self.fexml,
self.root_fragment,
self.novedad,
self.numero_secuencia_xml,
self.lugar_generacion_xml,
self.proveedor_xml)
return self.fexml return self.fexml
def _comprobante_total(self): def _comprobante_total(self):
devengados_total = self.root_fragment.get_element_text_or_attribute( devengados_total = self.root_fragment.get_element_text_or_attribute('./DevengadosTotal', '0.0')
'./DevengadosTotal', '0.0') deducciones_total = self.root_fragment.get_element_text_or_attribute('./DeduccionesTotal', '0.0')
deducciones_total = self.root_fragment.get_element_text_or_attribute(
'./DeduccionesTotal', '0.0')
comprobante_total = Amount(devengados_total) - \ comprobante_total = Amount(devengados_total) - Amount(deducciones_total)
Amount(deducciones_total)
self.root_fragment.set_element( self.root_fragment.set_element('./ComprobanteTotal', str(round(comprobante_total, 2)))
'./ComprobanteTotal', str(round(comprobante_total, 2)))
def _deducciones_total(self): def _deducciones_total(self):
xpaths = [ xpaths = [
self.root_fragment.xpath_from_root( self.root_fragment.xpath_from_root('/Deducciones/Salud/@Deduccion'),
'/Deducciones/Salud/@Deduccion'), self.root_fragment.xpath_from_root('/Deducciones/FondoPension/@Deduccion')
self.root_fragment.xpath_from_root( ]
'/Deducciones/FondoPension/@Deduccion')]
deducciones = map(lambda valor: Amount(valor), deducciones = map(lambda valor: Amount(valor),
self._values_of_xpaths(xpaths)) self._values_of_xpaths(xpaths))
@@ -681,19 +540,15 @@ class DIANNominaXML:
for deduccion in deducciones: for deduccion in deducciones:
deducciones_total += deduccion deducciones_total += deduccion
self.root_fragment.set_element( self.root_fragment.set_element('./DeduccionesTotal', str(round(deducciones_total, 2)))
'./DeduccionesTotal', str(round(deducciones_total, 2)))
def _devengados_total(self): def _devengados_total(self):
xpaths = [ xpaths = [
self.root_fragment.xpath_from_root( self.root_fragment.xpath_from_root('/Devengados/Basico/@SueldoTrabajado'),
'/Devengados/Basico/@SueldoTrabajado'), self.root_fragment.xpath_from_root('/Devengados/Transporte/@AuxilioTransporte'),
self.root_fragment.xpath_from_root( self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojS'),
'/Devengados/Transporte/@AuxilioTransporte'), self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojNS')
self.root_fragment.xpath_from_root( ]
'/Devengados/Transporte/@ViaticoManuAlojS'),
self.root_fragment.xpath_from_root(
'/Devengados/Transporte/@ViaticoManuAlojNS')]
devengados = map(lambda valor: Amount(valor), devengados = map(lambda valor: Amount(valor),
self._values_of_xpaths(xpaths)) self._values_of_xpaths(xpaths))
@@ -703,13 +558,10 @@ class DIANNominaXML:
# TODO(bit4bit) nque valor va redondeado? # TODO(bit4bit) nque valor va redondeado?
# NIE186 # NIE186
self.root_fragment.set_element('./Redondeo', str(round(0,2))) self.root_fragment.set_element('./Redondeo', str(round(0,2)))
self.root_fragment.set_element( self.root_fragment.set_element('./DevengadosTotal', str(round(devengados_total,2)))
'./DevengadosTotal', str(round(devengados_total, 2)))
def _values_of_xpaths(self, xpaths): def _values_of_xpaths(self, xpaths):
xpaths_values_of_values = map( xpaths_values_of_values = map(lambda val: self.fexml.get_element_text_or_attribute(val, multiple=True), xpaths)
lambda val: self.fexml.get_element_text_or_attribute(
val, multiple=True), xpaths)
xpaths_values = [] xpaths_values = []
# toda esta carreta para hacer un aplano de lista # toda esta carreta para hacer un aplano de lista
for xpath_values in xpaths_values_of_values: for xpath_values in xpaths_values_of_values:
@@ -721,22 +573,15 @@ class DIANNominaXML:
return filter(lambda val: val is not None, xpaths_values) return filter(lambda val: val is not None, xpaths_values)
class DIANNominaIndividual(DIANNominaXML): class DIANNominaIndividual(DIANNominaXML):
def __init__(self): def __init__(self):
schema = ( schema = "dian:gov:co:facturaelectronica:NominaIndividual NominaIndividualElectronicaXSD.xsd"
"dian:gov:co:facturaelectronica:NominaIndividual"
" NominaIndividualElectronicaXSD.xsd"
)
super().__init__('NominaIndividual', schemaLocation=schema) super().__init__('NominaIndividual', schemaLocation=schema)
self.informacion_general_version = ( self.informacion_general_version = 'V1.0: Documento Soporte de Pago de Nómina Electrónica'
'V1.0: Documento Soporte de Pago de Nómina Electrónica')
# TODO(bit4bit) confirmar que no tienen en comun con NominaIndividual # TODO(bit4bit) confirmar que no tienen en comun con NominaIndividual
class DIANNominaIndividualDeAjuste(DIANNominaXML): class DIANNominaIndividualDeAjuste(DIANNominaXML):
class Reemplazar(DIANNominaXML): class Reemplazar(DIANNominaXML):
@@ -749,8 +594,7 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
def apply(self, fragment): def apply(self, fragment):
# NIAE214 # NIAE214
fragment.set_element('./TipoNota', '1') fragment.set_element('./TipoNota', '1')
fragment.set_element( fragment.set_element('./Reemplazar/ReemplazandoPredecesor', None,
'./Reemplazar/ReemplazandoPredecesor', None,
# NIAE090 # NIAE090
NumeroPred = self.numero, NumeroPred = self.numero,
# NIAE191 # NIAE191
@@ -760,28 +604,18 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
) )
def __init__(self): def __init__(self):
schema = ( schema = "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd"
"dian:gov:co:facturaelectronica:NominaIndividualDeAjuste"
" NominaIndividualDeAjusteElectronicaXSD.xsd"
)
super().__init__( super().__init__('NominaIndividualDeAjuste', './Reemplazar', schemaLocation=schema, namespace_ajuste='dian:gov:co:facturaelectronica:NominaIndividualDeAjuste')
'NominaIndividualDeAjuste',
'./Reemplazar',
schemaLocation=schema,
namespace_ajuste=(
'dian:gov:co:facturaelectronica:'
'NominaIndividualDeAjuste'))
self.informacion_general_version = ( self.informacion_general_version = 'V1.0: Nota de Ajuste de Documento Soporte de Pago de Nómina Electrónica'
'V1.0: Nota de Ajuste de Documento Soporte de Pago de '
'Nómina Electrónica')
def asignar_predecesor(self, predecesor): def asignar_predecesor(self, predecesor):
if not isinstance(predecesor, self.Predecesor): if not isinstance(predecesor, self.Predecesor):
raise ValueError("se espera tipo Predecesor") raise ValueError("se espera tipo Predecesor")
predecesor.apply(self.fexml) predecesor.apply(self.fexml)
class Eliminar(DIANNominaXML): class Eliminar(DIANNominaXML):
@dataclass @dataclass
@@ -802,21 +636,10 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
) )
def __init__(self): def __init__(self):
schema = ( schema = "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd"
"dian:gov:co:facturaelectronica:NominaIndividualDeAjuste" super().__init__('NominaIndividualDeAjuste', './Eliminar', schemaLocation=schema, namespace_ajuste='dian:gov:co:facturaelectronica:NominaIndividualDeAjuste')
" NominaIndividualDeAjusteElectronicaXSD.xsd"
)
super().__init__(
'NominaIndividualDeAjuste',
'./Eliminar',
schemaLocation=schema,
namespace_ajuste=(
'dian:gov:co:facturaelectronica:'
'NominaIndividualDeAjuste'))
self.informacion_general_version = ( self.informacion_general_version = "V1.0: Nota de Ajuste de Documento Soporte de Pago de Nómina Electrónica"
"V1.0: Nota de Ajuste de Documento Soporte de Pago de "
"Nómina Electrónica")
def asignar_predecesor(self, predecesor): def asignar_predecesor(self, predecesor):
if not isinstance(predecesor, self.Predecesor): if not isinstance(predecesor, self.Predecesor):
@@ -825,3 +648,4 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
def __init__(self): def __init__(self):
super().__init__('NominaIndividualDeAjuste') super().__init__('NominaIndividualDeAjuste')

View File

@@ -1,5 +1,4 @@
from .. import form from .. import form
class Amount(form.Amount): class Amount(form.Amount):
pass pass

View File

@@ -2,9 +2,9 @@
# al crear objetos de valor # al crear objetos de valor
# se debe exportar en __all__ # se debe exportar en __all__
from .deduccion import Deduccion from .deduccion import *
from .salud import DeduccionSalud from .salud import *
from .fondo_pension import DeduccionFondoPension from .fondo_pension import *
__all__ = [ __all__ = [
'Deduccion', 'Deduccion',

View File

@@ -3,7 +3,6 @@ from dataclasses import dataclass
from ..amount import Amount from ..amount import Amount
from .deduccion import Deduccion from .deduccion import Deduccion
@dataclass @dataclass
class DeduccionFondoPension(Deduccion): class DeduccionFondoPension(Deduccion):
porcentaje: Amount porcentaje: Amount

View File

@@ -3,7 +3,6 @@ from dataclasses import dataclass
from ..amount import Amount from ..amount import Amount
from .deduccion import Deduccion from .deduccion import Deduccion
@dataclass @dataclass
class DeduccionSalud(Deduccion): class DeduccionSalud(Deduccion):
porcentaje: Amount porcentaje: Amount
@@ -17,3 +16,4 @@ class DeduccionSalud(Deduccion):
# NIE163 # NIE163
Deduccion = self.deduccion Deduccion = self.deduccion
) )

View File

@@ -1,5 +1,4 @@
from .. import form from .. import form
class Departamento(form.CountrySubentity): class Departamento(form.CountrySubentity):
pass pass

View File

@@ -1,17 +1,8 @@
from .basico import DevengadoBasico from .basico import *
from .transporte import DevengadoTransporte from .transporte import *
from .devengado import Devengado from .devengado import *
from .horas_extras import ( from .horas_extras import *
DevengadoHoraExtra,
DevengadoHorasExtrasDiarias,
DevengadoHorasExtrasNocturnas,
DevengadoHorasRecargoNocturno,
DevengadoHorasExtrasDiariasDominicalesYFestivos,
DevengadoHorasRecargoDiariasDominicalesYFestivos,
DevengadoHorasExtrasNocturnasDominicalesYFestivos,
DevengadoHorasRecargoNocturnoDominicalesYFestivos,
)
__all__ = [ __all__ = [
'Devengado', 'Devengado',

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import List
from ..amount import Amount from ..amount import Amount
from .devengado import Devengado from .devengado import Devengado
@@ -30,17 +30,16 @@ class DevengadoHoraExtra:
@dataclass @dataclass
class DevengadoHorasExtrasDiarias(Devengado): class DevengadoHorasExtrasDiarias(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HEDs') hora_extra_xml = fragment.fragment('./HEDs')
for hora_extra in self.horas_extras: for hora_extra in self.horas_extras:
hora_extra.apply('./HED', hora_extra_xml) hora_extra.apply('./HED', hora_extra_xml)
@dataclass @dataclass
class DevengadoHorasExtrasNocturnas(Devengado): class DevengadoHorasExtrasNocturnas(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HENs') hora_extra_xml = fragment.fragment('./HENs')
@@ -50,47 +49,43 @@ class DevengadoHorasExtrasNocturnas(Devengado):
@dataclass @dataclass
class DevengadoHorasRecargoNocturno(Devengado): class DevengadoHorasRecargoNocturno(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRNs') hora_extra_xml = fragment.fragment('./HRNs')
for hora_extra in self.horas_extras: for hora_extra in self.horas_extras:
hora_extra.apply('./HRN', hora_extra_xml) hora_extra.apply('./HRN', hora_extra_xml)
@dataclass @dataclass
class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado): class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HEDDFs') hora_extra_xml = fragment.fragment('./HEDDFs')
for hora_extra in self.horas_extras: for hora_extra in self.horas_extras:
hora_extra.apply('./HEDDF', hora_extra_xml) hora_extra.apply('./HEDDF', hora_extra_xml)
@dataclass @dataclass
class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado): class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRDDFs') hora_extra_xml = fragment.fragment('./HRDDFs')
for hora_extra in self.horas_extras: for hora_extra in self.horas_extras:
hora_extra.apply('./HRDDF', hora_extra_xml) hora_extra.apply('./HRDDF', hora_extra_xml)
@dataclass @dataclass
class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado): class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HENDFs') hora_extra_xml = fragment.fragment('./HENDFs')
for hora_extra in self.horas_extras: for hora_extra in self.horas_extras:
hora_extra.apply('./HENDF', hora_extra_xml) hora_extra.apply('./HENDF', hora_extra_xml)
@dataclass @dataclass
class DevengadoHorasRecargoNocturnoDominicalesYFestivos(Devengado): class DevengadoHorasRecargoNocturnoDominicalesYFestivos(Devengado):
horas_extras: list[DevengadoHoraExtra] horas_extras: List[DevengadoHoraExtra]
def apply(self, fragment): def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRNDFs') hora_extra_xml = fragment.fragment('./HRNDFs')

View File

@@ -3,7 +3,6 @@ from dataclasses import dataclass
from ..amount import Amount from ..amount import Amount
from .devengado import Devengado from .devengado import Devengado
@dataclass @dataclass
class DevengadoTransporte(Devengado): class DevengadoTransporte(Devengado):
auxilio_transporte: Amount = None auxilio_transporte: Amount = None
@@ -18,6 +17,5 @@ class DevengadoTransporte(Devengado):
# NIE072 # NIE072
ViaticoManuAlojS = self.viatico_manutencion, ViaticoManuAlojS = self.viatico_manutencion,
# NIE073 # NIE073
ViaticoManuAlojNS=( ViaticoManuAlojNS = self.viatico_manutencion_no_salarial
self.viatico_manutencion_no_salarial)
) )

View File

@@ -4,7 +4,6 @@ from ..pais import Pais
from ..departamento import Departamento from ..departamento import Departamento
from ..municipio import Municipio from ..municipio import Municipio
@dataclass @dataclass
class Empleador: class Empleador:
razon_social: str razon_social: str
@@ -32,3 +31,5 @@ class Empleador:
RazonSocial=self.razon_social RazonSocial=self.razon_social
) )

View File

@@ -3,7 +3,6 @@ import datetime
from facho import fe from facho import fe
class Habilitacion: class Habilitacion:
@dataclass @dataclass
@@ -17,18 +16,18 @@ class Habilitacion:
self.metadata = metadata self.metadata = metadata
def generar(self, zipname, fecha): def generar(self, zipname, fecha):
fe.DianZIP(open(zipname, 'w')) nominas = []
dianzip = fe.DianZIP(open(zipname, 'w'))
fechabase = datetime.datetime.now() fechabase = datetime.datetime.now()
consecutivo = 0 consecutivo = 0
for _ in range(1, 11): for _ in range(1, 11):
consecutivo += 1 consecutivo += 1
fechabase += datetime.timedelta(days=1) fechabase += datetime.timedelta(days=1)
self._crear_nomina_individual() nomina = self._crear_nomina_individual()
# pag 96 # pag 96
"nie%010d%s%08x.xml" % ( nombre = "nie%010d%s%08x.xml" % (int(self.nit), fecha.strftime('%s'), consecutivo)
int(self.nit), fecha.strftime('%s'), consecutivo)
def _crear_nomina_individual_reemplazar(self, nomina, fechabase): def _crear_nomina_individual_reemplazar(self, nomina, fechabase):
metadata = self.metadata metadata = self.metadata
@@ -37,15 +36,10 @@ class Habilitacion:
nomina_ajuste = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() nomina_ajuste = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
self._poblar_nomina(nomina_ajuste, metadata, fecha, prefijo='R') self._poblar_nomina(nomina_ajuste, metadata, fecha, prefijo='R')
nomina.informacion_general() informacion_general = nomina.informacion_general()
def _poblar_nomina(
self, def _poblar_nomina(self, nomina, metadata, fecha, prefijo='N', consecutivo='0001'):
nomina,
metadata,
fecha,
prefijo='N',
consecutivo='0001'):
nomina.asignar_fecha_pago(fecha) nomina.asignar_fecha_pago(fecha)
nomina.asignar_metadata(fe.nomina.Metadata( nomina.asignar_metadata(fe.nomina.Metadata(
@@ -158,3 +152,4 @@ class Habilitacion:
nomina = fe.nomina.DIANNominaIndividual() nomina = fe.nomina.DIANNominaIndividual()
self._poblar_nomina(nomina, metadata, fecha) self._poblar_nomina(nomina, metadata, fecha)

View File

@@ -5,7 +5,6 @@ from .departamento import Departamento
from .municipio import Municipio from .municipio import Municipio
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class Lugar: class Lugar:
pais: Pais pais: Pais

View File

@@ -1,5 +1,5 @@
from .. import form from .. import form
class Municipio(form.City): class Municipio(form.City):
pass pass

View File

@@ -3,7 +3,6 @@ from dataclasses import dataclass
from .forma_pago import FormaPago from .forma_pago import FormaPago
from .metodo_pago import MetodoPago from .metodo_pago import MetodoPago
@dataclass @dataclass
class Pago: class Pago:
forma: FormaPago forma: FormaPago

View File

@@ -2,7 +2,6 @@ from dataclasses import dataclass
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class FormaPago: class FormaPago:
code: str code: str

View File

@@ -2,7 +2,6 @@ from dataclasses import dataclass
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class MetodoPago: class MetodoPago:
code: str code: str
@@ -12,3 +11,4 @@ class MetodoPago:
if self.code not in codelist.MediosPago: if self.code not in codelist.MediosPago:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.MediosPago[self.code]['name'] self.name = codelist.MediosPago[self.code]['name']

View File

@@ -1,5 +1,4 @@
from .. import form from .. import form
class Pais(form.Country): class Pais(form.Country):
pass pass

View File

@@ -2,11 +2,12 @@ from dataclasses import dataclass, field
from ..amount import Amount from ..amount import Amount
from .tipo_contrato import TipoContrato from .tipo_contrato import *
from .tipo_documento import TipoDocumento from .tipo_documento import *
from .lugar_trabajo import LugarTrabajo from .lugar_trabajo import *
from .tipo_trabajador import TipoTrabajador from .tipo_trabajador import *
from .sub_tipo_trabajador import SubTipoTrabajador from .sub_tipo_trabajador import *
@dataclass @dataclass
@@ -28,13 +29,10 @@ class Trabajador:
codigo_trabajador: str = None codigo_trabajador: str = None
otros_nombres: str = None otros_nombres: str = None
sub_tipo: SubTipoTrabajador = field( sub_tipo: SubTipoTrabajador = field(default_factory=lambda: SubTipoTrabajador(code='00'))
default_factory=lambda: SubTipoTrabajador(code="00")
)
def apply(self, fragment): def apply(self, fragment):
fragment.set_attributes( fragment.set_attributes('./Trabajador',
"./Trabajador",
# NIE041 # NIE041
TipoTrabajador = self.tipo.code, TipoTrabajador = self.tipo.code,
# NIE042 # NIE042
@@ -55,12 +53,13 @@ class Trabajador:
OtrosNombres = self.otros_nombres, OtrosNombres = self.otros_nombres,
# NIE050 # NIE050
LugarTrabajoPais = self.lugar_trabajo.pais.code, LugarTrabajoPais = self.lugar_trabajo.pais.code,
# NIE051 # NIE051
LugarTrabajoDepartamentoEstado=( LugarTrabajoDepartamentoEstado = self.lugar_trabajo.departamento.code,
self.lugar_trabajo.departamento.code
),
# NIE052 # NIE052
LugarTrabajoMunicipioCiudad = self.lugar_trabajo.municipio.code, LugarTrabajoMunicipioCiudad = self.lugar_trabajo.municipio.code,
# NIE053 # NIE053
LugarTrabajoDireccion = self.lugar_trabajo.direccion, LugarTrabajoDireccion = self.lugar_trabajo.direccion,
# NIE056 # NIE056
@@ -70,5 +69,5 @@ class Trabajador:
# NIE062 # NIE062
Sueldo = str(self.sueldo), Sueldo = str(self.sueldo),
# NIE063 # NIE063
CodigoTrabajador=self.codigo_trabajador, CodigoTrabajador = self.codigo_trabajador
) )

View File

@@ -1,9 +1,10 @@
from dataclasses import dataclass from dataclasses import dataclass
from . import *
from ..pais import Pais from ..pais import Pais
from ..departamento import Departamento from ..departamento import Departamento
from ..municipio import Municipio from ..municipio import Municipio
@dataclass @dataclass
class LugarTrabajo: class LugarTrabajo:
pais: Pais pais: Pais

View File

@@ -2,13 +2,12 @@ from dataclasses import dataclass
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class SubTipoTrabajador: class SubTipoTrabajador:
code: str code: str
name: str = "" name: str = ''
def __post_init__(self): def __post_init__(self):
if self.code not in codelist.SubTipoTrabajador: if self.code not in codelist.SubTipoTrabajador:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.SubTipoTrabajador[self.code]["name"] self.name = codelist.SubTipoTrabajador[self.code]['name']

View File

@@ -2,13 +2,14 @@ from dataclasses import dataclass
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class TipoContrato: class TipoContrato:
code: str code: str
name: str = "" name: str = ''
def __post_init__(self): def __post_init__(self):
if self.code not in codelist.TipoContrato: if self.code not in codelist.TipoContrato:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.TipoContrato[self.code]["name"] self.name = codelist.TipoContrato[self.code]['name']

View File

@@ -2,13 +2,12 @@ from dataclasses import dataclass
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class TipoDocumento: class TipoDocumento:
code: str code: str
name: str = "" name: str = ''
def __post_init__(self): def __post_init__(self):
if self.code not in codelist.TipoIdFiscal: if self.code not in codelist.TipoIdFiscal:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.TipoIdFiscal[self.code]["name"] self.name = codelist.TipoIdFiscal[self.code]['name']

View File

@@ -2,13 +2,12 @@ from dataclasses import dataclass
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
@dataclass @dataclass
class TipoTrabajador: class TipoTrabajador:
code: str code: str
name: str = "" name: str = ''
def __post_init__(self): def __post_init__(self):
if self.code not in codelist.TipoTrabajador: if self.code not in codelist.TipoTrabajador:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.TipoTrabajador[self.code]["name"] self.name = codelist.TipoTrabajador[self.code]['name']

175
facho/model/__init__.py Normal file
View File

@@ -0,0 +1,175 @@
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
"""

View File

@@ -0,0 +1,21 @@
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

View File

@@ -0,0 +1,35 @@
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)

View File

@@ -0,0 +1,29 @@
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)

View File

@@ -0,0 +1,60 @@
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)

View File

@@ -0,0 +1,36 @@
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)

View File

@@ -0,0 +1,62 @@
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)

View File

@@ -0,0 +1,86 @@
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]

View File

@@ -0,0 +1,54 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +0,0 @@
[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}'"

17
requirements_dev.txt Normal file
View File

@@ -0,0 +1,17 @@
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 Normal file
View File

@@ -0,0 +1,25 @@
[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 Normal file
View File

@@ -0,0 +1,93 @@
#!/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,
)

View File

@@ -2,7 +2,6 @@ import pytest
import facho.fe.form as form import facho.fe.form as form
from datetime import datetime from datetime import datetime
@pytest.fixture @pytest.fixture
def simple_debit_note_without_lines(): def simple_debit_note_without_lines():
inv = form.DebitNote(form.InvoiceDocumentReference( inv = form.DebitNote(form.InvoiceDocumentReference(
@@ -37,22 +36,14 @@ def simple_debit_note_without_lines():
)) ))
return inv return inv
@pytest.fixture @pytest.fixture
def simple_credit_note_without_lines(): def simple_credit_note_without_lines():
inv = form.CreditNote( inv = form.CreditNote(form.InvoiceDocumentReference('1234', 'xx', datetime.now()))
form.InvoiceDocumentReference(
'1234', 'xx', datetime.now()))
inv.set_period(datetime.now(), datetime.now()) inv.set_period(datetime.now(), datetime.now())
inv.set_issue(datetime.now()) inv.set_issue(datetime.now())
inv.set_ident('ABC123') inv.set_ident('ABC123')
inv.set_operation_type('20') inv.set_operation_type('20')
inv.set_payment_mean( inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234'))
form.PaymentMean(
form.PaymentMean.DEBIT,
'41',
datetime.now(),
'1234'))
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
name = 'facho-supplier', name = 'facho-supplier',
ident = form.PartyIdentification('123','', '31'), ident = form.PartyIdentification('123','', '31'),
@@ -77,7 +68,6 @@ def simple_credit_note_without_lines():
)) ))
return inv return inv
@pytest.fixture @pytest.fixture
def simple_invoice_without_lines(): def simple_invoice_without_lines():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
@@ -85,12 +75,7 @@ def simple_invoice_without_lines():
inv.set_issue(datetime.now()) inv.set_issue(datetime.now())
inv.set_ident('ABC123') inv.set_ident('ABC123')
inv.set_operation_type('10') inv.set_operation_type('10')
inv.set_payment_mean( inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234'))
form.PaymentMean(
form.PaymentMean.DEBIT,
'41',
datetime.now(),
'1234'))
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
name = 'facho-supplier', name = 'facho-supplier',
ident = form.PartyIdentification('123','', '31'), ident = form.PartyIdentification('123','', '31'),
@@ -123,12 +108,7 @@ def simple_invoice():
inv.set_issue(datetime.now()) inv.set_issue(datetime.now())
inv.set_ident('ABC123') inv.set_ident('ABC123')
inv.set_operation_type('10') inv.set_operation_type('10')
inv.set_payment_mean( inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), ' 1234'))
form.PaymentMean(
form.PaymentMean.DEBIT,
'41',
datetime.now(),
' 1234'))
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
name = 'facho-supplier', name = 'facho-supplier',
ident = form.PartyIdentification('123','', '31'), ident = form.PartyIdentification('123','', '31'),

View File

@@ -14,7 +14,6 @@ def test_amount_positive():
with pytest.raises(ValueError): with pytest.raises(ValueError):
form.Amount(-1.0) form.Amount(-1.0)
def test_amount_equals(): def test_amount_equals():
price1 = form.Amount(110.0) price1 = form.Amount(110.0)
price2 = form.Amount(100 + 10.0) price2 = form.Amount(100 + 10.0)
@@ -23,7 +22,6 @@ def test_amount_equals():
assert price1 == form.Amount(10) * form.Amount(10) + form.Amount(10) assert price1 == form.Amount(10) * form.Amount(10) + form.Amount(10)
assert form.Amount(110) == (form.Amount(1.10) * form.Amount(100)) assert form.Amount(110) == (form.Amount(1.10) * form.Amount(100))
def test_round_half_even(): def test_round_half_even():
# https://www.w3.org/TR/xpath-functions-31/#func-round-half-to-even # https://www.w3.org/TR/xpath-functions-31/#func-round-half-to-even
assert form.Amount(0.5).round(0).float() == 0.0 assert form.Amount(0.5).round(0).float() == 0.0
@@ -32,20 +30,16 @@ def test_round_half_even():
assert form.Amount(3.567812e+3).round(2).float() == 3567.81e0 assert form.Amount(3.567812e+3).round(2).float() == 3567.81e0
assert form.Amount(4.7564e-3).round(2).float() == 0.0e0 assert form.Amount(4.7564e-3).round(2).float() == 0.0e0
def test_round(): def test_round():
# Entre 0 y 5 Mantener el dígito menos significativo # Entre 0 y 5 Mantener el dígito menos significativo
assert form.Amount(1.133).round(2) == form.Amount(1.13) assert form.Amount(1.133).round(2) == form.Amount(1.13)
# Entre 6 y 9 Incrementar el dígito menos significativo # Entre 6 y 9 Incrementar el dígito menos significativo
assert form.Amount(1.166).round(2) == form.Amount(1.17) 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 # 5, y el segundo dígito siguiente al dígito menos significativo es cero o par Mantener el dígito menos significativo
# par Mantener el dígito menos significativo
assert str(form.Amount(1.1542).round(2)) == str(form.Amount(1.15)) 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 # 5, y el segundo dígito siguiente al dígito menos significativo es impar Incrementar el dígito menos significativo
# Incrementar el dígito menos significativo
assert str(form.Amount(1.1563).round(2)) == str(form.Amount(1.16)) assert str(form.Amount(1.1563).round(2)) == str(form.Amount(1.16))
def test_amount_truncate(): def test_amount_truncate():
assert form.Amount(1.1569).truncate_as_string(2) == '1.15' assert form.Amount(1.1569).truncate_as_string(2) == '1.15'
assert form.Amount(587.0700).truncate_as_string(2) == '587.07' assert form.Amount(587.0700).truncate_as_string(2) == '587.07'
@@ -55,6 +49,5 @@ def test_amount_truncate():
assert form.Amount(10000.02245).truncate_as_string(2) == '10000.02' assert form.Amount(10000.02245).truncate_as_string(2) == '10000.02'
assert form.Amount(10000.02357).truncate_as_string(2) == '10000.02' assert form.Amount(10000.02357).truncate_as_string(2) == '10000.02'
def test_amount_format(): def test_amount_format():
assert str(round(form.Amount(1.1569),2)) == '1.16' assert str(round(form.Amount(1.1569),2)) == '1.16'

View File

@@ -0,0 +1,21 @@
#!/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')

View File

@@ -4,13 +4,122 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
# from datetime import datetime # from datetime import datetime
# import pytest import pytest
# from facho.fe import form_xml from facho.fe import form_xml
from datetime import datetime
import helpers
from fixtures import simple_invoice
# import helpers simple_invoice = simple_invoice
def test_xml_with_required_elements(simple_invoice):
# def test_xml_with_required_elements(): xml_header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
# doc = form_xml.AttachedDocument(id='123') DIANInvoiceXML = form_xml.DIANInvoiceXML(
# xml = doc.toFachoXML() simple_invoice)
# assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123'
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)

View File

@@ -4,7 +4,12 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from facho.fe.client import dian from facho.fe.client import dian
from facho import facho
import pytest
from datetime import datetime
class FakeDianClient(dian.DianClient): class FakeDianClient(dian.DianClient):
def __init__(self, username, password, resp): def __init__(self, username, password, resp):

View File

@@ -20,19 +20,18 @@ def test_facho_xml():
invoice.text = 'Test' invoice.text = 'Test'
assert xml.tostring() == '<root><Invoice>Test</Invoice></root>' assert xml.tostring() == '<root><Invoice>Test</Invoice></root>'
xml.find_or_create_element('/root/Invoice/Line') invoice_line = xml.find_or_create_element('/root/Invoice/Line')
assert xml.tostring() == '<root><Invoice>Test<Line/></Invoice></root>' assert xml.tostring() == '<root><Invoice>Test<Line/></Invoice></root>'
def test_facho_xml_with_attr(): def test_facho_xml_with_attr():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.find_or_create_element('/root/Invoice[id=123]') invoice = xml.find_or_create_element('/root/Invoice[id=123]')
assert xml.tostring() == '<root><Invoice id="123"/></root>' assert xml.tostring() == '<root><Invoice id="123"/></root>'
def test_facho_xml_idempotent(): def test_facho_xml_idempotent():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.find_or_create_element('/root/Invoice') invoice = xml.find_or_create_element('/root/Invoice')
assert xml.tostring() == '<root><Invoice/></root>' assert xml.tostring() == '<root><Invoice/></root>'
xml.find_or_create_element('/root/Invoice') xml.find_or_create_element('/root/Invoice')
@@ -47,7 +46,6 @@ def test_facho_xml_idempotent():
xml.find_or_create_element('/root/Invoice/Line') xml.find_or_create_element('/root/Invoice/Line')
assert xml.tostring() == '<root><Invoice><Line/></Invoice></root>' assert xml.tostring() == '<root><Invoice><Line/></Invoice></root>'
def test_facho_xml_aliases(): def test_facho_xml_aliases():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.register_alias_xpath('Invoice', '/root/Invoice') xml.register_alias_xpath('Invoice', '/root/Invoice')
@@ -56,42 +54,31 @@ def test_facho_xml_aliases():
invoice.text = 'Test' invoice.text = 'Test'
assert xml.tostring() == '<root><Invoice>Test</Invoice></root>' assert xml.tostring() == '<root><Invoice>Test</Invoice></root>'
def test_facho_xmlns(): def test_facho_xmlns():
xml = facho.FachoXML('root', nsmap={ xml = facho.FachoXML('root', nsmap={
'ext': 'https://ext', 'ext': 'https://ext',
'sts': 'https://sts', 'sts': 'https://sts',
}) })
invoiceAuthorization = xml.find_or_create_element( invoiceAuthorization = xml.find_or_create_element('/root/ext:UBLExtensions/ext:UBLExtension/'
'/root/ext:UBLExtensions/ext:UBLExtension/'
'ext:ExtensionContent/sts:DianExtensions/' 'ext:ExtensionContent/sts:DianExtensions/'
'sts:InvoiceControl/sts:InvoiceAuthorization') 'sts:InvoiceControl/sts:InvoiceAuthorization')
assert xml.tostring().strip() == ( assert xml.tostring().strip() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:UBLExtensions>'\
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">' '<ext:UBLExtension>'\
'<ext:UBLExtensions>' '<ext:ExtensionContent>'\
'<ext:UBLExtension>' '<sts:DianExtensions>'\
'<ext:ExtensionContent>' '<sts:InvoiceControl>'\
'<sts:DianExtensions>' '<sts:InvoiceAuthorization/>'\
'<sts:InvoiceControl>' '</sts:InvoiceControl></sts:DianExtensions></ext:ExtensionContent></ext:UBLExtension></ext:UBLExtensions></root>'
'<sts:InvoiceAuthorization/>'
'</sts:InvoiceControl></sts:DianExtensions>'
'</ext:ExtensionContent></ext:UBLExtension>'
'</ext:UBLExtensions></root>')
invoiceAuthorization.text = '123456789' invoiceAuthorization.text = '123456789'
assert xml.tostring().strip() == ( assert xml.tostring().strip() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:UBLExtensions>'\
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">' '<ext:UBLExtension>'\
'<ext:UBLExtensions>' '<ext:ExtensionContent>'\
'<ext:UBLExtension>' '<sts:DianExtensions>'\
'<ext:ExtensionContent>' '<sts:InvoiceControl>'\
'<sts:DianExtensions>' '<sts:InvoiceAuthorization>123456789</sts:InvoiceAuthorization>'\
'<sts:InvoiceControl>' '</sts:InvoiceControl></sts:DianExtensions></ext:ExtensionContent></ext:UBLExtension></ext:UBLExtensions></root>'
'<sts:InvoiceAuthorization>123456789</sts:InvoiceAuthorization>'
'</sts:InvoiceControl></sts:DianExtensions>'
'</ext:ExtensionContent></ext:UBLExtension>'
'</ext:UBLExtensions></root>')
def test_facho_xmlns_idempotent(): def test_facho_xmlns_idempotent():
xml = facho.FachoXML('root', nsmap={ xml = facho.FachoXML('root', nsmap={
@@ -100,22 +87,16 @@ def test_facho_xmlns_idempotent():
}) })
xml.find_or_create_element('/root/ext:Extension/sts:Sotoros') xml.find_or_create_element('/root/ext:Extension/sts:Sotoros')
assert xml.tostring() == ( assert xml.tostring() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:Extension><sts:Sotoros/></ext:Extension></root>'
'<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') xml.find_or_create_element('/root/ext:Extension/sts:Sotoros')
assert xml.tostring() == ( assert xml.tostring() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:Extension><sts:Sotoros/></ext:Extension></root>'
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
'<ext:Extension><sts:Sotoros/></ext:Extension></root>')
def test_facho_xml_set_element_with_format(): def test_facho_xml_set_element_with_format():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.set_element('/root/Invoice', 1, format_='%02d') invoice = xml.set_element('/root/Invoice', 1, format_='%02d')
assert xml.tostring() == '<root><Invoice>01</Invoice></root>' assert xml.tostring() == '<root><Invoice>01</Invoice></root>'
def test_facho_xml_fragment(): def test_facho_xml_fragment():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
invoice = xml.fragment('/root/Invoice') invoice = xml.fragment('/root/Invoice')
@@ -135,10 +116,7 @@ def test_facho_xml_fragments():
line = xml.fragment('/Invoice/Line', append=True) line = xml.fragment('/Invoice/Line', append=True)
line.set_element('/Line/Id', 3) line.set_element('/Line/Id', 3)
assert xml.tostring() == ( assert xml.tostring() == '<Invoice><Line><Id>1</Id></Line><Line><Id>2</Id></Line><Line><Id>3</Id></Line></Invoice>'
'<Invoice><Line><Id>1</Id></Line><Line><Id>2</Id></Line>'
'<Line><Id>3</Id></Line></Invoice>')
def test_facho_xml_nested_fragments(): def test_facho_xml_nested_fragments():
xml = facho.FachoXML('Invoice') xml = facho.FachoXML('Invoice')
@@ -150,11 +128,7 @@ def test_facho_xml_nested_fragments():
party.set_element('/Party/LastName', 'test') party.set_element('/Party/LastName', 'test')
assert xml.tostring() == ( assert xml.tostring() == '<Invoice><Party><Name>test</Name><Address><Line>line 1</Line></Address><LastName>test</LastName></Party></Invoice>'
'<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(): def test_facho_xml_get_element_text_of_fragment():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -163,7 +137,6 @@ def test_facho_xml_get_element_text_of_fragment():
assert invoice.get_element_text('/Invoice/Id') == '1' assert invoice.get_element_text('/Invoice/Id') == '1'
def test_facho_xml_get_element_text(): def test_facho_xml_get_element_text():
xml = facho.FachoXML('Invoice') xml = facho.FachoXML('Invoice')
xml.set_element('/Invoice/ID', 'ABC123') xml.set_element('/Invoice/ID', 'ABC123')
@@ -174,7 +147,6 @@ def test_facho_xml_get_element_text():
line.set_element('/Line/Quantity', 5) line.set_element('/Line/Quantity', 5)
assert line.get_element_text('/Line/Quantity', format_=int) == 5 assert line.get_element_text('/Line/Quantity', format_=int) == 5
def test_facho_xml_get_element_text_next_child(): def test_facho_xml_get_element_text_next_child():
xml = facho.FachoXML('Invoice') xml = facho.FachoXML('Invoice')
xml.set_element('/Invoice/ID', 'ABC123') xml.set_element('/Invoice/ID', 'ABC123')
@@ -194,32 +166,25 @@ def test_facho_xml_set_element_relative():
assert xml.get_element_text('/Invoice/ID') == 'ABC123' assert xml.get_element_text('/Invoice/ID') == 'ABC123'
def test_facho_xml_set_element_relative_with_namespace(): def test_facho_xml_set_element_relative_with_namespace():
xml = facho.FachoXML( xml = facho.FachoXML('{%s}Invoice' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
'{%s}Invoice' % nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
nsmap={
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
xml.set_element('./ID', 'ABC123') xml.set_element('./ID', 'ABC123')
assert xml.get_element_text('/fe:Invoice/ID') == 'ABC123' assert xml.get_element_text('/fe:Invoice/ID') == 'ABC123'
def test_facho_xml_fragment_relative(): def test_facho_xml_fragment_relative():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
invoice = xml.fragment('./Invoice') invoice = xml.fragment('./Invoice')
invoice.set_element('./Id', 1) invoice.set_element('./Id', 1)
assert xml.tostring() == '<root><Invoice><Id>1</Id></Invoice></root>' assert xml.tostring() == '<root><Invoice><Id>1</Id></Invoice></root>'
def test_facho_xml_get_element_fragment_relative(): def test_facho_xml_get_element_fragment_relative():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
invoice = xml.fragment('./Invoice') invoice = xml.fragment('./Invoice')
invoice.set_element('./Id', 1) invoice.set_element('./Id', 1)
assert invoice.get_element_text('./Id') == '1' assert invoice.get_element_text('./Id') == '1'
def test_facho_xml_replacement_for(): def test_facho_xml_replacement_for():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./child/type') xml.placeholder_for('./child/type')
@@ -227,47 +192,37 @@ def test_facho_xml_replacement_for():
'./child/code', 'test') './child/code', 'test')
assert xml.tostring() == '<root><child><code>test</code></child></root>' assert xml.tostring() == '<root><child><code>test</code></child></root>'
def test_facho_xml_set_element_content_invalid_validation(): def test_facho_xml_set_element_content_invalid_validation():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
with pytest.raises(facho.FachoValueInvalid): with pytest.raises(facho.FachoValueInvalid) as e:
xml.set_element_validator('./Id', lambda text, attrs: text == 'mero') xml.set_element_validator('./Id', lambda text, attrs: text == 'mero')
xml.set_element('./Id', 'bad') xml.set_element('./Id', 'bad')
def test_facho_xml_set_element_content_valid_validation(): def test_facho_xml_set_element_content_valid_validation():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.set_element_validator('./Id', lambda text, attrs: text == 'mero') xml.set_element_validator('./Id', lambda text, attrs: text == 'mero')
xml.set_element('./Id', 'mero') xml.set_element('./Id', 'mero')
def test_facho_xml_set_element_attribute_invalid_validation(): def test_facho_xml_set_element_attribute_invalid_validation():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
with pytest.raises(facho.FachoValueInvalid): with pytest.raises(facho.FachoValueInvalid) as e:
xml.set_element_validator( xml.set_element_validator('./Id', lambda text, attrs: attrs['code'] == 'ABC')
'./Id', lambda text, attrs: attrs['code'] == 'ABC')
xml.set_element('./Id', 'mero', code = 'CBA') xml.set_element('./Id', 'mero', code = 'CBA')
def test_facho_xml_set_element_attribute_valid_validation(): def test_facho_xml_set_element_attribute_valid_validation():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.set_element_validator( xml.set_element_validator('./Id', lambda text, attrs: attrs['code'] == 'ABC')
'./Id',
lambda text,
attrs: attrs['code'] == 'ABC')
xml.set_element('./Id', 'mero', code = 'ABC') xml.set_element('./Id', 'mero', code = 'ABC')
def test_facho_xml_get_element_attribute(): def test_facho_xml_get_element_attribute():
xml = facho.FachoXML('root') 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' assert xml.get_element_attribute('/root/Id', 'code') == 'ABC'
def test_facho_xml_keep_orden_slibing(): def test_facho_xml_keep_orden_slibing():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.find_or_create_element('./A') xml.find_or_create_element('./A')
@@ -278,7 +233,6 @@ def test_facho_xml_keep_orden_slibing():
assert xml.tostring() == '<root><A/><A/><B/><B/><C/></root>' assert xml.tostring() == '<root><A/><A/><B/><B/><C/></root>'
def test_facho_xml_placeholder_optional(): def test_facho_xml_placeholder_optional():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
@@ -287,7 +241,6 @@ def test_facho_xml_placeholder_optional():
assert xml.tostring() == '<root><A/><C/></root>' assert xml.tostring() == '<root><A/><C/></root>'
def test_facho_xml_placeholder_append_to_optional(): def test_facho_xml_placeholder_append_to_optional():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
@@ -297,7 +250,6 @@ def test_facho_xml_placeholder_append_to_optional():
xml.find_or_create_element('./B') xml.find_or_create_element('./B')
assert xml.tostring() == '<root><A/><B/><C/></root>' assert xml.tostring() == '<root><A/><B/><C/></root>'
def test_facho_xml_placeholder_set_element_to_optional(): def test_facho_xml_placeholder_set_element_to_optional():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
@@ -307,7 +259,6 @@ def test_facho_xml_placeholder_set_element_to_optional():
xml.set_element('./B', '2') xml.set_element('./B', '2')
assert xml.tostring() == '<root><A/><B>2</B><C/></root>' assert xml.tostring() == '<root><A/><B>2</B><C/></root>'
def test_facho_xml_placeholder_set_element_to_optional_with_append(): def test_facho_xml_placeholder_set_element_to_optional_with_append():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
@@ -343,7 +294,6 @@ def test_facho_xml_set_attributes_not_set_optional():
xml.get_element_attribute('/root/A', 'value1') xml.get_element_attribute('/root/A', 'value1')
assert xml.get_element_attribute('/root/A', 'value2') == '2' assert xml.get_element_attribute('/root/A', 'value2') == '2'
def test_facho_xml_placeholder_with_fragment(): def test_facho_xml_placeholder_with_fragment():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
@@ -356,9 +306,7 @@ def test_facho_xml_placeholder_with_fragment():
AA = xml.fragment('./AA/Child', append=True) AA = xml.fragment('./AA/Child', append=True)
assert xml.tostring() == ( assert xml.tostring() == '<root><A/><AA><Child><B/><B/></Child><Child/></AA><AAA/></root>'
'<root><A/><AA><Child><B/><B/></Child><Child/></AA><AAA/></root>')
def test_facho_xml_create_on_first_append(): def test_facho_xml_create_on_first_append():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -366,7 +314,6 @@ def test_facho_xml_create_on_first_append():
xml.find_or_create_element('./A', append=True) xml.find_or_create_element('./A', append=True)
assert xml.tostring() == '<root><A/></root>' assert xml.tostring() == '<root><A/></root>'
def test_facho_xml_create_on_first_append_multiple_appends(): def test_facho_xml_create_on_first_append_multiple_appends():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -377,7 +324,6 @@ def test_facho_xml_create_on_first_append_multiple_appends():
xml.find_or_create_element('./C', append=True) xml.find_or_create_element('./C', append=True)
assert xml.tostring() == '<root><B/><A/><A/><A/><C/></root>' assert xml.tostring() == '<root><B/><A/><A/><A/><C/></root>'
def test_facho_xml_fragment_create_on_first_append(): def test_facho_xml_fragment_create_on_first_append():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -387,7 +333,6 @@ def test_facho_xml_fragment_create_on_first_append():
A.find_or_create_element('./C') A.find_or_create_element('./C')
assert xml.tostring() == '<root><A><B/></A><A><C/></A></root>' assert xml.tostring() == '<root><A><B/></A><A><C/></A></root>'
def test_facho_xml_placeholder_optional_and_fragment(): def test_facho_xml_placeholder_optional_and_fragment():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -401,7 +346,6 @@ def test_facho_xml_placeholder_optional_and_fragment():
assert xml.tostring() == '<root><A><AA><B/><C/></AA></A></root>' assert xml.tostring() == '<root><A><AA><B/><C/></AA></A></root>'
def test_facho_xml_placeholder_optional_and_set_attributes(): def test_facho_xml_placeholder_optional_and_set_attributes():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
@@ -410,7 +354,6 @@ def test_facho_xml_placeholder_optional_and_set_attributes():
assert xml.get_element_attribute('/root/A', 'prueba') == 'OK' assert xml.get_element_attribute('/root/A', 'prueba') == 'OK'
assert xml.tostring() == '<root><A prueba="OK"/></root>' assert xml.tostring() == '<root><A prueba="OK"/></root>'
def test_facho_xml_placeholder_optional_and_fragment_with_set_element(): def test_facho_xml_placeholder_optional_and_fragment_with_set_element():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -422,19 +365,17 @@ def test_facho_xml_placeholder_optional_and_fragment_with_set_element():
assert xml.tostring() == '<root><A><AA prueba="OK"/></A></root>' assert xml.tostring() == '<root><A><AA prueba="OK"/></A></root>'
assert xml.get_element_attribute('/root/A/AA', 'prueba') == 'OK' assert xml.get_element_attribute('/root/A/AA', 'prueba') == 'OK'
def test_facho_xml_exist_element(): def test_facho_xml_exist_element():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
xml.placeholder_for('./A') xml.placeholder_for('./A')
assert xml.exist_element('/root/A') is False assert xml.exist_element('/root/A') == False
assert xml.tostring() == '<root><A/></root>' assert xml.tostring() == '<root><A/></root>'
xml.find_or_create_element('./A') xml.find_or_create_element('./A')
assert xml.exist_element('/root/A') assert xml.exist_element('/root/A') == True
assert xml.tostring() == '<root><A/></root>' assert xml.tostring() == '<root><A/></root>'
def test_facho_xml_query_element_text_or_attribute(): def test_facho_xml_query_element_text_or_attribute():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -443,7 +384,6 @@ 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') == 'contenido'
assert xml.get_element_text_or_attribute('/root/A/@clave') == 'valor' assert xml.get_element_text_or_attribute('/root/A/@clave') == 'valor'
def test_facho_xml_query_element_text_or_attribute_from_fragment(): def test_facho_xml_query_element_text_or_attribute_from_fragment():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -452,7 +392,6 @@ def test_facho_xml_query_element_text_or_attribute_from_fragment():
assert invoice.get_element_text_or_attribute('/Invoice/A') == 'contenido' assert invoice.get_element_text_or_attribute('/Invoice/A') == 'contenido'
def test_facho_xml_build_xml_absolute(): def test_facho_xml_build_xml_absolute():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -461,23 +400,18 @@ def test_facho_xml_build_xml_absolute():
def test_facho_xml_build_xml_absolute_namespace(): def test_facho_xml_build_xml_absolute_namespace():
xml = facho.FachoXML( xml = facho.FachoXML('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
'{%s}root' % nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
nsmap={
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
xpath = xml.xpath_from_root('/A') xpath = xml.xpath_from_root('/A')
assert xpath == '/fe:root/A' assert xpath == '/fe:root/A'
def test_facho_xml_build_xml_absolute_namespace_from_fragment(): def test_facho_xml_build_xml_absolute_namespace_from_fragment():
xml = facho.FachoXML( xml = facho.FachoXML('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
'{%s}root' % nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
nsmap={
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
invoice = xml.fragment('/root/Invoice') invoice = xml.fragment('/root/Invoice')
xpath = invoice.xpath_from_root('/A') xpath = invoice.xpath_from_root('/A')
assert xpath == '/fe:root/Invoice/A' assert xpath == '/fe:root/Invoice/A'

View File

@@ -4,14 +4,16 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from datetime import datetime from datetime import datetime
import pytest
from facho import fe from facho import fe
import helpers
def test_xmlsigned_build(monkeypatch): def test_xmlsigned_build(monkeypatch):
# openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 # openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 -newkey rsa:2048 -keyout example.key -out example.pem
# -newkey rsa:2048 -keyout example.key -out example.pem # openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem
# openssl pkcs12 -export -out example.p12 -inkey example.key -in
# example.pem
signer = fe.DianXMLExtensionSigner('./tests/example.p12') signer = fe.DianXMLExtensionSigner('./tests/example.p12')
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
@@ -19,90 +21,68 @@ def test_xmlsigned_build(monkeypatch):
signer.sign_xml_element(xml.root) signer.sign_xml_element(xml.root)
elem = xml.find_or_create_element( elem = xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/ds:Signature')
assert elem is not None 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): def test_xmlsigned_with_passphrase_build(monkeypatch):
# openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 #openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 -newkey rsa:2048 -keyout example.key -out example.pem
# -newkey rsa:2048 -keyout example.key -out example.pem #openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem
# openssl pkcs12 -export -out example.p12 -inkey example.key -in signer = fe.DianXMLExtensionSigner('./tests/example-with-passphrase.p12', 'test')
# example.pem
signer = fe.DianXMLExtensionSigner(
'./tests/example-with-passphrase.p12', 'test')
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
signer.sign_xml_element(xml.root) signer.sign_xml_element(xml.root)
elem = xml.find_or_create_element( elem = xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/ds:Signature')
assert elem is not None 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(): def test_dian_extension_software_security_code():
security_code = fe.DianXMLExtensionSoftwareSecurityCode( security_code = fe.DianXMLExtensionSoftwareSecurityCode('idsoftware', '1234', '1')
'idsoftware', '1234', '1')
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.add_extension(security_code) xml.add_extension(security_code)
content = xml.get_element_text( content = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/sts:DianExtensions/sts:SoftwareSecurityCode')
assert content is not None assert content is not None
def test_dian_extension_invoice_authorization(): def test_dian_extension_invoice_authorization():
invoice_authorization = '18762002346472' invoice_authorization = '18762002346472'
inv_auth_ext = fe.DianXMLExtensionInvoiceAuthorization( inv_auth_ext = fe.DianXMLExtensionInvoiceAuthorization(invoice_authorization,
invoice_authorization, datetime( datetime(2017, 2, 23),
2017, 2, 23), datetime( datetime(2019, 8, 23),
2019, 8, 23), 'MD', 100001, 174999) 'MD', 100001, 174999)
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.add_extension(inv_auth_ext) xml.add_extension(inv_auth_ext)
auth = xml.get_element_text( auth = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl/sts:InvoiceAuthorization')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/sts:DianExtensions/sts:InvoiceControl/sts:InvoiceAuthorization')
assert auth == invoice_authorization assert auth == invoice_authorization
def test_dian_extension_software_provider(): def test_dian_extension_software_provider():
nit = '123456789' nit = '123456789'
id_software = 'ABCDASDF123' id_software = 'ABCDASDF123'
software_provider_extension = fe.DianXMLExtensionSoftwareProvider( software_provider_extension = fe.DianXMLExtensionSoftwareProvider(nit, '', id_software)
nit, '', id_software)
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.add_extension(software_provider_extension) xml.add_extension(software_provider_extension)
give_nit = xml.get_element_text( give_nit = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider/sts:ProviderID')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/sts:DianExtensions/sts:SoftwareProvider/sts:ProviderID')
assert nit == give_nit assert nit == give_nit
def test_dian_extension_authorization_provider(): def test_dian_extension_authorization_provider():
auth_provider_extension = fe.DianXMLExtensionAuthorizationProvider() auth_provider_extension = fe.DianXMLExtensionAuthorizationProvider()
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.add_extension(auth_provider_extension) xml.add_extension(auth_provider_extension)
dian_nit = xml.get_element_text( dian_nit = xml.get_element_text('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/'
'ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/'
'sts:AuthorizationProviderID')
assert dian_nit == '800197268' assert dian_nit == '800197268'
def test_dian_invoice_without_namespace_in_root(): def test_dian_invoice_without_namespace_in_root():
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
@@ -113,13 +93,9 @@ def test_dian_invoice_without_namespace_in_root():
def test_xml_sign_dian(monkeypatch): def test_xml_sign_dian(monkeypatch):
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.find_or_create_element( xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True)
ublextension = xml.fragment( extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension',
append=True)
ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
xmlstring = xml.tostring() xmlstring = xml.tostring()
print(xmlstring) print(xmlstring)
@@ -127,17 +103,12 @@ def test_xml_sign_dian(monkeypatch):
xmlsigned = signer.sign_xml_string(xmlstring) xmlsigned = signer.sign_xml_string(xmlstring)
assert "Signature" in xmlsigned assert "Signature" in xmlsigned
def test_xml_sign_dian_using_bytes(monkeypatch): def test_xml_sign_dian_using_bytes(monkeypatch):
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.find_or_create_element( xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True)
ublextension = xml.fragment( extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension',
append=True)
ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
xmlstring = xml.tostring() xmlstring = xml.tostring()
p12_data = open('./tests/example.p12', 'rb').read() p12_data = open('./tests/example.p12', 'rb').read()
@@ -146,7 +117,6 @@ def test_xml_sign_dian_using_bytes(monkeypatch):
xmlsigned = signer.sign_xml_string(xmlstring) xmlsigned = signer.sign_xml_string(xmlstring)
assert "Signature" in xmlsigned assert "Signature" in xmlsigned
def test_xml_signature_timestamp(monkeypatch): def test_xml_signature_timestamp(monkeypatch):
xml = fe.FeXML( xml = fe.FeXML(
'Invoice', 'Invoice',
@@ -160,6 +130,3 @@ def test_xml_signature_timestamp(monkeypatch):
xmlstring = xml.tostring() xmlstring = xml.tostring()
signer = fe.DianXMLExtensionSigner('./tests/example.p12') signer = fe.DianXMLExtensionSigner('./tests/example.p12')
xmlsigned = signer.sign_xml_string(xmlstring) xmlsigned = signer.sign_xml_string(xmlstring)
with open('invoice.xml', 'w') as file_:
file_.write(xmlsigned)

View File

@@ -41,13 +41,11 @@ def test_invoicesimple_build(simple_invoice):
xml = DIANInvoiceXML(simple_invoice) xml = DIANInvoiceXML(simple_invoice)
supplier_name = xml.get_element_text( supplier_name = xml.get_element_text(
'/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc' '/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name')
':Name')
assert supplier_name == simple_invoice.invoice_supplier.name assert supplier_name == simple_invoice.invoice_supplier.name
customer_name = xml.get_element_text( customer_name = xml.get_element_text(
'/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc' '/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name')
':Name')
assert customer_name == simple_invoice.invoice_customer.name assert customer_name == simple_invoice.invoice_customer.name
@@ -68,8 +66,7 @@ def test_invoicesimple_xml_signed(monkeypatch, simple_invoice):
xml.add_extension(signer) xml.add_extension(signer)
elem = xml.get_element( elem = xml.get_element(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext' '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature')
':ExtensionContent/ds:Signature')
assert elem.text is not None assert elem.text is not None
@@ -78,16 +75,14 @@ def test_invoicesimple_zip(simple_invoice):
zipdata = io.BytesIO() zipdata = io.BytesIO()
with fe.DianZIP(zipdata) as dianzip: with fe.DianZIP(zipdata) as dianzip:
name_invoice = dianzip.add_invoice_xml( name_invoice = dianzip.add_invoice_xml(simple_invoice.invoice_ident, str(xml_invoice))
simple_invoice.invoice_ident, str(xml_invoice))
# el zip ademas de archivar debe comprimir los archivos # el zip ademas de archivar debe comprimir los archivos
# de lo contrario la DIAN lo rechaza # de lo contrario la DIAN lo rechaza
with zipfile.ZipFile(zipdata) as dianzip: with zipfile.ZipFile(zipdata) as dianzip:
dianzip.testzip() dianzip.testzip()
for zipinfo in dianzip.infolist(): for zipinfo in dianzip.infolist():
assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, ( assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, "se espera el zip comprimido"
"se espera el zip comprimido")
with zipfile.ZipFile(zipdata) as dianzip: with zipfile.ZipFile(zipdata) as dianzip:
xml_data = dianzip.open(name_invoice).read().decode('utf-8') xml_data = dianzip.open(name_invoice).read().decode('utf-8')
@@ -127,8 +122,7 @@ def test_invoice_invoice_type_code(simple_invoice):
def test_invoice_totals(simple_invoice_without_lines): def test_invoice_totals(simple_invoice_without_lines):
simple_invoice = simple_invoice_without_lines simple_invoice = simple_invoice_without_lines
simple_invoice.invoice_ident = '323200000129' simple_invoice.invoice_ident = '323200000129'
simple_invoice.invoice_issue = datetime.strptime( simple_invoice.invoice_issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
'2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_supplier.ident = '700085371' simple_invoice.invoice_supplier.ident = '700085371'
simple_invoice.invoice_customer.ident = '800199436' simple_invoice.invoice_customer.ident = '800199436'
simple_invoice.add_invoice_line(form.InvoiceLine( simple_invoice.add_invoice_line(form.InvoiceLine(
@@ -228,8 +222,7 @@ def test_invoice_cufe(simple_invoice_without_lines):
assert formatVars[12] == '800199436', "NumAdq" assert formatVars[12] == '800199436', "NumAdq"
# ClTec # ClTec
assert formatVars[13] == ( assert formatVars[13] == '693ff6f2a553c3646a063436fd4dd9ded0311471', "ClTec"
'693ff6f2a553c3646a063436fd4dd9ded0311471'), "ClTec"
# TipoAmbiente # TipoAmbiente
assert formatVars[14] == '1', "TipoAmbiente" assert formatVars[14] == '1', "TipoAmbiente"
@@ -330,13 +323,8 @@ def test_debit_note_cude(simple_debit_note_without_lines):
assert build_vars['TipoAmb'] == 2 assert build_vars['TipoAmb'] == 2
cude_composicion = "".join(cude_extension.formatVars()) cude_composicion = "".join(cude_extension.formatVars())
assert cude_composicion == ( assert cude_composicion == 'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.0090019726410254102102012'
'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.009'
'001'
'9726410254102102012')
xml_invoice.add_extension(cude_extension) xml_invoice.add_extension(cude_extension)
cude = xml_invoice.get_element_text('/fe:DebitNote/cbc:UUID') cude = xml_invoice.get_element_text('/fe:DebitNote/cbc:UUID')
assert cude == ( assert cude == '3fa73a86d57d9341c536afde1f85c4efd9d4591c2c22bce4dfb0e6b0d2e83b8f047a8bde7098292e9d2493e60d1c31da'
'3fa73a86d57d9341c536afde1f85c4efd9d4591c2c22bce4dfb0e6b0d2e83b8f047a8'
'bde7098292e9d2493e60d1c31da')

View File

@@ -121,9 +121,7 @@ def test_FAU14():
wants = form.Amount(119.0 + 19.0 - 50.0) wants = form.Amount(119.0 + 19.0 - 50.0)
assert 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)
"got %s want %s" % (
inv.invoice_legal_monetary_total.payable_amount, wants))
def test_invalid_tipo_operacion_nota_debito(): def test_invalid_tipo_operacion_nota_debito():

View File

@@ -6,7 +6,10 @@
"""Tests for `facho` package.""" """Tests for `facho` package."""
import pytest import pytest
# from datetime import datetime
import copy
from facho.fe import form
from facho.fe import form_xml from facho.fe import form_xml
# from fixtures import * # from fixtures import *

601
tests/test_model.py Normal file
View File

@@ -0,0 +1,601 @@
#!/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()

119
tests/test_model_invoice.py Normal file
View File

@@ -0,0 +1,119 @@
#!/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'

View File

@@ -28,12 +28,8 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30'
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', # assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00'
# 'DiasTrabajados') == '30'
# assert
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico',
# 'SueldoTrabajado') == '1000000.00'
# def test_adicionar_devengado_transporte(): # def test_adicionar_devengado_transporte():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -44,9 +40,7 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte', 'AuxilioTransporte') == '2000000.0'
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte',
# 'AuxilioTransporte') == '2000000.0'
# def test_adicionar_devengado_comprobante_total(): # def test_adicionar_devengado_comprobante_total():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -64,9 +58,7 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '1000000.00'
# xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') ==
# '1000000.00'
# def test_adicionar_devengado_comprobante_total_cero(): # def test_adicionar_devengado_comprobante_total_cero():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -83,8 +75,7 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00'
# xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00'
# def test_adicionar_devengado_transporte_muchos(): # def test_adicionar_devengado_transporte_muchos():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -99,8 +90,7 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml) # print(xml)
# assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal') # assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal') == '5000000.00'
# == '5000000.00'
# def test_adicionar_deduccion_salud(): # def test_adicionar_deduccion_salud():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -117,9 +107,7 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml) # print(xml)
# assert # assert xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') == '1000.00'
# xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') ==
# '1000.00'
# def test_nomina_obligatorios_segun_anexo_tecnico(): # def test_nomina_obligatorios_segun_anexo_tecnico():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -225,62 +213,26 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# expected_cune = # expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab'
# 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484 # assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune
# b5da15ff585e773b0b0ab' # assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102'
# assert # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001'
# xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001'
# 'CUNE') == expected_cune # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO'
# assert # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05'
# xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001'
# 'TipoXML') == '102' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999'
# assert # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2'
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaX # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx'
# ML/@Numero') == 'N00001' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None
# assert # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') == f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_cune}"
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaX # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT') == '700085371'
# ML/@Consecutivo') == '00001' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436'
# assert # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True'
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov') == 'N0111'
# 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 # # confirmar el namespace
# assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in # assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in xml.tostring()
# xml.tostring()
# def test_asignar_pago(): # def test_asignar_pago():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -297,11 +249,11 @@
# xml.add_extension(signer) # xml.add_extension(signer)
# print(xml.tostring()) # print(xml.tostring())
# elem = # elem = xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature')
# xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension
# /ext:ExtensionContent/ds:Signature')
# assert elem is not None # assert elem is not None
# def test_nomina_devengado_horas_extras_nocturnas(): # def test_nomina_devengado_horas_extras_nocturnas():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -325,9 +277,7 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = # extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN', multiple=True)
# xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
# assert extras[0].get('Cantidad') == '1' # assert extras[0].get('Cantidad') == '1'
@@ -362,9 +312,7 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = # extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN', multiple=True)
# xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
# assert extras[0].get('Cantidad') == '1' # assert extras[0].get('Cantidad') == '1'
@@ -379,9 +327,7 @@
# def test_nomina_devengado_horas_extras_diarias_dominicales_y_festivos(): # def test_nomina_devengado_horas_extras_diarias_dominicales_y_festivos():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
# # nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiariasDominicalesYFestivos(
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiariasDominicalesY
# Festivos(
# horas_extras=[ # horas_extras=[
# fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
# hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
@@ -401,9 +347,7 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = # extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF', multiple=True)
# xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
# assert extras[0].get('Cantidad') == '1' # assert extras[0].get('Cantidad') == '1'
@@ -418,9 +362,7 @@
# def test_nomina_devengado_horas_recargo_diarias_dominicales_y_festivos(): # def test_nomina_devengado_horas_recargo_diarias_dominicales_y_festivos():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
# # nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoDiariasDominicalesYFestivos(
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoDiariasDominicales
# YFestivos(
# horas_extras=[ # horas_extras=[
# fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
# hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
@@ -440,9 +382,7 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = # extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF', multiple=True)
# xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
# assert extras[0].get('Cantidad') == '1' # assert extras[0].get('Cantidad') == '1'
@@ -458,9 +398,7 @@
# def test_nomina_devengado_horas_extras_nocturnas_dominicales_y_festivos(): # def test_nomina_devengado_horas_extras_nocturnas_dominicales_y_festivos():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
# # nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnasDominicalesYFestivos(
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnasDominicale
# sYFestivos(
# horas_extras=[ # horas_extras=[
# fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
# hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
@@ -480,9 +418,7 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = # extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF', multiple=True)
# xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
# assert extras[0].get('Cantidad') == '1' # assert extras[0].get('Cantidad') == '1'
@@ -497,9 +433,7 @@
# def test_nomina_devengado_horas_recargo_nocturno_dominicales_y_festivos(): # def test_nomina_devengado_horas_recargo_nocturno_dominicales_y_festivos():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
# # nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturnoDominicalesYFestivos(
# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturnoDominicale
# sYFestivos(
# horas_extras=[ # horas_extras=[
# fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
# hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
@@ -519,9 +453,7 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = # extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF', multiple=True)
# xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
# assert extras[0].get('Cantidad') == '1' # assert extras[0].get('Cantidad') == '1'

View File

@@ -101,9 +101,7 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/InformacionGeneral', 'TipoXML') == '103'
# xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar
# /InformacionGeneral', 'TipoXML') == '103'
# def test_adicionar_reemplazar_devengado_comprobante_total(): # def test_adicionar_reemplazar_devengado_comprobante_total():
@@ -121,17 +119,13 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ComprobanteTotal') == '1000000.00'
# xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Comp
# robanteTotal') == '1000000.00'
# def test_adicionar_reemplazar_asignar_predecesor(): # def test_adicionar_reemplazar_asignar_predecesor():
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
# # nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor(
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.
# Predecesor(
# numero = '123456', # numero = '123456',
# cune = 'ABC123456', # cune = 'ABC123456',
# fecha_generacion = '2021-11-16' # fecha_generacion = '2021-11-16'
@@ -139,23 +133,15 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@NumeroPred') == '123456'
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456'
# emplazar/ReemplazandoPredecesor/@NumeroPred') == '123456' # 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/@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(): # def test_adicionar_reemplazar_eliminar_predecesor_opcional():
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
# # nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor(
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.
# Predecesor(
# numero = '123456', # numero = '123456',
# cune = 'ABC123456', # cune = 'ABC123456',
# fecha_generacion = '2021-11-16' # fecha_generacion = '2021-11-16'
@@ -164,19 +150,13 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Reemplaza # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is None
# ndoPredecesor') is not None
# assert
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoP
# redecesor') is None
# def test_adicionar_eliminar_reemplazar_predecesor_opcional(): # def test_adicionar_eliminar_reemplazar_predecesor_opcional():
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
# # nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor(
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Pr
# edecesor(
# numero = '123456', # numero = '123456',
# cune = 'ABC123456', # cune = 'ABC123456',
# fecha_generacion = '2021-11-16' # fecha_generacion = '2021-11-16'
@@ -184,12 +164,8 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoP # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is None
# redecesor') is not None
# assert
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Reemplaza
# ndoPredecesor') is None
# def test_adicionar_eliminar_devengado_comprobante_total(): # def test_adicionar_eliminar_devengado_comprobante_total():
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
@@ -206,16 +182,12 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert # assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/ComprobanteTotal') == '1000000.00'
# xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/Compro
# banteTotal') == '1000000.00'
# def test_adicionar_eliminar_asignar_predecesor(): # def test_adicionar_eliminar_asignar_predecesor():
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
# # nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor(
# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Pr
# edecesor(
# numero = '123456', # numero = '123456',
# cune = 'ABC123456', # cune = 'ABC123456',
# fecha_generacion = '2021-11-16' # fecha_generacion = '2021-11-16'
@@ -223,15 +195,9 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@NumeroPred') == '123456'
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456'
# iminar/EliminandoPredecesor/@NumeroPred') == '123456' # 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/@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(): # def test_nomina_devengado_horas_extras_diarias():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()

22
tox.ini
View File

@@ -1,18 +1,26 @@
[tox] [tox]
envlist = py310, py311, py312, py313, flake8 envlist = py39, py310, py311, py312, flake8
[flake8] [travis]
exclude = docs, build, venv, .tox, .git, .pytest_cache, *.egg-info python =
3.9: py39
3.10: py310
3.11: py311
3.12: py312
[testenv:flake8] [testenv:flake8]
basepython = python3.13 basepython = python
deps = flake8>=7.0.0 deps = flake8
commands = flake8 facho tests examples commands = flake8 facho
[testenv] [testenv]
setenv = setenv =
PYTHONPATH = {toxinidir} PYTHONPATH = {toxinidir}
deps = pytest>=9.1.1,<10.0.0 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
commands = commands =
pip install -U pip pip install -U pip
py.test --basetemp={envtmpdir} py.test --basetemp={envtmpdir}