4 Commits

Author SHA1 Message Date
c4a29e4a6f Fix: Formateo PEP8 (79 cols) y limpieza flake8 en tests y examples
- Líneas >79 cols reajustadas (asserts, strings CUDE/CUDS, comentarios)
- Elimina imports y variables muertas (F401/F841), E712 (== False -> is False)
- Reordena imports en docs/conf.py
2026-08-12 19:56:33 -05:00
4a7c2e2947 Fix: Formateo PEP8 (79 cols) y limpieza flake8 en paquete facho
- Líneas >79 cols reajustadas en todo el paquete
- Fix F821: etree.cleanup_namespaces(security) en signature.py (era 'header', NameError)
- Fix bug cune_xpath -> xpath en nomina/informacion_general() (NameError)
- Elimina clase TaxScheme duplicada en form/__init__.py
- Imports explícitos (F403/F405) y __all__ en form_xml/invoice.py, nomina
- Elimina imports y variables muertas (F401/F841), E712 (== None -> is None)
2026-08-12 19:56:29 -05:00
26ce89ca33 Fix: Migración a pyproject.toml y poetry
- Reemplaza setup.py/setup.cfg por pyproject.toml (PEP 621) + poetry.lock
- Dev dependencies en [tool.poetry.group.dev.dependencies]
- Elimina requirements_dev.txt (deps dev en pyproject.toml)
- Actualiza tox.ini, Makefile, Makefile.dev, Dockerfile y docs
2026-08-12 19:56:23 -05:00
93a3d18e32 Compatibilidad con Python 3.13
- Actualiza dependencias: cryptography>=41.0.0, pyOpenSSL>=23.2.0,
  xmlsec>=1.3.12, xmlsig>=0.1.9, zeep>=4.2.1, lxml>=5.2.2
- Anade monkey-patch para xades.load_pkcs12 con pyOpenSSL >= 24
  (PKCS12 eliminado de OpenSSL.crypto)
- Reemplaza datetime.utcnow() por datetime.now(timezone.utc)
- Reemplaza APIs deprecadas de lxml en facho.py y codelist
- Migra verificacion PKCS12 de pyOpenSSL a cryptography API en fe.py
- Moderniza typing: Union/Optional/List a sintaxis | y list[]
- Actualiza requirements_dev.txt con versiones recientes
- Actualiza tox.ini y Makefile.dev para Python 3.13
2026-07-02 22:45:33 -05:00
80 changed files with 3910 additions and 3762 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
$ python setup.py develop $ pip install -e .
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
$ python setup.py test or py.test $ 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 build 1. make -f Makefile.dev dev-setup
2. make -f Makefile.dev dev-shell 2. make -f Makefile.dev dev-shell
3. make -f Makefile.dev python3.8 setup.py develop 3. make -f Makefile.dev test
4. make -f Makefile.dev python3.8 setup.py test 4. make -f Makefile.dev tox
Pull Request Guidelines Pull Request Guidelines
----------------------- -----------------------

View File

@@ -7,34 +7,45 @@ 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 \
build-essential \ xmlsec1 \
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
======= =======
* 0.2.1 version usada en produccion. * First release on PyPI.

View File

@@ -80,9 +80,8 @@ 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 setup.py sdist python -m build
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
python setup.py install pip 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.12 setup.py test' docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.13 -m pytest -vv'
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
$ python setup.py install $ pip install .
.. _Github repo: https://github.com/bit4bit/facho .. _Github repo: https://github.com/bit4bit/facho

View File

@@ -11,13 +11,18 @@ 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, date from datetime import datetime
# Datos del fomulario del SET de pruebas # Datos del fomulario del SET de pruebas
INVOICE_AUTHORIZATION = '181360000001' # Número suministrado por la Dian en el momento de la creación del SET de Pruebas # Número suministrado por la Dian en el momento de la creación del SET de
ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas # Pruebas
PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas INVOICE_AUTHORIZATION = '181360000001'
CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por la Dian en el momento de la creación del SET de Pruebas # 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
@@ -26,15 +31,24 @@ CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por
# 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(ID_SOFTWARE, PIN, inv.invoice_ident) security_code = fe.DianXMLExtensionSoftwareSecurityCode(
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('nit_empresa', 'dígito_verificación', ID_SOFTWARE) software_provider = fe.DianXMLExtensionSoftwareProvider(
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(INVOICE_AUTHORIZATION, 'nit_empresa', 'dígito_verificación', ID_SOFTWARE)
datetime(2019, 1, 19),#Datos toamdos de inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(
datetime(2030, 1, 19),#la configuración INVOICE_AUTHORIZATION,
# 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 [security_code, authorization_provider, cufe, software_provider, inv_authorization] return [
security_code,
authorization_provider,
cufe,
software_provider,
inv_authorization]
def invoice(): def invoice():

View File

@@ -1,9 +1,11 @@
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,39 +1,37 @@
# -*- 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, {
'formatters': { "version": 1,
'verbose': { "formatters": {"verbose": {"format": "%(name)s: %(message)s"}},
'format': '%(name)s: %(message)s' "handlers": {
} "console": {
}, "level": "DEBUG",
'handlers': { "class": "logging.StreamHandler",
'console': { "formatter": "verbose",
'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:
@@ -41,99 +39,122 @@ 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(nit, nit_proveedor, id_software, username, password): def consultaResolucionesFacturacion(
nit, nit_proveedor, id_software, username, password
):
from facho.fe.client import dian from facho.fe.client import dian
client_dian = dian.DianClient(username,
password) client_dian = dian.DianClient(username, password)
resp = client_dian.request(dian.ConsultaResolucionesFacturacionPeticion( resp = client_dian.request(
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(private_key, public_key, habilitacion, password, test_setid, filename, zipfile): def soap_send_test_set_async(
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(private_key, public_key, password=password) client = dian.DianSignatureClient(
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(req( resp = client.request(
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(private_key, public_key, habilitacion, password, filename, zipfile): def soap_send_bill_async(
private_key, public_key, habilitacion, password, filename, zipfile
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient(private_key, public_key, password=password) client = dian.DianSignatureClient(
private_key, public_key, password=password
)
req = dian.SendBillAsync req = dian.SendBillAsync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendBillAsync req = dian.Habilitacion.SendBillAsync
resp = client.request(req( resp = client.request(req(filename, open(zipfile, "rb").read()))
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(private_key, public_key, habilitacion, password, filename, zipfile): def soap_send_bill_sync(
private_key, public_key, habilitacion, password, filename, zipfile
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient(private_key, public_key, password=password) client = dian.DianSignatureClient(
private_key, public_key, password=password
)
req = dian.SendBillSync req = dian.SendBillSync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendBillSync req = dian.Habilitacion.SendBillSync
resp = client.request(req( resp = client.request(req(filename, open(zipfile, "rb").read()))
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(private_key, public_key, habilitacion, password, track_id): def soap_get_status_zip(
private_key, public_key, habilitacion, password, track_id
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient(private_key, public_key, password=password) client = dian.DianSignatureClient(
private_key, public_key, password=password
)
req = dian.GetStatusZip req = dian.GetStatusZip
if habilitacion: if habilitacion:
req = dian.Habilitacion.GetStatusZip req = dian.Habilitacion.GetStatusZip
resp = client.request(req( resp = client.request(req(trackId=track_id))
trackId = track_id
))
print("StatusCode:", resp.StatusCode) print("StatusCode:", resp.StatusCode)
print("StatusDescription:", resp.StatusDescription) print("StatusDescription:", resp.StatusDescription)
@@ -143,68 +164,78 @@ def soap_get_status_zip(private_key, public_key, habilitacion, password, track_i
@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(private_key, public_key, habilitacion, password, track_id): def soap_get_status(
private_key, public_key, habilitacion, password, track_id
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient(private_key, public_key, password=password) client = dian.DianSignatureClient(
private_key, public_key, password=password
)
req = dian.GetStatus req = dian.GetStatus
if habilitacion: if habilitacion:
req = dian.Habilitacion.GetStatus req = dian.Habilitacion.GetStatus
resp = client.request(req( resp = client.request(req(trackId=track_id))
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(private_key, def soap_get_numbering_range(
private_key,
public_key, public_key,
habilitacion, habilitacion,
password, password,
nit, nit_proveedor, id_software): nit,
nit_proveedor,
id_software,
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient(private_key, public_key, password=password) client = dian.DianSignatureClient(
private_key, public_key, password=password
)
req = dian.GetNumberingRange req = dian.GetNumberingRange
if habilitacion: if habilitacion:
req = dian.Habilitacion.GetNumberingRange req = dian.Habilitacion.GetNumberingRange
resp = client.request(req( resp = client.request(req(nit, nit_proveedor, id_software))
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"',
@@ -213,35 +244,55 @@ 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(private_key, passphrase, xmlfile, ssl=True, use_cache_policy=False, output=None): def sign_xml(
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(private_key, passphrase=passphrase, localpolicy=use_cache_policy) signer = fe.DianXMLExtensionSigner(
document = open(xmlfile, 'r').read().encode('utf-8') private_key, passphrase=passphrase, localpolicy=use_cache_policy
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(private_key, passphrase, scriptname, generate=False, ssl=True, sign=False, use_cache_policy=False, output=None): def generate_invoice(
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
@@ -254,13 +305,15 @@ def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=Tr
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)
import facho.fe.form as form from facho.fe.form_xml import (
from facho.fe.form_xml import DIANInvoiceXML, DIANWriteSigned, DIANWrite, DIANSupportDocumentXML DIANWriteSigned,
from facho import fe DIANWrite,
DIANSupportDocumentXML,
)
try: try:
invoice_xml = module.document_xml() invoice_xml = module.document_xml()
@@ -279,24 +332,39 @@ def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=Tr
xml.add_extension(extension) xml.add_extension(extension)
if sign: if sign:
DIANWriteSigned(xml, output, private_key, passphrase, use_cache_policy) DIANWriteSigned(
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(private_key, passphrase, scriptname, ssl=True, sign=False, use_cache_policy=False, output=None): def generate_nomina(
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() -> fe.nomina.NominaIndividual def nomina() -> (
def extensions(fe.nomina.NominaIndividual): -> List[facho.FachoXMLExtension] fe.nomina.NominaIndividual
)
def extensions(
fe.nomina.NominaIndividual
): -> List[facho.FachoXMLExtension]
""" """
if not ssl: if not ssl:
@@ -304,7 +372,7 @@ def generate_nomina(private_key, passphrase, scriptname, ssl=True, sign=False, u
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)
@@ -320,59 +388,83 @@ def generate_nomina(private_key, passphrase, scriptname, ssl=True, sign=False, u
xml.add_extension(extension) xml.add_extension(extension)
if sign: if sign:
DIANWriteSigned(xml, output, private_key, passphrase, use_cache_policy, dian_signer=facho.fe.nomina.DianXMLExtensionSigner) DIANWriteSigned(
xml,
output,
private_key,
passphrase,
use_cache_policy,
dian_signer=facho.fe.nomina.DianXMLExtensionSigner,
)
else: 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(private_key, public_key, habilitacion, password, filename, zipfile): def soap_send_nomina_sync(
private_key, public_key, habilitacion, password, filename, zipfile
):
from facho.fe.client import dian from facho.fe.client import dian
client = dian.DianSignatureClient(private_key, public_key, password=password) client = dian.DianSignatureClient(
private_key, public_key, password=password
)
req = dian.SendNominaSync req = dian.SendNominaSync
if habilitacion: if habilitacion:
req = dian.Habilitacion.SendNominaSync req = dian.Habilitacion.SendNominaSync
resp = client.request(req( resp = client.request(req(open(zipfile, "rb").read()))
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(private_key, passphrase, xmlfile, ssl=True, use_cache_policy=False, output=None): def sign_verify_xml(
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(private_key, passphrase=passphrase, localpolicy=use_cache_policy) signer = fe.DianXMLExtensionSignerVerifier(
document = open(xmlfile, 'r').read().encode('utf-8') private_key, passphrase=passphrase, localpolicy=use_cache_policy
)
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(software_id, software_pin, nit, dv, output_zippath): def generate_nomina_habilitacion(
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(
@@ -380,15 +472,17 @@ def generate_nomina_habilitacion(software_id, software_pin, nit, dv, output_zipp
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.getiterator(): for el in elem.iter():
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 el.getchildren() == [] and el.keys() == [ if is_optional and list(el) == [] and el.keys() == [
'facho_optional']: 'facho_optional']:
el.getparent().remove(el) el.getparent().remove(el)
@@ -156,6 +156,7 @@ 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:
@@ -291,7 +292,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 parent.getchildren(): for child in list(parent):
if child.tag == node_tag: if child.tag == node_tag:
last_slibing = child last_slibing = child
@@ -389,12 +390,94 @@ 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)
elem = self.builder.xpath(self.root, xpath) return self.builder.xpath(self.root, xpath, multiple=multiple)
if elem is None:
raise AttributeError('xpath %s invalid' % (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) text = self.builder.get_text(elem)
return str(text) if text is None:
return None
return format_(text)
def get_element_text_or_attribute(
self, xpath, default=None, multiple=False, raise_on_fail=False):
parts = xpath.split('/')
is_attribute = parts[-1].startswith('@')
if is_attribute:
attribute_name = parts.pop(-1).lstrip('@')
element_path = "/".join(parts)
try:
val = self.get_element_attribute(
element_path, attribute_name, multiple=multiple)
if val is None:
return default
return val
except KeyError as e:
if raise_on_fail:
raise e
return default
except ValueError as e:
if raise_on_fail:
raise e
return default
else:
try:
val = self.get_element_text(xpath, multiple=multiple)
if val is None:
return default
return val
except ValueError as e:
if raise_on_fail:
raise e
return default
def get_elements_text_or_attributes(self, xpaths, raise_on_fail=True):
"""
returna el contenido o attributos de un conjunto de XPATHS
si algun XPATH es una tupla se retorna el primer elemento del mismo.
"""
vals = []
for xpath in xpaths:
if isinstance(xpath, tuple):
val = xpath[0]
else:
val = self.get_element_text_or_attribute(
xpath, raise_on_fail=raise_on_fail)
vals.append(val)
return vals
def exist_element(self, xpath):
elem = self.get_element(xpath)
# no se encontro elemento
if elem is None:
return False
# el placeholder no ha sido populado
if elem.get('facho_placeholder') == 'True':
return False
# el valor opcional no ha sido populado
if elem.get('facho_optional') == 'True':
return False
return True
def _remove_facho_attributes(self, elem):
self.builder.remove_attributes(
elem, ['facho_optional', 'facho_placeholder'])
def tostring(self, **kw): def tostring(self, **kw):
return self.builder.tostring(self.root, **kw) return self.builder.tostring(self.root, **kw)

View File

@@ -14,3 +14,22 @@ 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,23 +1,13 @@
from facho import facho
import zeep import zeep
from zeep.wsse.username import UsernameToken from zeep.wsse.username import UsernameToken
from .wsse.signature import Signature, BinarySignature from .wsse.signature import BinarySignature
from zeep.wsa import WsAddressingPlugin
import xmlsec import xmlsec
import urllib.request from dataclasses import dataclass, asdict
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:
@@ -33,6 +23,7 @@ class SOAPService:
def todict(self): def todict(self):
return asdict(self) return asdict(self)
@dataclass @dataclass
class GetNumberingRangeResponse: class GetNumberingRangeResponse:
@@ -47,8 +38,7 @@ 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):
@@ -91,7 +81,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):
@@ -100,6 +90,7 @@ class SendTestSetAsyncResponse:
data['ErrorMessageList'] or [] data['ErrorMessageList'] or []
) )
@dataclass @dataclass
class SendTestSetAsync(SOAPService): class SendTestSetAsync(SOAPService):
fileName: str fileName: str
@@ -115,6 +106,7 @@ 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
@@ -129,12 +121,13 @@ 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):
@@ -162,6 +155,7 @@ 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
@@ -175,6 +169,7 @@ 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
@@ -220,6 +215,7 @@ 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):
@@ -250,7 +246,11 @@ class DianClient(DianGateway):
self._password = password self._password = password
def _open(self, service): def _open(self, service):
return zeep.Client(service.get_wsdl(), wsse=UsernameToken(self._username, self._password)) return zeep.Client(
service.get_wsdl(),
wsse=UsernameToken(
self._username,
self._password))
class DianSignatureClient(DianGateway): class DianSignatureClient(DianGateway):
@@ -262,13 +262,10 @@ 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= client = zeep.Client(service.get_wsdl(), wsse=BinarySignature(
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 from datetime import datetime, timedelta, timezone
from lxml import etree from lxml import etree
from lxml.etree import QName from lxml.etree import QName
@@ -70,8 +70,11 @@ 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, key, self.signature_method, self.digest_method, expires_dt=self.expires_dt envelope,
) key,
self.signature_method,
self.digest_method,
expires_dt=self.expires_dt)
return envelope, headers return envelope, headers
def verify(self, envelope): def verify(self, envelope):
@@ -81,7 +84,7 @@ class MemorySignature(object):
class Signature(MemorySignature): class Signature(MemorySignature):
"""Sign given SOAP envelope with WSSE sig using given key file and cert file.""" """Sign given SOAP envelope with WSSE sig using given key and cert file."""
def __init__( def __init__(
self, self,
@@ -101,15 +104,18 @@ class Signature(MemorySignature):
class BinarySignature(Signature): class BinarySignature(Signature):
"""Sign given SOAP envelope with WSSE sig using given key file and cert file. """Sign given SOAP envelope with WSSE sig using given key and cert file.
Place the key information into BinarySecurityElement.""" 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, key, self.signature_method, self.digest_method, expires_dt = self.expires_dt envelope,
) key,
self.signature_method,
self.digest_method,
expires_dt=self.expires_dt)
return envelope, headers return envelope, headers
@@ -219,10 +225,12 @@ 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(envelope, key, signature_method, digest_method) return _sign_envelope_with_key(
envelope, key, signature_method, digest_method)
def get_timestamp(timestamp=None, delta=None): def get_timestamp(timestamp=None, delta=None):
timestamp = timestamp or datetime.utcnow() timestamp = timestamp or datetime.now(timezone.utc)
if delta: if delta:
timestamp += delta timestamp += delta
@@ -230,25 +238,33 @@ 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)
timestamp = datetime.now() etimestamp = utils.WSU.Timestamp({
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()}) '{http://docs.oasis-open.org/wss/2004/01/'
etimestamp.append(utils.WSU.Created(get_timestamp(timestamp=timestamp))) 'oasis-200401-wss-wssecurity-utility-1.0.xsd}Id':
etimestamp.append(utils.WSU.Expires(get_timestamp(timestamp=timestamp, delta=expires_dt))) utils.get_unique_id()})
etimestamp.append(utils.WSU.Created(get_timestamp()))
etimestamp.append(utils.WSU.Expires(get_timestamp(delta=expires_dt)))
security.insert(0, etimestamp) 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(header) etree.cleanup_namespaces(security)
def _signature_prepare(envelope, key, signature_method, digest_method, expires_dt=None):
def _signature_prepare(
envelope,
key,
signature_method,
digest_method,
expires_dt=None):
"""Prepare envelope and sign.""" """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(
@@ -279,7 +295,7 @@ def _signature_prepare(envelope, key, signature_method, digest_method, expires_d
_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 != None: if timestamp is not None:
_sign_node(ctx, signature, timestamp, digest_method) _sign_node(ctx, signature, timestamp, digest_method)
ctx.sign(signature) ctx.sign(signature)
@@ -287,18 +303,30 @@ def _signature_prepare(envelope, key, signature_method, digest_method, expires_d
# KeyInfo. The recipient expects this structure, but we can't rearrange # 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(key_info, QName(ns.WSSE, "SecurityTokenReference")) sec_token_ref = etree.SubElement(
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(envelope, key, signature_method, digest_method, expires_dt=None): def _sign_envelope_with_key(
envelope,
key,
signature_method,
digest_method,
expires_dt=None):
_, sec_token_ref, x509_data = _signature_prepare( _, 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(envelope, key, signature_method, digest_method, expires_dt=None): def _sign_envelope_with_key_binary(
envelope,
key,
signature_method,
digest_method,
expires_dt=None):
security, sec_token_ref, x509_data = _signature_prepare( 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
) )
@@ -354,7 +382,10 @@ 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("ds:SignedInfo/ds:Reference", namespaces={"ds": ns.DS}) refs = signature.xpath(
"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,10 +7,17 @@ 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'))
NominaIndividual = xmlschema.XMLSchema(path_for_xsd('nomina', 'NominaIndividualElectronicaXSDV1.0.6.xsd')) UBLInvoice = xmlschema.XMLSchema(
NominaIndividualDeAjuste = xmlschema.XMLSchema(path_for_xsd('nomina', 'NominaIndividualDeAjusteElectronicaXSDV1.0.6.xsd')) path_for_xsd(
'maindoc',
'UBL-Invoice-2.1.xsd'))
NominaIndividual = xmlschema.XMLSchema(path_for_xsd(
'nomina', 'NominaIndividualElectronicaXSDV1.0.6.xsd'))
NominaIndividualDeAjuste = xmlschema.XMLSchema(path_for_xsd(
'nomina', 'NominaIndividualDeAjusteElectronicaXSDV1.0.6.xsd'))
def validate(xml, schema): 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 xmlrow.getchildren(): for value in list(xmlrow):
row[value.attrib['ColumnRef']] = value.getchildren()[0].text row[value.attrib['ColumnRef']] = list(value)[0].text
return row return row
@@ -60,6 +60,7 @@ 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',
@@ -72,38 +73,88 @@ __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')
TipoResponsabilidad = CodeList(path_for_codelist('TipoResponsabilidad-2.1.gc'), 'code', 'name')\ TipoOrganizacion = CodeList(path_for_codelist(
.update(CodeList(path_for_codelist('TipoResponsabilidad-2.1.custom.gc'), 'code', 'name')) 'TipoOrganizacion-2.1.gc'), 'code', 'name')
TipoAmbiente = CodeList(path_for_codelist('TipoAmbiente-2.1.gc'), 'code', 'name') TipoResponsabilidad = CodeList(
TipoDocumento = CodeList(path_for_codelist('TipoDocumento-2.1.gc'), 'code', 'name') path_for_codelist('TipoResponsabilidad-2.1.gc'),
TipoImpuesto = CodeList(path_for_codelist('TipoImpuesto-2.1.gc'), 'code', 'name')\ 'code',
.update(CodeList(path_for_codelist('TipoImpuesto-2.1.custom.gc'), 'code', 'name')) 'name') .update(
TarifaImpuesto = CodeList(path_for_codelist('TarifaImpuestoINC-2.1.gc'), 'code', 'name')\ CodeList(
.update(CodeList(path_for_codelist('TarifaImpuestoIVA-2.1.gc'), 'code', 'name'))\ path_for_codelist('TipoResponsabilidad-2.1.custom.gc'),
.update(CodeList(path_for_codelist('TarifaImpuestoReteIVA-2.1.gc'), 'code', 'name'))\ 'code',
.update(CodeList(path_for_codelist('TarifaImpuestoReteRenta-2.1.gc'), 'code', 'name')) 'name'))
CodigoPrecioReferencia = CodeList(path_for_codelist('CodigoPrecioReferencia-2.1.gc'), 'code', 'name') TipoAmbiente = CodeList(path_for_codelist(
'TipoAmbiente-2.1.gc'), 'code', 'name')
TipoDocumento = CodeList(path_for_codelist(
'TipoDocumento-2.1.gc'), 'code', 'name')
TipoImpuesto = CodeList(
path_for_codelist('TipoImpuesto-2.1.gc'),
'code',
'name') .update(
CodeList(
path_for_codelist('TipoImpuesto-2.1.custom.gc'),
'code',
'name'))
TarifaImpuesto = CodeList(
path_for_codelist('TarifaImpuestoINC-2.1.gc'),
'code',
'name') .update(
CodeList(
path_for_codelist('TarifaImpuestoIVA-2.1.gc'),
'code',
'name')) .update(
CodeList(
path_for_codelist('TarifaImpuestoReteIVA-2.1.gc'),
'code',
'name')) .update(
CodeList(
path_for_codelist(
'TarifaImpuestoReteRenta-2.1.gc'),
'code',
'name'))
CodigoPrecioReferencia = CodeList(path_for_codelist(
'CodigoPrecioReferencia-2.1.gc'), 'code', 'name')
MediosPago = CodeList(path_for_codelist('MediosPago-2.1.gc'), 'code', 'name') 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-2.1.custom.gc'), 'code', 'name') RegimenFiscal = CodeList(path_for_codelist(
TipoOperacionNC = CodeList(path_for_codelist('TipoOperacionNC-2.1.gc'), 'code', 'name') 'RegimenFiscal-2.1.custom.gc'), 'code', 'name')
TipoOperacionNCDS = CodeList(path_for_codelist('TipoOperacionNCDS-2.1.gc'), 'code', 'name') TipoOperacionNC = CodeList(path_for_codelist(
TipoOperacionND = CodeList(path_for_codelist('TipoOperacionND-2.1 - copia.gc'), 'code', 'name') 'TipoOperacionNC-2.1.gc'), 'code', 'name')
TipoOperacionF = CodeList(path_for_codelist('TipoOperacionF-2.1.gc'), 'code', 'name')\ TipoOperacionNCDS = CodeList(path_for_codelist(
.update(CodeList(path_for_codelist('TipoOperacionF-2.1.custom.gc'), 'code', 'name')) 'TipoOperacionNCDS-2.1.gc'), 'code', 'name')
TipoOperacionND = CodeList(path_for_codelist(
'TipoOperacionND-2.1 - copia.gc'), 'code', 'name')
TipoOperacionF = CodeList(
path_for_codelist('TipoOperacionF-2.1.gc'),
'code',
'name') .update(
CodeList(
path_for_codelist('TipoOperacionF-2.1.custom.gc'),
'code',
'name'))
Municipio = CodeList(path_for_codelist('Municipio-2.1.gc'), 'code', 'name') Municipio = CodeList(path_for_codelist('Municipio-2.1.gc'), 'code', 'name')
Departamento = CodeList(path_for_codelist('Departamentos-2.1.gc'), 'code', 'name') Departamento = CodeList(path_for_codelist(
'Departamentos-2.1.gc'), 'code', 'name')
Paises = CodeList(path_for_codelist('Paises-2.1.gc'), 'code', 'name') Paises = CodeList(path_for_codelist('Paises-2.1.gc'), 'code', 'name')
TipoIdFiscal = CodeList(path_for_codelist('TipoIdFiscal-2.1.gc'), 'code', 'name') TipoIdFiscal = CodeList(path_for_codelist(
CodigoDescuento = CodeList(path_for_codelist('CodigoDescuento-2.1.gc'), 'code', 'name') 'TipoIdFiscal-2.1.gc'), 'code', 'name')
UnidadesMedida = CodeList(path_for_codelist('UnidadesMedida-2.1.gc'), 'code', 'name') CodigoDescuento = CodeList(path_for_codelist(
TipoTrabajador = CodeList(path_for_codelist('TipoTrabajador-2.1.gc'), 'code', 'name') 'CodigoDescuento-2.1.gc'), 'code', 'name')
SubTipoTrabajador = CodeList(path_for_codelist('SubTipoTrabajador-2.1.gc'), 'code', 'name') UnidadesMedida = CodeList(path_for_codelist(
TipoContrato = CodeList(path_for_codelist('TipoContrato-2.1.gc'), 'code', 'name') 'UnidadesMedida-2.1.gc'), 'code', 'name')
PeriodoNomina = CodeList(path_for_codelist('PeriodoNomina-2.1.gc'), 'code', 'name') TipoTrabajador = CodeList(path_for_codelist(
'TipoTrabajador-2.1.gc'), 'code', 'name')
SubTipoTrabajador = CodeList(path_for_codelist(
'SubTipoTrabajador-2.1.gc'), 'code', 'name')
TipoContrato = CodeList(path_for_codelist(
'TipoContrato-2.1.gc'), 'code', 'name')
PeriodoNomina = CodeList(path_for_codelist(
'PeriodoNomina-2.1.gc'), 'code', 'name')
TipoMoneda = CodeList(path_for_codelist('TipoMoneda-2.1.gc'), 'code', 'name') TipoMoneda = CodeList(path_for_codelist('TipoMoneda-2.1.gc'), 'code', 'name')
IdiomaISO6391 = CodeList(path_for_codelist('Idioma-2.1.gc'), 'iso-639-1', 'name') IdiomaISO6391 = CodeList(path_for_codelist(
'Idioma-2.1.gc'), 'iso-639-1', 'name')

View File

@@ -5,7 +5,6 @@ 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
@@ -18,36 +17,65 @@ 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 Nacionales)', 'schemeAgencyName': 'CO, DIAN (Dirección de Impuestos y Aduanas'
'schemeAgencyID': '195' ' Nacionales)',
} 'schemeAgencyID': '195'}
# RESOLUCION 0001: pagina 516 # RESOLUCION 0001: pagina 516
POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf' POLICY_ID = (
POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.' 'https://facturaelectronica.dian.gov.co/politicadefirma/v2'
'/politicadefirmav2.pdf'
)
POLICY_NAME = (
u'Política de firma para facturas electrónicas de la República de '
u'Colombia.'
)
Bogota = tz.gettz('America/Bogota') 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:NominaIndividualDeAjuste', # 'nominaajuste': 'dian:gov:co:facturaelectronica:'
# '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:CommonAggregateComponents-2', # 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:'
# 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', # 'CommonAggregateComponents-2',
# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', # 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:'
# 'CommonBasicComponents-2',
# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:'
# 'schema:xsd:DocumentInformationAggregateComponents-1',
# 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001', # 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001',
# 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003', # 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:'
# 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', # 'IANAMIMEMediaType:2003',
# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', # 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:'
# 'CommonExtensionComponents-2',
# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:'
# 'QualifiedDatatypes-2',
# 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', # 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1',
# 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', # 'udt': 'urn:un:unece:uncefact:data:specification:'
# 'UnqualifiedDataTypesSchemaModule:2',
# 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', # '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#',
@@ -57,15 +85,20 @@ 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': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', 'cac': (
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', 'urn:oasis:names:specification:ubl:schema:xsd'
'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', ':CommonAggregateComponents-2'),
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd'
':CommonBasicComponents-2',
'ext': (
'urn:oasis:names:specification:ubl:schema:xsd'
':CommonExtensionComponents-2'),
'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', '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:UnqualifiedDataTypesSchemaModule:2', 'udt': (
'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#',
@@ -107,22 +140,21 @@ 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 namespace # MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un
urn_oasis = { # namespace
'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[root_localname]) .replace(root_namespace, urn_oasis)
class DianXMLExtensionCUDFE(FachoXMLExtension): class DianXMLExtensionCUDFE(FachoXMLExtension):
@@ -141,9 +173,12 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
def _get_qrcode(self, cufe): def _get_qrcode(self, cufe):
url_for = { url_for = {
AMBIENTE_PRUEBAS: 'https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey=', AMBIENTE_PRUEBAS: (
AMBIENTE_PRODUCCION: 'https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=' 'https://catalogo-vpfe-hab.dian.gov.co/document/'
} 'searchqr?documentkey='),
AMBIENTE_PRODUCCION: (
'https://catalogo-vpfe.dian.gov.co/document/'
'searchqr?documentkey=')}
return url_for[self.tipo_ambiente] + cufe return url_for[self.tipo_ambiente] + cufe
def build(self, fachoxml): def build(self, fachoxml):
@@ -154,21 +189,29 @@ 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('./cbc:ProfileID', fachoxml.set_element(
'DIAN 2.1: documento soporte en adquisiciones efectuadas a no obligados a facturar.') './cbc:ProfileID',
'DIAN 2.1: documento soporte en adquisiciones '
'efectuadas a no obligados a facturar.')
else: else:
fachoxml.set_element('./cbc:ProfileID', fachoxml.set_element(
'DIAN 2.1: Nota de ajuste al documento soporte en adquisiciones efectuadas a sujetos no obligados a expedir factura o documento equivalente') './cbc:ProfileID',
'DIAN 2.1: Nota de ajuste al documento soporte en '
'adquisiciones efectuadas a sujetos no obligados a '
'expedir factura o documento equivalente')
else: else:
fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') fachoxml.set_element(
'./cbc:ProfileID',
'DIAN 2.1: Factura Electrónica de Venta')
# #DIAN 1.8.-2021: FAD03 # fachoxml.set_element('./cbc:ProfileID',
# fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') # '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:DianExtensions/sts:QRCode', './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:QRCode',
self._get_qrcode(cufe)) self._get_qrcode(cufe))
def issue_time(self, datetime_): def issue_time(self, datetime_):
@@ -184,7 +227,8 @@ 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'] = invoice.invoice_legal_monetary_total.line_extension_amount build_vars['ValorBruto'] = (
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))
@@ -196,7 +240,8 @@ 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(codigo_impuesto, form.Amount(0.0)) ValorImpuestoPara.setdefault(
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
@@ -236,21 +281,35 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
CodImpuesto2 = build_vars['CodImpuesto2'] CodImpuesto2 = build_vars['CodImpuesto2']
CodImpuesto3 = build_vars['CodImpuesto3'] CodImpuesto3 = build_vars['CodImpuesto3']
return [ return [
'%s' % build_vars['NumFac'], '%s' %
'%s' % build_vars['FecFac'], build_vars['NumFac'],
'%s' % build_vars['HoraFac'], '%s' %
form.Amount(build_vars['ValorBruto']).truncate_as_string(2), build_vars['FecFac'],
'%s' %
build_vars['HoraFac'],
form.Amount(
build_vars['ValorBruto']).truncate_as_string(2),
CodImpuesto1, CodImpuesto1,
build_vars['ValorImpuestoPara'].get(CodImpuesto1, form.Amount(0.0)).truncate_as_string(2), build_vars['ValorImpuestoPara'].get(
CodImpuesto1,
form.Amount(0.0)).truncate_as_string(2),
CodImpuesto2, CodImpuesto2,
build_vars['ValorImpuestoPara'].get(CodImpuesto2, form.Amount(0.0)).truncate_as_string(2), build_vars['ValorImpuestoPara'].get(
CodImpuesto2,
form.Amount(0.0)).truncate_as_string(2),
CodImpuesto3, CodImpuesto3,
build_vars['ValorImpuestoPara'].get(CodImpuesto3, form.Amount(0.0)).truncate_as_string(2), build_vars['ValorImpuestoPara'].get(
CodImpuesto3,
form.Amount(0.0)).truncate_as_string(2),
build_vars['ValorTotalPagar'].truncate_as_string(2), build_vars['ValorTotalPagar'].truncate_as_string(2),
'%s' % build_vars['NitOFE'], '%s' %
'%s' % build_vars['NumAdq'], build_vars['NitOFE'],
'%s' % build_vars['ClTec'], '%s' %
'%d' % build_vars['TipoAmb'], build_vars['NumAdq'],
'%s' %
build_vars['ClTec'],
'%d' %
build_vars['TipoAmb'],
] ]
@@ -274,21 +333,39 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
CodImpuesto2 = build_vars['CodImpuesto2'] CodImpuesto2 = build_vars['CodImpuesto2']
CodImpuesto3 = build_vars['CodImpuesto3'] CodImpuesto3 = build_vars['CodImpuesto3']
return [ return [
'%s' % build_vars['NumFac'], '%s' %
'%s' % build_vars['FecFac'], build_vars['NumFac'],
'%s' % build_vars['HoraFac'], '%s' %
form.Amount(build_vars['ValorBruto']).truncate_as_string(2), build_vars['FecFac'],
'%s' %
build_vars['HoraFac'],
form.Amount(
build_vars['ValorBruto']).truncate_as_string(2),
CodImpuesto1, CodImpuesto1,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2), form.Amount(
build_vars['ValorImpuestoPara'].get(
CodImpuesto1,
0.0)).truncate_as_string(2),
CodImpuesto2, CodImpuesto2,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).truncate_as_string(2), form.Amount(
build_vars['ValorImpuestoPara'].get(
CodImpuesto2,
0.0)).truncate_as_string(2),
CodImpuesto3, CodImpuesto3,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).truncate_as_string(2), form.Amount(
form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2), build_vars['ValorImpuestoPara'].get(
'%s' % build_vars['NitOFE'], CodImpuesto3,
'%s' % build_vars['NumAdq'], 0.0)).truncate_as_string(2),
'%s' % build_vars['Software-PIN'], form.Amount(
'%d' % build_vars['TipoAmb'], build_vars['ValorTotalPagar']).truncate_as_string(2),
'%s' %
build_vars['NitOFE'],
'%s' %
build_vars['NumAdq'],
'%s' %
build_vars['Software-PIN'],
'%d' %
build_vars['TipoAmb'],
] ]
@@ -309,20 +386,30 @@ 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' % build_vars['NumFac'], '%s' %
'%s' % build_vars['FecFac'], build_vars['NumFac'],
'%s' % build_vars['HoraFac'], '%s' %
form.Amount(build_vars['ValorBruto']).truncate_as_string(2), build_vars['FecFac'],
'%s' %
build_vars['HoraFac'],
form.Amount(
build_vars['ValorBruto']).truncate_as_string(2),
CodImpuesto1, CodImpuesto1,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2), form.Amount(
form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2), build_vars['ValorImpuestoPara'].get(
'%s' % build_vars['NitOFE'], CodImpuesto1,
'%s' % build_vars['NumAdq'], 0.0)).truncate_as_string(2),
'%s' % build_vars['Software-PIN'], form.Amount(
'%d' % build_vars['TipoAmb'], build_vars['ValorTotalPagar']).truncate_as_string(2),
'%s' %
build_vars['NitOFE'],
'%s' %
build_vars['NumAdq'],
'%s' %
build_vars['Software-PIN'],
'%d' %
build_vars['TipoAmb'],
] ]
@@ -336,14 +423,19 @@ 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:DianExtensions/sts:SoftwareProvider') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':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('/sts:SoftwareProvider/sts:ProviderID', self.nit, software_provider.set_element(
'/sts:SoftwareProvider/sts:ProviderID',
self.nit,
**provider_id_attrs) **provider_id_attrs)
software_provider.set_element('/sts:SoftwareProvider/sts:SoftwareID', self.id_software, software_provider.set_element(
'/sts:SoftwareProvider/sts:SoftwareID',
self.id_software,
**SCHEME_AGENCY_ATTRS) **SCHEME_AGENCY_ATTRS)
@@ -356,7 +448,10 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
self.invoice_ident = invoice_ident self.invoice_ident = invoice_ident
def build(self, fexml): def build(self, fexml):
dian_path = './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode' dian_path = (
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:SoftwareSecurityCode'
)
code = str(self.id_software) + str(self.pin) + str(self.invoice_ident) 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'))
@@ -410,8 +505,11 @@ class DianXMLExtensionSigner:
xml.append(signature) xml.append(signature)
ref = xmlsig.template.add_reference( ref = xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="", name="xmldsig-%s-ref0" % (id_uuid) signature,
) xmlsig.constants.TransformSha256,
uri="",
name="xmldsig-%s-ref0" %
(id_uuid))
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped) xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid) id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid)
@@ -430,9 +528,8 @@ 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" % (id_props), signature, xmlsig.constants.TransformSha256, uri="#%s" %
uri_type="http://uri.etsi.org/01903#SignedProperties" (id_props), 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)
@@ -450,9 +547,6 @@ 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)
@@ -476,7 +570,9 @@ 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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider') authorization_provider = fexml.fragment(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:AuthorizationProvider')
authorization_provider.set_element('./sts:AuthorizationProviderID', authorization_provider.set_element('./sts:AuthorizationProviderID',
'800197268', '800197268',
**attrs) **attrs)
@@ -485,12 +581,17 @@ class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
class DianXMLExtensionInvoiceSource(FachoXMLExtension): class DianXMLExtensionInvoiceSource(FachoXMLExtension):
# CAB13 # CAB13
def build(self, fexml): def build(self, fexml):
dian_path = '/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode' dian_path = (
'/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionCo'
'ntent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode'
)
fexml.set_element( 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="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1") listSchemeURI=(
"urn:oasis:names:specification:ubl:codelist:gc:"
"CountryIdentificationCode-2.1"))
class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension): class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
@@ -507,28 +608,38 @@ class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
self.to = to self.to = to
def build(self, fexml): def build(self, fexml):
invoice_control = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl') invoice_control = fexml.fragment(
invoice_control.set_element('/sts:InvoiceControl/sts:InvoiceAuthorization', self.authorization) './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:StartDate', ':DianExtensions/sts:InvoiceControl')
invoice_control.set_element(
'/sts:InvoiceControl/sts:InvoiceAuthorization',
self.authorization)
invoice_control.set_element(
'/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:StartDate',
self.period_startdate.strftime('%Y-%m-%d')) self.period_startdate.strftime('%Y-%m-%d'))
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:EndDate', invoice_control.set_element(
'/sts:InvoiceControl/sts:AuthorizationPeriod/cbc:EndDate',
self.period_enddate.strftime('%Y-%m-%d')) self.period_enddate.strftime('%Y-%m-%d'))
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:Prefix', invoice_control.set_element(
'/sts:InvoiceControl/sts:AuthorizedInvoices/sts:Prefix',
self.prefix) self.prefix)
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:From', invoice_control.set_element(
self.from_) '/sts:InvoiceControl/sts:AuthorizedInvoices/sts:From', self.from_)
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To', invoice_control.set_element(
self.to) '/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To', self.to)
fexml.set_element( fexml.set_element(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode', './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
'CO', '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="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1") listSchemeURI=(
"urn:oasis:names:specification:ubl:codelist:gc:"
"CountryIdentificationCode-2.1"))
class DianZIP: class DianZIP:
@@ -571,7 +682,11 @@ class DianZIP:
class DianXMLExtensionSignerVerifier: class DianXMLExtensionSignerVerifier:
def __init__(self, pkcs12_path_or_bytes, passphrase=None, localpolicy=True): def __init__(
self,
pkcs12_path_or_bytes,
passphrase=None,
localpolicy=True):
self._pkcs12_path_or_bytes = pkcs12_path_or_bytes self._pkcs12_path_or_bytes = pkcs12_path_or_bytes
self._passphrase = None self._passphrase = None
self._localpolicy = localpolicy self._localpolicy = localpolicy
@@ -596,8 +711,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(OpenSSL.crypto.load_pkcs12(pkcs12_data, ctx.load_pkcs12(pkcs12.load_key_and_certificates(
self._passphrase)) pkcs12_data, self._passphrase))
try: try:
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():
@@ -605,5 +720,5 @@ class DianXMLExtensionSignerVerifier:
else: else:
ctx.verify(signature) ctx.verify(signature)
return True return True
except: except BaseException:
return False return False

View File

@@ -5,13 +5,14 @@
# 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, date from datetime import datetime
# 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
@@ -60,10 +61,9 @@ class AmountCollection(Collection):
class Amount: class Amount:
def __init__( def __init__(
self, amount: int or float or str, self, amount: int | float | str | "Amount",
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,10 +77,9 @@ class Amount:
self.amount = Decimal( self.amount = Decimal(
amount, decimal.Context( amount, decimal.Context(
prec=self.precision, prec=DECIMAL_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):
@@ -98,24 +97,20 @@ 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, self.precision) < round(other, 2) return round(self.amount, DECIMAL_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, self.precision) == round( return round(self.amount, DECIMAL_PRECISION) == round(
other.amount, self.precision) other.amount, DECIMAL_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)
@@ -285,22 +280,11 @@ 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: typing.List[Responsability] responsability_code: 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'))
@@ -333,7 +317,7 @@ class TaxScheme:
@dataclass @dataclass
class TaxSubTotal: class TaxSubTotal:
percent: float percent: float
scheme: typing.Optional[TaxScheme] = None scheme: TaxScheme | None = 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):
@@ -365,7 +349,7 @@ class TaxTotalOmit(TaxTotal):
@dataclass @dataclass
class WithholdingTaxSubTotal: class WithholdingTaxSubTotal:
percent: float percent: float
scheme: typing.Optional[TaxScheme] = None scheme: TaxScheme | None = 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):
@@ -497,7 +481,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: typing.Optional[Amount] = field( base_amount: Amount | None = field(
default_factory=lambda: Amount(0.0)) default_factory=lambda: Amount(0.0))
# Porcentaje: Porcentaje que aplicar. # Porcentaje: Porcentaje que aplicar.
@@ -542,9 +526,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: typing.Optional[TaxTotal] tax: TaxTotal | None
withholding: typing.Optional[WithholdingTaxTotal] withholding: WithholdingTaxTotal | None
allowance_charge: typing.List[AllowanceCharge] = dataclasses.field( allowance_charge: 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,7 +6,10 @@ 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,8 +1,18 @@
from .invoice import * from .invoice import DIANInvoiceXML
from .credit_note import * from .credit_note import DIANCreditNoteXML
from .debit_note import * from .debit_note import DIANDebitNoteXML
from .utils import * from .utils import DIANWrite, DIANWriteSigned
from .support_document import * from .attached_document import AttachedDocument
from .support_document_credit_note import * from .support_document import DIANSupportDocumentXML
from .attached_document import * from .support_document_credit_note import DIANSupportDocumentCreditNoteXML
from .application_response import *
__all__ = [
'DIANInvoiceXML',
'DIANCreditNoteXML',
'DIANDebitNoteXML',
'DIANWrite',
'DIANWriteSigned',
'AttachedDocument',
'DIANSupportDocumentXML',
'DIANSupportDocumentCreditNoteXML',
]

View File

@@ -1,177 +0,0 @@
from .. import fe
__all__ = ['ApplicationResponse']
class ApplicationResponse:
def __init__(self, invoice, tag_document='ApplicationResponse'):
self.schema =\
'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2'
self.tag_document = tag_document
self.invoice = invoice
self.fexml = fe.FeXML(
self.tag_document, self.schema)
self.application_response = self.application_response()
def application_response(self):
# DIAN 1.9.-2023: AE02
self.fexml.set_element(
'./cbc:UBLVersionID', 'UBL 2.1')
# DIAN 1.9.-2023: AE03
self.fexml.set_element(
'./cbc:CustomizationID', 'Documentos adjuntos')
# DIAN 1.9.-2023: AE04
self.fexml.set_element(
'./cbc:ProfileID', 'DIAN 2.1')
# DIAN 1.9.-2023: AE04a
self.fexml.set_element(
'./cbc:ProfileExecutionID', '1')
self.fexml.set_element(
'./cbc:ID', '1')
self.fexml.set_element(
'./cbc:UUID', '1', schemeName="CUDE-SHA384")
self.fexml.set_element(
'./cbc:IssueDate',
self.invoice.invoice_issue.strftime('%Y-%m-%d'))
# DIAN 1.9.-2023: AE06
self.fexml.set_element(
'./cbc:IssueTime', self.invoice.invoice_issue.strftime(
'%H:%M:%S-05:00'))
self.set_sender_party()
self.set_receiver_party()
self.set_document_response()
def set_sender_party(self):
# DIAN 1.9.-2023: AE09
self.fexml.placeholder_for(
'./cac:SenderParty')
# DIAN 1.9.-2023: AE10
self.fexml.placeholder_for(
'./cac:SenderParty/cac:PartyTaxScheme')
# DIAN 1.9.-2023: AE11
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cbc:RegistrationName',
self.invoice.invoice_supplier.name)
# DIAN 1.9.-2023: AE12
# DIAN 1.9.-2023: AE13
# DIAN 1.9.-2023: AE14
# DIAN 1.9.-2023: AE15
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cbc:CompanyID',
self.invoice.invoice_supplier.ident,
schemeAgencyID='195',
schemeID=self.invoice.invoice_supplier.ident.dv,
schemeName=self.invoice.invoice_supplier.ident.type_fiscal)
# DIAN 1.9.-2023: AE16
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
self.invoice.invoice_supplier.responsability_code)
# DIAN 1.9.-2023: AE18
self.fexml.placeholder_for(
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme')
# DIAN 1.9.-2023: AE19
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
self.invoice.invoice_supplier.tax_scheme.code)
# DIAN 1.9.-2023: AE20
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
self.invoice.invoice_supplier.tax_scheme.name)
def set_receiver_party(self):
# DIAN 1.9.-2023: AE21
self.fexml.placeholder_for(
'./cac:ReceiverParty')
# DIAN 1.9.-2023: AE22
self.fexml.placeholder_for(
'./cac:ReceiverParty/cac:PartyTaxScheme')
# DIAN 1.9.-2023: AE23
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:RegistrationName',
self.invoice.invoice_customer.name)
# DIAN 1.9.-2023: AE24
# DIAN 1.9.-2023: AE25
# DIAN 1.9.-2023: AE26
# DIAN 1.9.-2023: AE27
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:CompanyID',
self.invoice.invoice_customer.ident,
schemeAgencyID='195',
schemeID=self.invoice.invoice_customer.ident.dv,
schemeName=self.invoice.invoice_customer.ident.type_fiscal)
# DIAN 1.9.-2023: AE28
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
self.invoice.invoice_customer.responsability_code)
# DIAN 1.9.-2023: AE30
self.fexml.placeholder_for(
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme')
# DIAN 1.9.-2023: AE31
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
self.invoice.invoice_customer.tax_scheme.code)
# DIAN 1.9.-2023: AE32
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
self.invoice.invoice_customer.tax_scheme.name)
def set_document_response(self):
self.fexml.placeholder_for(
'./cac:DocumentResponse')
self.fexml.placeholder_for(
'./cac:DocumentResponse/cac:Response')
self.fexml.set_element(
'./cac:DocumentResponse/cac:Response/cbc:ResponseCode',
'02')
self.fexml.set_element(
'./cac:DocumentResponse/cac:Response/cbc:Description',
'Documento validado por la DIAN')
self.set_documnent_reference()
def set_documnent_reference(self):
self.fexml.placeholder_for(
'./cac:DocumentResponse/cac:DocumentReference')
self.fexml.set_element(
'./cac:DocumentResponse/cac:DocumentReference/cbc:ID',
'FESS19566058')
self.fexml.set_element(
'./cac:DocumentResponse/cac:DocumentReference/cbc:UUID',
'f51ee529aabd19d10e39444f2f593b94d56d5885fbf433faf718d53a7e968f64bf54a6ee43c6a2df842771b54a6aae1a',
schemeName="CUFE-SHA384")
self.set_response_lines()
def set_response_lines(self):
lines = [{
'LineID': '1',
'ResponseCode': '0000',
'Description': '0',
}]
for line in lines:
self.fexml.set_element(
'./cac:DocumentResponse/cac:LineResponse/cac:LineReference/cbc:LineID', line[
'LineID'])
self.fexml.set_element(
'./cac:DocumentResponse/cac:LineResponse/cac:Response/cbc:ResponseCode', line[
'ResponseCode'])
self.fexml.set_element(
'./cac:DocumentResponse/cac:LineResponse/cac:Response/cbc:Description', line[
'Description'])
def toFachoXML(self):
return self.fexml

View File

@@ -1,227 +1,15 @@
from .. import fe from .. import fe
from .application_response import ApplicationResponse
__all__ = ['AttachedDocument'] __all__ = ['AttachedDocument']
class AttachedDocument(): class AttachedDocument():
def __init__(self, invoice, DIANInvoiceXML, id): def __init__(self, id):
self.schema =\ schema =\
'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2' 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2'
self.id = id self.fexml = fe.FeXML('AttachedDocument', schema)
self.invoice = invoice self.fexml.set_element('./cbc:ID', id)
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,10 +1,18 @@
from .. import fe
from ..form import *
from collections import defaultdict from collections import defaultdict
from .attached_document import AttachedDocument
from .. import fe
from ..form import (
Amount,
CreditNoteDocumentReference,
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
@@ -12,69 +20,32 @@ class DIANInvoiceXML(fe.FeXML):
""" """
def __init__(self, invoice, tag_document='Invoice'): def __init__(self, invoice, tag_document='Invoice'):
super().__init__(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1') super().__init__(
self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl') tag_document,
self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource') 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') self.placeholder_for(
self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') ':DianExtensions/sts:InvoiceControl')
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('./ext:UBLExtensions/ext:UBLExtension', append=True) ublextension = self.fragment(
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') './ext:UBLExtensions/ext:UBLExtension', append=True)
self.attach_invoice = self.attach_invoice(invoice) ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
# self.attach_document = self.attached_document_invoice(attach_invoice) self.attach_invoice(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')
@@ -94,54 +65,62 @@ 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:Address') './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:ID', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CityName', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CountrySubentity', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CountrySubentityCode', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:AddressLine/cbc:Line', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:Country/cbc:IdentificationCode', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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: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.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
@@ -151,18 +130,21 @@ 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:RegistrationName', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:CompanyID', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:TaxLevelCode', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
':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,
@@ -172,63 +154,76 @@ 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:RegistrationAddress') './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:ID', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:CityName', invoice.invoice_supplier.address.city.name) './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:CountrySubentity', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:CountrySubentityCode', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:AddressLine/cbc:Line', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode', fexml.set_element(
'./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:RegistrationAddress/cac:Country/cbc:Name', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme') './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:ID', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:Name', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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
@@ -239,25 +234,29 @@ 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:RegistrationName', './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc'
':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:CompanyID', './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc'
':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:CorporateRegistrationScheme') './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac'
':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:CorporateRegistrationScheme/cbc:ID', './cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac'
':CorporateRegistrationScheme/cbc:ID',
invoice.invoice_ident_prefix) invoice.invoice_ident_prefix)
# DIAN 1.7.-2020: CAJ67 # DIAN 1.7.-2020: CAJ67
@@ -267,7 +266,8 @@ 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:ElectronicMail', './cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc'
':ElectronicMail',
invoice.invoice_supplier.email) invoice.invoice_supplier.email)
def set_customer(fexml, invoice): def set_customer(fexml, invoice):
@@ -276,7 +276,8 @@ 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/cbc:ID', './cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification'
'/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',
@@ -287,52 +288,62 @@ 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:Address') './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:ID', './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CityName', invoice.invoice_customer.address.city.name) './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CountrySubentity', './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CountrySubentityCode', './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:AddressLine/cbc:Line', './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:Country/cbc:IdentificationCode', './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:Country/cbc:Name', './cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac'
':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
@@ -346,20 +357,23 @@ 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:RegistrationName', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:CompanyID', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:TaxLevelCode', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
':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
@@ -369,83 +383,97 @@ 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:RegistrationAddress') './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:ID', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:CityName', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:CountrySubentity', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cbc:CountrySubentityCode', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:AddressLine') './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:AddressLine/cbc:Line', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:Country') './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:Country/cbc:IdentificationCode', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:Country/cbc:Name', invoice.invoice_customer.address.country.name) './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:RegistrationAddress/cac:Country/cbc:IdentificationCode', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme') './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:ID', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:Name', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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(
@@ -454,13 +482,15 @@ 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:RegistrationName', './cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc'
':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:CompanyID', './cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc'
':CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
**customer_company_id_attrs) **customer_company_id_attrs)
@@ -470,19 +500,22 @@ 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:ElectronicMail', './cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc'
':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:ID', payment_mean.id) './cac:PaymentMeans/cbc:PaymentMeansCode',
payment_mean.code)
fexml.set_element( fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentMeansCode', payment_mean.code) './cac:PaymentMeans/cbc:PaymentDueDate',
payment_mean.due_at.strftime('%Y-%m-%d'))
fexml.set_element( fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d')) './cac:PaymentMeans/cbc:PaymentID',
fexml.set_element( payment_mean.payment_id)
'./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):
@@ -497,38 +530,48 @@ 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('./cac:LegalMonetaryTotal/cbc:LineExtensionAmount', fexml.set_element_amount(
'./cac:LegalMonetaryTotal/cbc:LineExtensionAmount',
invoice.invoice_legal_monetary_total.line_extension_amount) invoice.invoice_legal_monetary_total.line_extension_amount)
fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', fexml.set_element_amount(
'./cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
invoice.invoice_legal_monetary_total.tax_exclusive_amount) invoice.invoice_legal_monetary_total.tax_exclusive_amount)
fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', fexml.set_element_amount(
'./cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
invoice.invoice_legal_monetary_total.tax_inclusive_amount) invoice.invoice_legal_monetary_total.tax_inclusive_amount)
fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:ChargeTotalAmount', fexml.set_element_amount(
'./cac:LegalMonetaryTotal/cbc:ChargeTotalAmount',
invoice.invoice_legal_monetary_total.charge_total_amount) invoice.invoice_legal_monetary_total.charge_total_amount)
fexml.set_element_amount('./cac:LegalMonetaryTotal/cbc:PayableAmount', fexml.set_element_amount(
'./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(reference, 'cac:InvoiceDocumentReference') fexml._do_set_billing_reference(
reference, 'cac:InvoiceDocumentReference')
def _set_credit_note_document_reference(fexml, reference): def _set_credit_note_document_reference(fexml, reference):
fexml._do_set_billing_reference(reference, 'cac:CreditNoteDocumentReference') fexml._do_set_billing_reference(
reference, 'cac:CreditNoteDocumentReference')
def _set_debit_note_document_reference(fexml, reference): def _set_debit_note_document_reference(fexml, reference):
fexml._do_set_billing_reference(reference, 'cac:DebitNoteDocumentReference') fexml._do_set_billing_reference(
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('./cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID', fexml.set_element(
'./cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID',
reference.uuid, reference.uuid,
schemeName='CUFE-SHA384') schemeName='CUFE-SHA384')
fexml.set_element('./cac:BillingReference/cac:InvoiceDocumentReference/cbc:IssueDate', fexml.set_element(
'./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):
@@ -546,7 +589,8 @@ 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(lambda: defaultdict(lambda: Amount(0.0))) withholding_amount_for = defaultdict(
lambda: defaultdict(lambda: Amount(0.0)))
percent_for = defaultdict(lambda: None) percent_for = defaultdict(lambda: None)
# requeridos para CUFE # requeridos para CUFE
@@ -564,9 +608,12 @@ 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:
tax_amount_for[subtotal.scheme.code]['tax_amount'] += subtotal.tax_amount scheme = subtotal.scheme.code
tax_amount_for[subtotal.scheme.code]['taxable_amount'] += invoice_line.taxable_amount tax_amount_for[scheme][
tax_amount_for[subtotal.scheme.code]['name'] = subtotal.scheme.name 'tax_amount'] += subtotal.tax_amount
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
@@ -575,12 +622,16 @@ 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:
withholding_amount_for[subtotal_withholding.scheme.code]['tax_amount'] += subtotal_withholding.tax_amount scheme = subtotal_withholding.scheme.code
withholding_amount_for[subtotal_withholding.scheme.code]['taxable_amount'] += invoice_line.withholding_taxable_amount withholding_amount_for[scheme][
'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[subtotal_withholding.scheme.code] = subtotal_withholding.percent percent_for[scheme] = subtotal_withholding.percent
total_withholding_amount += subtotal_withholding.tax_amount total_withholding_amount += subtotal_withholding.tax_amount
@@ -602,12 +653,14 @@ class DIANInvoiceXML(fe.FeXML):
tax_amount) tax_amount)
# DIAN 1.7.-2020: FAS05 # DIAN 1.7.-2020: FAS05
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
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(line, fexml.set_element_amount_for(
line,
'/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', '/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
amount_of['tax_amount']) amount_of['tax_amount'])
@@ -616,14 +669,19 @@ 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('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', line.set_element(
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/'
'cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', line.set_element(
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
'/cbc:ID',
cod_impuesto) cod_impuesto)
line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', line.set_element(
'/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()):
@@ -632,13 +690,12 @@ class DIANInvoiceXML(fe.FeXML):
# DIAN 1.7.-2020: FAS01 # DIAN 1.7.-2020: FAS01
line = fexml.fragment( line = fexml.fragment(
'./cac:WithholdingTaxTotal', append=next_append) './cac:WithholdingTaxTotal',
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, line, '/cac:WithholdingTaxTotal/cbc:TaxAmount', tax_amount)
'/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(
@@ -660,14 +717,17 @@ 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/cbc:Percent', '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory'
'/cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element( line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
':TaxScheme/cbc:ID',
cod_impuesto) cod_impuesto)
line.set_element( line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
':TaxScheme/cbc:Name',
'ReteRenta') 'ReteRenta')
# abstract method # abstract method
@@ -679,8 +739,7 @@ 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( fexml.set_element_amount_for(line,
line,
'./cac:TaxTotal/cbc:TaxAmount', './cac:TaxTotal/cbc:TaxAmount',
invoice_line.tax_amount) invoice_line.tax_amount)
@@ -689,7 +748,6 @@ 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',
@@ -698,21 +756,28 @@ 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:Percent', '%0.2f' % round(subtotal.percent, 2)) './cac:TaxTotal/cac:TaxSubtotal/cac: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:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code) './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
':TaxScheme/cbc:ID',
subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', subtotal.scheme.name) './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
':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( fexml.set_element_amount_for(line,
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,
@@ -727,44 +792,54 @@ 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:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2)) './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
':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:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code) './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
':TaxCategory/cac:TaxScheme/cbc:ID',
subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', subtotal.scheme.name) './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
':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' % (fexml.tag_document()), './cac:%sLine' %
(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( fexml.set_element_amount_for(line,
line,
'./cbc:LineExtensionAmount', './cbc:LineExtensionAmount',
invoice_line.total_amount) invoice_line.total_amount)
if not isinstance( if not isinstance(invoice_line.tax, TaxTotalOmit):
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, WithholdingTaxTotalOmit): invoice_line.withholding,
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', invoice_line.item.description) './cac:Item/cbc:Description',
invoice_line.item.description)
line.set_element( line.set_element(
'./cac:Item/cac:StandardItemIdentification/cbc:ID', './cac:Item/cac:StandardItemIdentification/cbc:ID',
@@ -777,10 +852,8 @@ 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( line.set_element('./cac:Price/cbc:BaseQuantity',
'./cac:Price/cbc:BaseQuantity',
invoice_line.price.quantity, invoice_line.price.quantity,
unitCode=invoice_line.quantity.code) unitCode=invoice_line.quantity.code)
@@ -815,6 +888,56 @@ 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,36 +14,45 @@ __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 sujetos no obligados a expedir factura de venta o documento equivalente. lo indicado para él Documento soporte en adquisiciones efectuadas con
sujetos no obligados a expedir factura de venta o documento equivalente.
""" """
def __init__(self, invoice, tag_document='Invoice'): def __init__(self, invoice, tag_document='Invoice'):
super().__init__(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1') super().__init__(
tag_document,
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
# DIAN 1.1.-2021: DSAB03 # DIAN 1.1.-2021: DSAB03
# DIAN 1.1.-2021: NSAB03 # DIAN 1.1.-2021: NSAB03
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:InvoiceControl')
# DIAN 1.1.-2021: DSAB13 # DIAN 1.1.-2021: DSAB13
# DIAN 1.1.-2021: NSAB13 # DIAN 1.1.-2021: NSAB13
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:InvoiceSource')
# DIAN 1.1.-2021: DSAB18 # DIAN 1.1.-2021: DSAB18
# DIAN 1.1.-2021: NSAB18 # DIAN 1.1.-2021: NSAB18
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:SoftwareProvider')
# DIAN 1.1.-2021: DSAB27 # DIAN 1.1.-2021: DSAB27
# DIAN 1.1.-2021: NSAB27 # DIAN 1.1.-2021: NSAB27
self.placeholder_for( self.placeholder_for(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts'
':DianExtensions/sts:SoftwareSecurityCode')
# DIAN 1.1.-2021: DSAB30 DSAB31 # DIAN 1.1.-2021: 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/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/'
'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
@@ -52,7 +61,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
extcontent = ublextension.find_or_create_element( ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent') '/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice(invoice) self.attach_invoice(invoice)
@@ -70,51 +79,61 @@ 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:Address') './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:ID', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CityName', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:PostalZone', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CountrySubentity', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cbc:CountrySubentityCode', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:AddressLine/cbc:Line', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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:Address/cac:Country/cbc:IdentificationCode', './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac'
':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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name', fexml.set_element(
'./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
@@ -134,13 +153,15 @@ 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:RegistrationName', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:CompanyID', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
':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
@@ -149,25 +170,29 @@ 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:TaxLevelCode', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:TaxScheme') './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:ID', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:Name', './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac'
':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):
@@ -193,7 +218,8 @@ 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:RegistrationName', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
':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()
@@ -205,7 +231,8 @@ 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:CompanyID', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
':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
@@ -214,24 +241,28 @@ 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:TaxLevelCode', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc'
':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:TaxScheme') './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:ID', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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:TaxScheme/cbc:Name', './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac'
':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):
@@ -328,7 +359,8 @@ class DIANSupportDocumentXML(fe.FeXML):
reference.uuid, reference.uuid,
schemeName=schemeName) schemeName=schemeName)
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):
@@ -419,14 +451,17 @@ 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/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', 'IVA') '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
'/cbc:Name', 'IVA')
# abstract method # abstract method
@@ -455,7 +490,8 @@ 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:TaxCategory/cbc:Percent', './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
':TaxCategory/cbc:Percent',
'%0.2f' % '%0.2f' %
round( round(
subtotal.percent, subtotal.percent,
@@ -464,10 +500,12 @@ 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:TaxCategory/cac:TaxScheme/cbc:ID', './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
':TaxCategory/cac:TaxScheme/cbc:ID',
subtotal.scheme.code) subtotal.scheme.code)
line.set_element( line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac'
':TaxCategory/cac:TaxScheme/cbc:Name',
subtotal.scheme.name) subtotal.scheme.name)
def set_invoice_line_tax(fexml, line, invoice_line): def set_invoice_line_tax(fexml, line, invoice_line):
@@ -488,7 +526,8 @@ 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:Percent', './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc'
':Percent',
'%0.2f' % '%0.2f' %
round( round(
subtotal.percent, subtotal.percent,
@@ -497,10 +536,12 @@ 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:TaxScheme/cbc:ID', './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac'
':TaxScheme/cbc:ID',
subtotal.scheme.code) subtotal.scheme.code)
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',
subtotal.scheme.name) subtotal.scheme.name)
def set_invoice_lines(fexml, invoice): def set_invoice_lines(fexml, invoice):
@@ -615,7 +656,9 @@ class DIANSupportDocumentXML(fe.FeXML):
(fexml.tag_document()), (fexml.tag_document()),
invoice.invoice_type_code, invoice.invoice_type_code,
listAgencyID='195', listAgencyID='195',
listAgencyName='No matching global declaration available for the validation root', listAgencyName=(
'No matching global declaration available for the '
'validation root'),
listURI='http://www.dian.gov.co') 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(

View File

@@ -1,367 +0,0 @@
import facho.model as model
import facho.model.fields as fields
import facho.fe.form as form
from facho import fe
from .common import *
from . import dian
from datetime import date, datetime
from collections import defaultdict
from copy import copy
import hashlib
class PhysicalLocation(model.Model):
__name__ = 'PhysicalLocation'
address = fields.Many2One(Address, namespace='cac')
class PartyTaxScheme(model.Model):
__name__ = 'PartyTaxScheme'
registration_name = fields.Many2One(Name, name='RegistrationName', namespace='cbc')
company_id = fields.Many2One(ID, name='CompanyID', namespace='cbc')
tax_level_code = fields.Many2One(ID, name='TaxLevelCode', namespace='cbc', default='ZZ')
class Party(model.Model):
__name__ = 'Party'
id = fields.Virtual(setter='_on_set_id')
name = fields.Many2One(PartyName, namespace='cac')
tax_scheme = fields.Many2One(PartyTaxScheme, namespace='cac')
location = fields.Many2One(PhysicalLocation, namespace='cac')
contact = fields.Many2One(Contact, namespace='cac')
def _on_set_id(self, name, value):
self.tax_scheme.company_id = value
return value
class AccountingCustomerParty(model.Model):
__name__ = 'AccountingCustomerParty'
party = fields.Many2One(Party, namespace='cac')
class AccountingSupplierParty(model.Model):
__name__ = 'AccountingSupplierParty'
party = fields.Many2One(Party, namespace='cac')
class Quantity(model.Model):
__name__ = 'Quantity'
code = fields.Attribute('unitCode', default='NAR')
def __setup__(self):
self.value = 0
def __default_set__(self, value):
self.value = value
return value
def __default_get__(self, name, value):
return self.value
class Amount(model.Model):
__name__ = 'Amount'
currency = fields.Attribute('currencyID', default='COP')
value = fields.Amount(name='amount', default=0.00, precision=2)
def __default_set__(self, value):
self.value = value
return value
def __default_get__(self, name, value):
return self.value
def __str__(self):
return str(self.value)
class Price(model.Model):
__name__ = 'Price'
amount = fields.Many2One(Amount, name='PriceAmount', namespace='cbc')
def __default_set__(self, value):
self.amount = value
return value
def __default_get__(self, name, value):
return self.amount
class Percent(model.Model):
__name__ = 'Percent'
class TaxScheme(model.Model):
__name__ = 'TaxScheme'
id = fields.Many2One(ID, namespace='cbc')
name= fields.Many2One(Name, namespace='cbc')
class TaxCategory(model.Model):
__name__ = 'TaxCategory'
percent = fields.Many2One(Percent, namespace='cbc')
tax_scheme = fields.Many2One(TaxScheme, namespace='cac')
class TaxSubTotal(model.Model):
__name__ = 'TaxSubTotal'
taxable_amount = fields.Many2One(Amount, name='TaxableAmount', namespace='cbc', default=0.00)
tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00)
tax_percent = fields.Many2One(Percent, namespace='cbc')
tax_category = fields.Many2One(TaxCategory, namespace='cac')
percent = fields.Virtual(setter='set_category', getter='get_category')
scheme = fields.Virtual(setter='set_category', getter='get_category')
def set_category(self, name, value):
if name == 'percent':
self.tax_category.percent = value
# TODO(bit4bit) debe variar en conjunto?
self.tax_percent = value
elif name == 'scheme':
self.tax_category.tax_scheme.id = value
return value
def get_category(self, name, value):
if name == 'percent':
return value
elif name == 'scheme':
return self.tax_category.tax_scheme
class TaxTotal(model.Model):
__name__ = 'TaxTotal'
tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00)
subtotals = fields.One2Many(TaxSubTotal, namespace='cac')
class AllowanceCharge(model.Model):
__name__ = 'AllowanceCharge'
amount = fields.Many2One(Amount, namespace='cbc')
is_discount = fields.Virtual(default=False)
def isCharge(self):
return self.is_discount == False
def isDiscount(self):
return self.is_discount == True
class Taxes:
class Scheme:
def __init__(self, scheme):
self.scheme = scheme
class Iva(Scheme):
def __init__(self, percent):
super().__init__('01')
self.percent = percent
def calculate(self, amount):
return form.Amount(amount) * form.Amount(self.percent / 100)
class InvoiceLine(model.Model):
__name__ = 'InvoiceLine'
id = fields.Many2One(ID, namespace='cbc')
quantity = fields.Many2One(Quantity, name='InvoicedQuantity', namespace='cbc')
taxtotal = fields.Many2One(TaxTotal, namespace='cac')
price = fields.Many2One(Price, namespace='cac')
amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc')
allowance_charge = fields.One2Many(AllowanceCharge, 'cac')
tax_amount = fields.Virtual(getter='get_tax_amount')
def __setup__(self):
self._taxs = defaultdict(list)
self._subtotals = {}
def add_tax(self, tax):
if not isinstance(tax, Taxes.Scheme):
raise ValueError('tax expected TaxIva')
# inicialiamos subtotal para impuesto
if not tax.scheme in self._subtotals:
subtotal = self.taxtotal.subtotals.create()
subtotal.scheme = tax.scheme
self._subtotals[tax.scheme] = subtotal
self._taxs[tax.scheme].append(tax)
def get_tax_amount(self, name, value):
total = form.Amount(0)
for (scheme, subtotal) in self._subtotals.items():
total += subtotal.tax_amount
return total
@fields.on_change(['price', 'quantity'])
def update_amount(self, name, value):
charge = form.AmountCollection(self.allowance_charge)\
.filter(lambda charge: charge.isCharge())\
.map(lambda charge: charge.amount)\
.sum()
discount = form.AmountCollection(self.allowance_charge)\
.filter(lambda charge: charge.isDiscount())\
.map(lambda charge: charge.amount)\
.sum()
total = form.Amount(self.quantity) * form.Amount(self.price)
self.amount = total + charge - discount
for (scheme, subtotal) in self._subtotals.items():
subtotal.tax_amount = 0
for (scheme, taxes) in self._taxs.items():
for tax in taxes:
self._subtotals[scheme].tax_amount += tax.calculate(self.amount)
class LegalMonetaryTotal(model.Model):
__name__ = 'LegalMonetaryTotal'
line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc', default=0)
tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount', namespace='cbc', default=form.Amount(0))
tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount', namespace='cbc', default=form.Amount(0))
charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount', namespace='cbc', default=form.Amount(0))
payable_amount = fields.Many2One(Amount, name='PayableAmount', namespace='cbc', default=form.Amount(0))
@fields.on_change(['tax_inclusive_amount', 'charge_total'])
def update_payable_amount(self, name, value):
self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount
class DIANExtensionContent(model.Model):
__name__ = 'ExtensionContent'
dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts')
class DIANExtension(model.Model):
__name__ = 'UBLExtension'
content = fields.Many2One(DIANExtensionContent, namespace='ext')
def __default_get__(self, name, value):
return self.content.dian
class UBLExtension(model.Model):
__name__ = 'UBLExtension'
content = fields.Many2One(Element, name='ExtensionContent', namespace='ext', default='')
class UBLExtensions(model.Model):
__name__ = 'UBLExtensions'
dian = fields.Many2One(DIANExtension, namespace='ext', create=True)
extension = fields.Many2One(UBLExtension, namespace='ext', create=True)
class Invoice(model.Model):
__name__ = 'Invoice'
__namespace__ = fe.NAMESPACES
_ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext')
# nos interesa el acceso solo los atributos de la DIAN
dian = fields.Virtual(getter='get_dian_extension')
profile_id = fields.Many2One(Element, name='ProfileID', namespace='cbc', default='DIAN 2.1')
profile_execute_id = fields.Many2One(Element, name='ProfileExecuteID', namespace='cbc', default='2')
id = fields.Many2One(ID, namespace='cbc')
issue = fields.Virtual(setter='set_issue')
issue_date = fields.Many2One(Date, name='IssueDate', namespace='cbc')
issue_time = fields.Many2One(Time, name='IssueTime', namespace='cbc')
period = fields.Many2One(Period, name='InvoicePeriod', namespace='cac')
supplier = fields.Many2One(AccountingSupplierParty, namespace='cac')
customer = fields.Many2One(AccountingCustomerParty, namespace='cac')
legal_monetary_total = fields.Many2One(LegalMonetaryTotal, namespace='cac')
lines = fields.One2Many(InvoiceLine, namespace='cac')
taxtotal_01 = fields.Many2One(TaxTotal)
taxtotal_04 = fields.Many2One(TaxTotal)
taxtotal_03 = fields.Many2One(TaxTotal)
def __setup__(self):
self._namespace_prefix = 'fe'
# Se requieren minimo estos impuestos para
# validar el cufe
self._subtotal_01 = self.taxtotal_01.subtotals.create()
self._subtotal_01.scheme = '01'
self._subtotal_01.percent = 19.0
self._subtotal_04 = self.taxtotal_04.subtotals.create()
self._subtotal_04.scheme = '04'
self._subtotal_03 = self.taxtotal_03.subtotals.create()
self._subtotal_03.scheme = '03'
def cufe(self, token, environment):
valor_bruto = self.legal_monetary_total.line_extension_amount
valor_total_pagar = self.legal_monetary_total.payable_amount
valor_impuesto_01 = form.Amount(0.0)
valor_impuesto_04 = form.Amount(0.0)
valor_impuesto_03 = form.Amount(0.0)
for line in self.lines:
for subtotal in line.taxtotal.subtotals:
if subtotal.scheme.id == '01':
valor_impuesto_01 += subtotal.tax_amount
elif subtotal.scheme.id == '04':
valor_impuesto_04 += subtotal.tax_amount
elif subtotal.scheme.id == '03':
valor_impuesto_03 += subtotal.tax_amount
pattern = [
'%s' % str(self.id),
'%s' % str(self.issue_date),
'%s' % str(self.issue_time),
valor_bruto.truncate_as_string(2),
'01', valor_impuesto_01.truncate_as_string(2),
'04', valor_impuesto_04.truncate_as_string(2),
'03', valor_impuesto_03.truncate_as_string(2),
valor_total_pagar.truncate_as_string(2),
str(self.supplier.party.id),
str(self.customer.party.id),
str(token),
str(environment)
]
cufe = "".join(pattern)
h = hashlib.sha384()
h.update(cufe.encode('utf-8'))
return h.hexdigest()
@fields.on_change(['lines'])
def update_legal_monetary_total(self, name, value):
self.legal_monetary_total.line_extension_amount = 0
self.legal_monetary_total.tax_inclusive_amount = 0
for line in self.lines:
self.legal_monetary_total.line_extension_amount += line.amount
self.legal_monetary_total.tax_inclusive_amount += line.amount + line.tax_amount
def set_issue(self, name, value):
if not isinstance(value, datetime):
raise ValueError('expected type datetime')
self.issue_date = value.date()
self.issue_time = value
def get_dian_extension(self, name, _value):
return self._ubl_extensions.dian
def to_xml(self, **kw):
# al generar documento el namespace
# se hace respecto a la raiz
return super().to_xml(**kw)\
.replace("fe:", "")\
.replace("xmlns:fe", "xmlns")

View File

@@ -1,90 +0,0 @@
import facho.model as model
import facho.model.fields as fields
from datetime import date, datetime
__all__ = ['Element', 'PartyName', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country', 'Contact']
class Element(model.Model):
"""
Lo usuamos para elementos que solo manejan contenido
"""
__name__ = 'Element'
class Name(model.Model):
__name__ = 'Name'
class Date(model.Model):
__name__ = 'Date'
def __default_set__(self, value):
if isinstance(value, str):
return value
if isinstance(value, date):
return value.isoformat()
def __str__(self):
return str(self._value)
class Time(model.Model):
__name__ = 'Time'
def __default_set__(self, value):
if isinstance(value, str):
return value
if isinstance(value, date):
return value.strftime('%H:%M:%S-05:00')
def __str__(self):
return str(self._value)
class Period(model.Model):
__name__ = 'Period'
start_date = fields.Many2One(Date, name='StartDate', namespace='cbc')
end_date = fields.Many2One(Date, name='EndDate', namespace='cbc')
class ID(model.Model):
__name__ = 'ID'
def __default_get__(self, name, value):
return self._value
def __str__(self):
return str(self._value)
class Country(model.Model):
__name__ = 'Country'
name = fields.Many2One(Element, name='Name', namespace='cbc')
class Address(model.Model):
__name__ = 'Address'
#DIAN 1.7.-2020: FAJ08
#DIAN 1.7.-2020: CAJ09
id = fields.Many2One(Element, name='ID', namespace='cbc')
#DIAN 1.7.-2020: FAJ09
#DIAN 1.7.-2020: CAJ10
city = fields.Many2One(Element, name='CityName', namespace='cbc')
class PartyName(model.Model):
__name__ = 'PartyName'
name = fields.Many2One(Name, namespace='cbc')
def __default_set__(self, value):
self.name = value
return value
def __default_get__(self, name, value):
return self.name
class Contact(model.Model):
__name__ = 'Contact'
email = fields.Many2One(Name, name='ElectronicEmail', namespace='cbc')

View File

@@ -1,58 +0,0 @@
import facho.model as model
import facho.model.fields as fields
from .common import *
class DIANElement(Element):
"""
Elemento que contiene atributos por defecto.
Puede extender esta clase y modificar los atributos nuevamente
"""
__name__ = 'DIANElement'
scheme_id = fields.Attribute('schemeID', default='4')
scheme_name = fields.Attribute('schemeName', default='31')
scheme_agency_name = fields.Attribute('schemeAgencyName', default='CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)')
scheme_agency_id = fields.Attribute('schemeAgencyID', default='195')
class SoftwareProvider(model.Model):
__name__ = 'SoftwareProvider'
provider_id = fields.Many2One(Element, name='ProviderID', namespace='sts')
software_id = fields.Many2One(Element, name='SoftwareID', namespace='sts')
class InvoiceSource(model.Model):
__name__ = 'InvoiceSource'
identification_code = fields.Many2One(Element, name='IdentificationCode', namespace='sts', default='CO')
class AuthorizedInvoices(model.Model):
__name__ = 'AuthorizedInvoices'
prefix = fields.Many2One(Element, name='Prefix', namespace='sts')
from_range = fields.Many2One(Element, name='From', namespace='sts')
to_range = fields.Many2One(Element, name='To', namespace='sts')
class InvoiceControl(model.Model):
__name__ = 'InvoiceControl'
authorization = fields.Many2One(Element, name='InvoiceAuthorization', namespace='sts')
period = fields.Many2One(Period, name='AuthorizationPeriod', namespace='sts')
invoices = fields.Many2One(AuthorizedInvoices, namespace='sts')
class AuthorizationProvider(model.Model):
__name__ = 'AuthorizationProvider'
id = fields.Many2One(DIANElement, name='AuthorizationProviderID', namespace='sts', default='800197268')
class DianExtensions(model.Model):
__name__ = 'DianExtensions'
authorization_provider = fields.Many2One(AuthorizationProvider, namespace='sts', create=True)
software_security_code = fields.Many2One(Element, name='SoftwareSecurityCode', namespace='sts')
software_provider = fields.Many2One(SoftwareProvider, namespace='sts')
source = fields.Many2One(InvoiceSource, namespace='sts')
control = fields.Many2One(InvoiceControl, namespace='sts')

View File

@@ -8,21 +8,91 @@
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 .devengado import *
from .deduccion import *
from .trabajador import *
from .empleador import *
from .pago import *
from .lugar import Lugar
from .amount import Amount from .amount import Amount
from .exception import * from .deduccion import (
Deduccion,
DeduccionFondoPension,
DeduccionSalud,
)
from .devengado import (
Devengado,
DevengadoBasico,
DevengadoHoraExtra,
DevengadoHorasExtrasDiarias,
DevengadoHorasExtrasDiariasDominicalesYFestivos,
DevengadoHorasExtrasNocturnas,
DevengadoHorasExtrasNocturnasDominicalesYFestivos,
DevengadoHorasRecargoDiariasDominicalesYFestivos,
DevengadoHorasRecargoNocturno,
DevengadoHorasRecargoNocturnoDominicalesYFestivos,
DevengadoTransporte,
)
from .empleador import Empleador
from .exception import DIANNominaIndividualError
from .lugar import Lugar
from .pago import (
FormaPago,
MetodoPago,
Pago,
)
from .trabajador import (
LugarTrabajo,
SubTipoTrabajador,
TipoContrato,
TipoDocumento,
TipoTrabajador,
Trabajador,
)
__all__ = [
'Amount',
'Deduccion',
'DeduccionFondoPension',
'DeduccionSalud',
'Devengado',
'DevengadoBasico',
'DevengadoHoraExtra',
'DevengadoHorasExtrasDiarias',
'DevengadoHorasExtrasDiariasDominicalesYFestivos',
'DevengadoHorasExtrasNocturnas',
'DevengadoHorasExtrasNocturnasDominicalesYFestivos',
'DevengadoHorasRecargoDiariasDominicalesYFestivos',
'DevengadoHorasRecargoNocturno',
'DevengadoHorasRecargoNocturnoDominicalesYFestivos',
'DevengadoTransporte',
'DIANNominaIndividual',
'DIANNominaIndividualDeAjuste',
'DIANNominaXML',
'DIANNominaIndividualError',
'DianXMLExtensionSigner',
'Empleador',
'Fecha',
'FechaPago',
'FormaPago',
'InformacionGeneral',
'Lugar',
'LugarTrabajo',
'Metadata',
'MetodoPago',
'Novedad',
'NumeroSecuencia',
'Pago',
'Periodo',
'PeriodoNomina',
'Proveedor',
'SubTipoTrabajador',
'TipoContrato',
'TipoDocumento',
'TipoMoneda',
'TipoTrabajador',
'Trabajador',
]
class Fecha: class Fecha:
def __init__(self, fecha): def __init__(self, fecha):
@@ -46,6 +116,7 @@ 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)
@@ -84,19 +155,21 @@ class NumeroSecuencia:
# NIE012 # NIE012
Numero=numero) Numero=numero)
@dataclass @dataclass
class Periodo: class Periodo:
fecha_ingreso: typing.Union[str, Fecha] fecha_ingreso: str | Fecha
fecha_liquidacion_inicio: typing.Union[str, Fecha] fecha_liquidacion_inicio: str | Fecha
fecha_liquidacion_fin: typing.Union[str, Fecha] fecha_liquidacion_fin: str | Fecha
fecha_generacion: typing.Union[str, Fecha] fecha_generacion: str | Fecha
tiempo_laborado: int = 1 tiempo_laborado: int = 1
fecha_retiro: typing.Union[str, Fecha] = None fecha_retiro: str | Fecha | None = 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) self.fecha_liquidacion_inicio = Fecha.cast(
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)
@@ -107,7 +180,8 @@ class Periodo:
# NIE003 # NIE003
FechaRetiro=self.fecha_retiro, FechaRetiro=self.fecha_retiro,
# NIE004 # NIE004
FechaLiquidacionInicio=self.fecha_liquidacion_inicio, FechaLiquidacionInicio=(
self.fecha_liquidacion_inicio),
# NIE005 # NIE005
FechaLiquidacionFin=self.fecha_liquidacion_fin, FechaLiquidacionFin=self.fecha_liquidacion_fin,
# NIE006 # NIE006
@@ -115,6 +189,7 @@ class Periodo:
# NIE008 # NIE008
FechaGen=self.fecha_generacion) FechaGen=self.fecha_generacion)
@dataclass @dataclass
class Proveedor: class Proveedor:
razon_social: str razon_social: str
@@ -141,11 +216,16 @@ 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(scopexml.xpath_from_root('/InformacionGeneral'), 'Ambiente') ambiente = fexml.get_element_attribute(
codigo_qr = f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={cune}" scopexml.xpath_from_root('/InformacionGeneral'), 'Ambiente')
codigo_qr = (
f"https://catalogo-vpfe.dian.gov.co/document/"
f"searchqr?documentkey={cune}")
if InformacionGeneral.AMBIENTE_PRUEBAS == ambiente: if InformacionGeneral.AMBIENTE_PRUEBAS == ambiente:
codigo_qr = f"https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey={cune}" codigo_qr = (
f"https://catalogo-vpfe-hab.dian.gov.co/document/"
f"searchqr?documentkey={cune}")
elif ambiente is None: elif ambiente is None:
raise RuntimeError('fail to get InformacionGeneral/@Ambiente') raise RuntimeError('fail to get InformacionGeneral/@Ambiente')
@@ -153,13 +233,15 @@ class Proveedor:
# NIE020 # NIE020
software_code = self._software_security_code(fexml, scopexml) software_code = self._software_security_code(fexml, scopexml)
fexml.set_attributes(scopexml.xpath_from_root('/ProveedorXML'), SoftwareSC=software_code) fexml.set_attributes(
scopexml.xpath_from_root('/ProveedorXML'),
SoftwareSC=software_code)
def _software_security_code(self, fexml, scopexml): def _software_security_code(self, fexml, scopexml):
# 8.2 # 8.2
numero = fexml.get_element_attribute(scopexml.xpath_from_root('/NumeroSecuenciaXML'), 'Numero') numero = fexml.get_element_attribute(
scopexml.xpath_from_root('/NumeroSecuenciaXML'), 'Numero')
if numero is None: if numero is None:
raise RuntimeError('fallo obtener NumeroSequenciaXML/@Numero') raise RuntimeError('fallo obtener NumeroSequenciaXML/@Numero')
@@ -173,6 +255,7 @@ 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
@@ -181,18 +264,33 @@ class Metadata:
lugar_generacion: Lugar lugar_generacion: Lugar
proveedor: Proveedor proveedor: Proveedor
def apply(self, novedad, numero_secuencia_xml, lugar_generacion_xml, proveedor_xml): def apply(
self,
novedad,
numero_secuencia_xml,
lugar_generacion_xml,
proveedor_xml):
if novedad: 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(lugar_generacion_xml, './LugarGeneracionXML') self.lugar_generacion.apply(
lugar_generacion_xml,
'./LugarGeneracionXML')
self.proveedor.apply(proveedor_xml) self.proveedor.apply(proveedor_xml)
def post_apply(self, fexml, scopexml, novedad, numero_secuencia_xml, lugar_generacion_xml, proveedor_xml): def post_apply(
self,
fexml,
scopexml,
novedad,
numero_secuencia_xml,
lugar_generacion_xml,
proveedor_xml):
self.proveedor.post_apply(fexml, scopexml, proveedor_xml) 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
@@ -203,6 +301,7 @@ 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
@@ -213,6 +312,7 @@ 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
@@ -259,7 +359,7 @@ class InformacionGeneral:
def __str__(self): def __str__(self):
self.valor self.valor
fecha_generacion: typing.Union[str, Fecha] fecha_generacion: str | Fecha
hora_generacion: str hora_generacion: str
periodo_nomina: PeriodoNomina periodo_nomina: PeriodoNomina
tipo_moneda: TipoMoneda tipo_moneda: TipoMoneda
@@ -328,17 +428,28 @@ 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__(pkcs12_path, passphrase=passphrase, localpolicy=localpolicy) super().__init__(
pkcs12_path,
passphrase=passphrase,
localpolicy=localpolicy)
def _element_extension_content(self, fachoxml): def _element_extension_content(self, fachoxml):
return fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') return fachoxml.builder.xpath(
fachoxml.root,
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
class DIANNominaXML: class DIANNominaXML:
def __init__(self, tag_document, xpath_ajuste=None, schemaLocation=None, namespace_ajuste=None): def __init__(
self,
tag_document,
xpath_ajuste=None,
schemaLocation=None,
namespace_ajuste=None):
self.informacion_general_version = None self.informacion_general_version = None
self.tag_document = tag_document self.tag_document = tag_document
@@ -346,21 +457,26 @@ 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(tag_document, 'dian:gov:co:facturaelectronica:NominaIndividual') self.fexml = fe.FeXML(
tag_document,
'dian:gov:co:facturaelectronica:NominaIndividual')
self.fexml.root.set("SchemaLocation", "") self.fexml.root.set("SchemaLocation", "")
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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') self.fexml.placeholder_for(
'./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('./ReemplazandoPredecesor', optional=True) self.root_fragment.placeholder_for(
self.root_fragment.placeholder_for('./EliminandoPredecesor', optional=True) './ReemplazandoPredecesor', 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')
@@ -374,16 +490,20 @@ 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('./Devengados/Transporte', optional=True) self.root_fragment.placeholder_for(
'./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('./InformacionGeneral') self.informacion_general_xml = self.root_fragment.fragment(
'./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('./NumeroSecuenciaXML') self.numero_secuencia_xml = self.root_fragment.fragment(
self.lugar_generacion_xml = self.root_fragment.fragment('./LugarGeneracionXML') './NumeroSecuenciaXML')
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')
@@ -399,13 +519,19 @@ class DIANNominaXML:
raise ValueError('se espera tipo Metadata') raise ValueError('se espera tipo Metadata')
self.metadata = metadata self.metadata = metadata
self.metadata.apply(self.novedad, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml) self.metadata.apply(
self.novedad,
self.numero_secuencia_xml,
self.lugar_generacion_xml,
self.proveedor_xml)
def asignar_informacion_general(self, general): 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_xml, self.informacion_general_version) self.informacion_general.apply(
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):
@@ -498,10 +624,13 @@ 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_xpath, 'CUNE'), 'cune': self.fexml.get_element_attribute(
'fecha_generacion': self.fexml.get_element_attribute(xpath, 'FechaGen'), xpath, 'CUNE'),
'numero': self.fexml.get_element_attribute(self.root_fragment('/NumeroSecuenciaXML', 'Numero')) 'fecha_generacion': self.fexml.get_element_attribute(
} 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()
@@ -512,26 +641,38 @@ 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.fexml, self.root_fragment, self.informacion_general_xml) self.informacion_general.post_apply(
self.fexml, self.root_fragment, self.informacion_general_xml)
if self.metadata is not None: if self.metadata is not None:
self.metadata.post_apply(self.fexml, self.root_fragment, self.novedad, self.numero_secuencia_xml, self.lugar_generacion_xml, self.proveedor_xml) self.metadata.post_apply(
self.fexml,
self.root_fragment,
self.novedad,
self.numero_secuencia_xml,
self.lugar_generacion_xml,
self.proveedor_xml)
return self.fexml return self.fexml
def _comprobante_total(self): def _comprobante_total(self):
devengados_total = self.root_fragment.get_element_text_or_attribute('./DevengadosTotal', '0.0') devengados_total = self.root_fragment.get_element_text_or_attribute(
deducciones_total = self.root_fragment.get_element_text_or_attribute('./DeduccionesTotal', '0.0') './DevengadosTotal', '0.0')
deducciones_total = self.root_fragment.get_element_text_or_attribute(
'./DeduccionesTotal', '0.0')
comprobante_total = Amount(devengados_total) - Amount(deducciones_total) comprobante_total = Amount(devengados_total) - \
Amount(deducciones_total)
self.root_fragment.set_element('./ComprobanteTotal', str(round(comprobante_total, 2))) self.root_fragment.set_element(
'./ComprobanteTotal', str(round(comprobante_total, 2)))
def _deducciones_total(self): def _deducciones_total(self):
xpaths = [ xpaths = [
self.root_fragment.xpath_from_root('/Deducciones/Salud/@Deduccion'), self.root_fragment.xpath_from_root(
self.root_fragment.xpath_from_root('/Deducciones/FondoPension/@Deduccion') '/Deducciones/Salud/@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))
@@ -540,15 +681,19 @@ class DIANNominaXML:
for deduccion in deducciones: for deduccion in deducciones:
deducciones_total += deduccion deducciones_total += deduccion
self.root_fragment.set_element('./DeduccionesTotal', str(round(deducciones_total, 2))) self.root_fragment.set_element(
'./DeduccionesTotal', str(round(deducciones_total, 2)))
def _devengados_total(self): def _devengados_total(self):
xpaths = [ xpaths = [
self.root_fragment.xpath_from_root('/Devengados/Basico/@SueldoTrabajado'), self.root_fragment.xpath_from_root(
self.root_fragment.xpath_from_root('/Devengados/Transporte/@AuxilioTransporte'), '/Devengados/Basico/@SueldoTrabajado'),
self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojS'), self.root_fragment.xpath_from_root(
self.root_fragment.xpath_from_root('/Devengados/Transporte/@ViaticoManuAlojNS') '/Devengados/Transporte/@AuxilioTransporte'),
] self.root_fragment.xpath_from_root(
'/Devengados/Transporte/@ViaticoManuAlojS'),
self.root_fragment.xpath_from_root(
'/Devengados/Transporte/@ViaticoManuAlojNS')]
devengados = map(lambda valor: Amount(valor), devengados = map(lambda valor: Amount(valor),
self._values_of_xpaths(xpaths)) self._values_of_xpaths(xpaths))
@@ -558,10 +703,13 @@ 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('./DevengadosTotal', str(round(devengados_total,2))) self.root_fragment.set_element(
'./DevengadosTotal', str(round(devengados_total, 2)))
def _values_of_xpaths(self, xpaths): def _values_of_xpaths(self, xpaths):
xpaths_values_of_values = map(lambda val: self.fexml.get_element_text_or_attribute(val, multiple=True), xpaths) xpaths_values_of_values = map(
lambda val: self.fexml.get_element_text_or_attribute(
val, multiple=True), xpaths)
xpaths_values = [] 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:
@@ -573,15 +721,22 @@ 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 = "dian:gov:co:facturaelectronica:NominaIndividual NominaIndividualElectronicaXSD.xsd" schema = (
"dian:gov:co:facturaelectronica:NominaIndividual"
" NominaIndividualElectronicaXSD.xsd"
)
super().__init__('NominaIndividual', schemaLocation=schema) super().__init__('NominaIndividual', schemaLocation=schema)
self.informacion_general_version = 'V1.0: Documento Soporte de Pago de Nómina Electrónica' self.informacion_general_version = (
'V1.0: Documento Soporte de Pago de Nómina Electrónica')
# TODO(bit4bit) confirmar que no tienen en comun con NominaIndividual # TODO(bit4bit) confirmar que no tienen en comun con NominaIndividual
class DIANNominaIndividualDeAjuste(DIANNominaXML): class DIANNominaIndividualDeAjuste(DIANNominaXML):
class Reemplazar(DIANNominaXML): class Reemplazar(DIANNominaXML):
@@ -594,7 +749,8 @@ 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('./Reemplazar/ReemplazandoPredecesor', None, fragment.set_element(
'./Reemplazar/ReemplazandoPredecesor', None,
# NIAE090 # NIAE090
NumeroPred=self.numero, NumeroPred=self.numero,
# NIAE191 # NIAE191
@@ -604,18 +760,28 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
) )
def __init__(self): def __init__(self):
schema = "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd" schema = (
"dian:gov:co:facturaelectronica:NominaIndividualDeAjuste"
" NominaIndividualDeAjusteElectronicaXSD.xsd"
)
super().__init__('NominaIndividualDeAjuste', './Reemplazar', schemaLocation=schema, namespace_ajuste='dian:gov:co:facturaelectronica:NominaIndividualDeAjuste') super().__init__(
'NominaIndividualDeAjuste',
'./Reemplazar',
schemaLocation=schema,
namespace_ajuste=(
'dian:gov:co:facturaelectronica:'
'NominaIndividualDeAjuste'))
self.informacion_general_version = 'V1.0: Nota de Ajuste de Documento Soporte de Pago de Nómina Electrónica' self.informacion_general_version = (
'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
@@ -636,10 +802,21 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
) )
def __init__(self): def __init__(self):
schema = "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd" schema = (
super().__init__('NominaIndividualDeAjuste', './Eliminar', schemaLocation=schema, namespace_ajuste='dian:gov:co:facturaelectronica:NominaIndividualDeAjuste') "dian:gov:co:facturaelectronica:NominaIndividualDeAjuste"
" NominaIndividualDeAjusteElectronicaXSD.xsd"
)
super().__init__(
'NominaIndividualDeAjuste',
'./Eliminar',
schemaLocation=schema,
namespace_ajuste=(
'dian:gov:co:facturaelectronica:'
'NominaIndividualDeAjuste'))
self.informacion_general_version = "V1.0: Nota de Ajuste de Documento Soporte de Pago de Nómina Electrónica" self.informacion_general_version = (
"V1.0: Nota de Ajuste de Documento Soporte de Pago de "
"Nómina Electrónica")
def asignar_predecesor(self, predecesor): def asignar_predecesor(self, predecesor):
if not isinstance(predecesor, self.Predecesor): if not isinstance(predecesor, self.Predecesor):
@@ -648,4 +825,3 @@ class DIANNominaIndividualDeAjuste(DIANNominaXML):
def __init__(self): def __init__(self):
super().__init__('NominaIndividualDeAjuste') super().__init__('NominaIndividualDeAjuste')

View File

@@ -1,4 +1,5 @@
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 * from .deduccion import Deduccion
from .salud import * from .salud import DeduccionSalud
from .fondo_pension import * from .fondo_pension import DeduccionFondoPension
__all__ = [ __all__ = [
'Deduccion', 'Deduccion',

View File

@@ -3,6 +3,7 @@ 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,6 +3,7 @@ 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
@@ -16,4 +17,3 @@ class DeduccionSalud(Deduccion):
# NIE163 # NIE163
Deduccion=self.deduccion Deduccion=self.deduccion
) )

View File

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

View File

@@ -1,8 +1,17 @@
from .basico import * from .basico import DevengadoBasico
from .transporte import * from .transporte import DevengadoTransporte
from .devengado import * from .devengado import Devengado
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,16 +30,17 @@ 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')
@@ -49,43 +50,47 @@ 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,6 +3,7 @@ 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
@@ -17,5 +18,6 @@ class DevengadoTransporte(Devengado):
# NIE072 # NIE072
ViaticoManuAlojS=self.viatico_manutencion, ViaticoManuAlojS=self.viatico_manutencion,
# NIE073 # NIE073
ViaticoManuAlojNS = self.viatico_manutencion_no_salarial ViaticoManuAlojNS=(
self.viatico_manutencion_no_salarial)
) )

View File

@@ -4,6 +4,7 @@ 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
@@ -31,5 +32,3 @@ class Empleador:
RazonSocial=self.razon_social RazonSocial=self.razon_social
) )

View File

@@ -3,6 +3,7 @@ import datetime
from facho import fe from facho import fe
class Habilitacion: class Habilitacion:
@dataclass @dataclass
@@ -16,18 +17,18 @@ class Habilitacion:
self.metadata = metadata self.metadata = metadata
def generar(self, zipname, fecha): def generar(self, zipname, fecha):
nominas = [] fe.DianZIP(open(zipname, 'w'))
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)
nomina = self._crear_nomina_individual() self._crear_nomina_individual()
# pag 96 # pag 96
nombre = "nie%010d%s%08x.xml" % (int(self.nit), fecha.strftime('%s'), consecutivo) "nie%010d%s%08x.xml" % (
int(self.nit), fecha.strftime('%s'), consecutivo)
def _crear_nomina_individual_reemplazar(self, nomina, fechabase): def _crear_nomina_individual_reemplazar(self, nomina, fechabase):
metadata = self.metadata metadata = self.metadata
@@ -36,10 +37,15 @@ 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')
informacion_general = nomina.informacion_general() nomina.informacion_general()
def _poblar_nomina(
def _poblar_nomina(self, nomina, metadata, fecha, prefijo='N', consecutivo='0001'): self,
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(
@@ -152,4 +158,3 @@ 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,6 +5,7 @@ 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,6 +3,7 @@ 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,6 +2,7 @@ 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,6 +2,7 @@ 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
@@ -11,4 +12,3 @@ 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,4 +1,5 @@
from .. import form from .. import form
class Pais(form.Country): class Pais(form.Country):
pass pass

View File

@@ -2,12 +2,11 @@ from dataclasses import dataclass, field
from ..amount import Amount from ..amount import Amount
from .tipo_contrato import * from .tipo_contrato import TipoContrato
from .tipo_documento import * from .tipo_documento import TipoDocumento
from .lugar_trabajo import * from .lugar_trabajo import LugarTrabajo
from .tipo_trabajador import * from .tipo_trabajador import TipoTrabajador
from .sub_tipo_trabajador import * from .sub_tipo_trabajador import SubTipoTrabajador
@dataclass @dataclass
@@ -29,10 +28,13 @@ class Trabajador:
codigo_trabajador: str = None codigo_trabajador: str = None
otros_nombres: str = None otros_nombres: str = None
sub_tipo: SubTipoTrabajador = field(default_factory=lambda: SubTipoTrabajador(code='00')) sub_tipo: SubTipoTrabajador = field(
default_factory=lambda: SubTipoTrabajador(code="00")
)
def apply(self, fragment): def apply(self, fragment):
fragment.set_attributes('./Trabajador', fragment.set_attributes(
"./Trabajador",
# NIE041 # NIE041
TipoTrabajador=self.tipo.code, TipoTrabajador=self.tipo.code,
# NIE042 # NIE042
@@ -53,13 +55,12 @@ 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 = self.lugar_trabajo.departamento.code, LugarTrabajoDepartamentoEstado=(
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
@@ -69,5 +70,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,10 +1,9 @@
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,12 +2,13 @@ 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,14 +2,13 @@ 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,12 +2,13 @@ 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,12 +2,13 @@ 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"]

View File

@@ -1,175 +0,0 @@
from .fields import Field
from collections import defaultdict
class ModelMeta(type):
def __new__(cls, name, bases, ns):
new = type.__new__(cls, name, bases, ns)
# mapeamos asignacion en declaracion de clase
# a attributo de objeto
if '__name__' in ns:
new.__name__ = ns['__name__']
if '__namespace__' in ns:
new.__namespace__ = ns['__namespace__']
else:
new.__namespace__ = {}
return new
class ModelBase(object, metaclass=ModelMeta):
def __new__(cls, *args, **kwargs):
obj = super().__new__(cls, *args, **kwargs)
obj._xml_attributes = {}
obj._fields = {}
obj._value = None
obj._namespace_prefix = None
obj._on_change_fields = defaultdict(list)
obj._order_fields = []
def on_change_fields_for_function():
# se recorre arbol de herencia buscando attributo on_changes
for parent_cls in type(obj).__mro__:
for parent_attr in dir(parent_cls):
parent_meth = getattr(parent_cls, parent_attr, None)
if not callable(parent_meth):
continue
on_changes = getattr(parent_meth, 'on_changes', None)
if on_changes:
return (parent_meth, on_changes)
return (None, [])
# forzamos registros de campos al modelo
# al instanciar
for (key, v) in type(obj).__dict__.items():
if isinstance(v, fields.Field):
obj._order_fields.append(key)
if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function) or isinstance(v, fields.Amount):
if hasattr(v, 'default') and v.default is not None:
setattr(obj, key, v.default)
if hasattr(v, 'create') and v.create == True:
setattr(obj, key, '')
# register callbacks for changes
(fun, on_change_fields) = on_change_fields_for_function()
for field in on_change_fields:
obj._on_change_fields[field].append(fun)
# post inicializacion del objeto
obj.__setup__()
return obj
def _set_attribute(self, field, name, value):
self._xml_attributes[field] = (name, value)
def __setitem__(self, key, val):
self._xml_attributes[key] = val
def __getitem__(self, key):
return self._xml_attributes[key]
def _get_field(self, name):
return self._fields[name]
def _set_field(self, name, field):
field.name = name
self._fields[name] = field
def _set_content(self, value):
default = self.__default_set__(value)
if default is not None:
self._value = default
def to_xml(self):
"""
Genera xml del modelo y sus relaciones
"""
def _hook_before_xml():
self.__before_xml__()
for field in self._fields.values():
if hasattr(field, '__before_xml__'):
field.__before_xml__()
_hook_before_xml()
tag = self.__name__
ns = ''
if self._namespace_prefix is not None:
ns = "%s:" % (self._namespace_prefix)
pair_attributes = ["%s=\"%s\"" % (k, v) for (k, v) in self._xml_attributes.values()]
for (prefix, url) in self.__namespace__.items():
pair_attributes.append("xmlns:%s=\"%s\"" % (prefix, url))
attributes = ""
if pair_attributes:
attributes = " " + " ".join(pair_attributes)
content = ""
ordered_fields = {}
for name in self._order_fields:
if name in self._fields:
ordered_fields[name] = True
else:
for key in self._fields.keys():
if key.startswith(name):
ordered_fields[key] = True
for name in ordered_fields.keys():
value = self._fields[name]
# al ser virtual no adicinamos al arbol xml
if hasattr(value, 'virtual') and value.virtual:
continue
if hasattr(value, 'to_xml'):
content += value.to_xml()
elif isinstance(value, str):
content += value
if self._value is not None:
content += str(self._value)
if content == "":
return "<%s%s%s/>" % (ns, tag, attributes)
else:
return "<%s%s%s>%s</%s%s>" % (ns, tag, attributes, content, ns, tag)
def __str__(self):
return self.to_xml()
class Model(ModelBase):
"""
Model clase que representa el modelo
"""
def __before_xml__(self):
"""
Ejecuta antes de generar el xml, este
metodo sirve para realizar actualizaciones
en los campos en el ultimo momento
"""
pass
def __default_set__(self, value):
"""
Al asignar un valor al modelo atraves de una relacion (person.relation = '33')
se puede personalizar como hacer esta asignacion.
"""
return value
def __default_get__(self, name, value):
"""
Al obtener el valor atraves de una relacion (age = person.age)
Retorno de valor por defecto
"""
return value
def __setup__(self):
"""
Inicializar modelo
"""

View File

@@ -1,21 +0,0 @@
from .attribute import Attribute
from .many2one import Many2One
from .one2many import One2Many
from .function import Function
from .virtual import Virtual
from .field import Field
from .amount import Amount
__all__ = [Attribute, One2Many, Many2One, Virtual, Field, Amount]
def on_change(fields):
from functools import wraps
def decorator(func):
setattr(func, 'on_changes', fields)
@wraps(func)
def wrapper(self, *arg, **kwargs):
return func(self, *arg, **kwargs)
return wrapper
return decorator

View File

@@ -1,35 +0,0 @@
from .field import Field
from collections import defaultdict
import facho.fe.form as form
class Amount(Field):
"""
Amount representa un campo moneda usando form.Amount
"""
def __init__(self, name=None, default=None, precision=6):
self.field_name = name
self.values = {}
self.default = default
self.precision = precision
def __get__(self, model, cls):
if model is None:
return self
assert self.name is not None
self.__init_value(model)
model._set_field(self.name, self)
return self.values[model]
def __set__(self, model, value):
assert self.name is not None
self.__init_value(model)
model._set_field(self.name, self)
self.values[model] = form.Amount(value, precision=self.precision)
self._changed_field(model, self.name, value)
def __init_value(self, model):
if model not in self.values:
self.values[model] = form.Amount(self.default or 0)

View File

@@ -1,29 +0,0 @@
from .field import Field
class Attribute(Field):
"""
Attribute es un atributo del elemento actual.
"""
def __init__(self, name, default=None):
"""
:param name: nombre del atribute
:param default: valor por defecto del attributo
"""
self.attribute = name
self.value = default
self.default = default
def __get__(self, inst, cls):
if inst is None:
return self
assert self.name is not None
return self.value
def __set__(self, inst, value):
assert self.name is not None
self.value = value
self._changed_field(inst, self.name, value)
inst._set_attribute(self.name, self.attribute, value)

View File

@@ -1,60 +0,0 @@
import warnings
class Field:
def __set_name__(self, owner, name, virtual=False):
self.name = name
self.virtual = virtual
def __get__(self, inst, cls):
if inst is None:
return self
assert self.name is not None
return inst._fields[self.name]
def __set__(self, inst, value):
assert self.name is not None
inst._fields[self.name] = value
def _set_namespace(self, inst, name, namespaces):
if name is None:
return
#TODO(bit4bit) aunque las pruebas confirmar
#que si se escribe el namespace que es
#no ahi confirmacion de declaracion previa del namespace
inst._namespace_prefix = name
def _call(self, inst, method, *args):
call = getattr(inst, method or '', None)
if callable(call):
return call(*args)
def _create_model(self, inst, name=None, model=None, attribute=None, namespace=None):
try:
return inst._fields[self.name]
except KeyError:
if model is not None:
obj = model()
else:
obj = self.model()
if name is not None:
obj.__name__ = name
if namespace:
self._set_namespace(obj, namespace, inst.__namespace__)
else:
self._set_namespace(obj, self.namespace, inst.__namespace__)
if attribute:
inst._fields[attribute] = obj
else:
inst._fields[self.name] = obj
return obj
def _changed_field(self, inst, name, value):
for fun in inst._on_change_fields[name]:
fun(inst, name, value)

View File

@@ -1,36 +0,0 @@
from .field import Field
class Function(Field):
"""
Permite modificar el modelo cuando se intenta,
obtener el valor de este campo.
DEPRECATED usar Virtual
"""
def __init__(self, field, getter=None, default=None):
self.field = field
self.getter = getter
self.default = default
def __get__(self, inst, cls):
if inst is None:
return self
assert self.name is not None
# si se indica `field` se adiciona
# como campo del modelo, esto es
# que se serializa a xml
inst._set_field(self.name, self.field)
if self.getter is not None:
value = self._call(inst, self.getter, self.name, self.field)
if value is not None:
self.field.__set__(inst, value)
return self.field
def __set__(self, inst, value):
inst._set_field(self.name, self.field)
self._changed_field(inst, self.name, value)
self.field.__set__(inst, value)

View File

@@ -1,62 +0,0 @@
from .field import Field
from collections import defaultdict
class Many2One(Field):
"""
Many2One describe una relacion pertenece a.
"""
def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False, create=False):
"""
:param model: nombre del modelo destino
:param name: nombre del elemento xml
:param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3
:param namespace: sufijo del namespace al que pertenece el elemento
:param default: el valor o contenido por defecto
:param virtual: se crea la relacion por no se ve reflejada en el xml final
:param create: fuerza la creacion del elemento en el xml, ya que los elementos no son creados sino tienen contenido
"""
self.model = model
self.setter = setter
self.namespace = namespace
self.field_name = name
self.default = default
self.virtual = virtual
self.relations = defaultdict(dict)
self.create = create
def __get__(self, inst, cls):
if inst is None:
return self
assert self.name is not None
if self.name in self.relations:
value = self.relations[inst][self.name]
else:
value = self._create_model(inst, name=self.field_name)
self.relations[inst][self.name] = value
# se puede obtener directamente un valor indicado por el modelo
if hasattr(value, '__default_get__'):
return value.__default_get__(self.name, value)
elif hasattr(inst, '__default_get__'):
return inst.__default_get__(self.name, value)
else:
return value
def __set__(self, inst, value):
assert self.name is not None
inst_model = self._create_model(inst, name=self.field_name, model=self.model)
self.relations[inst][self.name] = inst_model
# si hay setter manual se ejecuta
# de lo contrario se asigna como texto del elemento
setter = getattr(inst, self.setter or '', None)
if callable(setter):
setter(inst_model, value)
else:
inst_model._set_content(value)
self._changed_field(inst, self.name, value)

View File

@@ -1,86 +0,0 @@
from .field import Field
from collections import defaultdict
# TODO(bit4bit) lograr que isinstance se aplique
# al objeto envuelto
class _RelationProxy():
def __init__(self, obj, inst, attribute):
self.__dict__['_obj'] = obj
self.__dict__['_inst'] = inst
self.__dict__['_attribute'] = attribute
def __getattr__(self, name):
if (name in self.__dict__):
return self.__dict__[name]
rel = getattr(self.__dict__['_obj'], name)
if hasattr(rel, '__default_get__'):
return rel.__default_get__(name, rel)
return rel
def __setattr__(self, attr, value):
# TODO(bit4bit) hacemos proxy al sistema de notificacion de cambios
# algo burdo, se usa __dict__ para saltarnos el __getattr__ y evitar un fallo por recursion
rel = getattr(self.__dict__['_obj'], attr)
if hasattr(rel, '__default_set__'):
response = setattr(self._obj, attr, rel.__default_set__(value))
else:
response = setattr(self._obj, attr, value)
for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]:
fun(self.__dict__['_inst'], self.__dict__['_attribute'], value)
return response
class _Relation():
def __init__(self, creator, inst, attribute):
self.creator = creator
self.inst = inst
self.attribute = attribute
self.relations = []
def create(self):
n_relations = len(self.relations)
attribute = '%s_%d' % (self.attribute, n_relations)
relation = self.creator(attribute)
proxy = _RelationProxy(relation, self.inst, self.attribute)
self.relations.append(relation)
return proxy
def __len__(self):
return len(self.relations)
def __iter__(self):
for relation in self.relations:
yield relation
class One2Many(Field):
"""
One2Many describe una relacion tiene muchos.
"""
def __init__(self, model, name=None, namespace=None, default=None):
"""
:param model: nombre del modelo destino
:param name: nombre del elemento xml cuando se crea hijo
:param namespace: sufijo del namespace al que pertenece el elemento
:param default: el valor o contenido por defecto
"""
self.model = model
self.field_name = name
self.namespace = namespace
self.default = default
self.relation = {}
def __get__(self, inst, cls):
assert self.name is not None
def creator(attribute):
return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute, namespace=self.namespace)
if inst in self.relation:
return self.relation[inst]
else:
self.relation[inst] = _Relation(creator, inst, self.name)
return self.relation[inst]

View File

@@ -1,54 +0,0 @@
from .field import Field
# Un campo virtual
# no participa del renderizado
# pero puede interactura con este
class Virtual(Field):
"""
Virtual es un campo que no es renderizado en el xml final
"""
def __init__(self,
setter=None,
getter='',
default=None,
update_internal=False):
"""
:param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3
:param getter: nombre del metodo usando cuando se obtiene, ejemplo: valor = mode.relation
:param default: valor por defecto
:param update_internal: indica que cuando se asigne algun valor este se almacena localmente
"""
self.default = default
self.setter = setter
self.getter = getter
self.values = {}
self.update_internal = update_internal
self.virtual = True
def __get__(self, inst, cls):
if inst is None:
return self
assert self.name is not None
value = self.default
try:
value = self.values[inst]
except KeyError:
pass
try:
self.values[inst] = getattr(inst, self.getter)(self.name, value)
except AttributeError:
self.values[inst] = value
return self.values[inst]
def __set__(self, inst, value):
if self.update_internal:
inst._value = value
if self.setter is None:
self.values[inst] = value
else:
self.values[inst] = self._call(inst, self.setter, self.name, value)
self._changed_field(inst, self.name, value)

1499
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

79
pyproject.toml Normal file
View File

@@ -0,0 +1,79 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "facho"
version = "0.1.0"
description = "Facturacion Electronica Colombia"
readme = "README.rst"
license = { text = "GPL-3.0-or-later" }
authors = [
{ name = "Jovany Leandro G.C", email = "bit4bit@riseup.net" },
]
keywords = ["facho"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
requires-python = ">=3.10"
dependencies = [
"Click>=8.1.7",
"zeep>=4.2.1",
"lxml>=5.2.2",
"cryptography>=41.0.0",
"pyOpenSSL>=23.2.0",
"xmlsig>=0.1.9",
"xades>=1.0.0",
"xmlsec>=1.3.12",
"python-dateutil>=2.9.0",
# usamos esta dependencia en runtime
# para forzar uso de policy_id de archivo local
"mock>=5.1.0",
"xmlschema>=3.0.0",
]
[project.scripts]
facho = "facho.cli:main"
[project.urls]
Repository = "https://github.com/bit4bit/facho"
[tool.poetry.group.dev.dependencies]
build = ">=1.2.2"
coverage = ">=7.8.0"
flake8 = ">=7.0.0"
pytest = ">=9.1.1,<10.0.0"
tox = ">=4.23.2"
[tool.setuptools]
packages = { find = { exclude = ["tests*"] } }
[tool.setuptools.package-data]
"*" = ["*.gc", "*.xsd", "politicadefirmav2.pdf"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.bumpversion]
current_version = "0.1.0"
commit = true
tag = true
[[tool.bumpversion.files]]
filename = "pyproject.toml"
search = 'version = "{current_version}"'
replace = 'version = "{new_version}"'
[[tool.bumpversion.files]]
filename = "facho/__init__.py"
search = "__version__ = '{current_version}'"
replace = "__version__ = '{new_version}'"

View File

@@ -1,17 +0,0 @@
attrs==22.1.0
distlib==0.3.6
filelock==3.8.0
iniconfig==1.1.1
packaging==21.3
platformdirs==2.5.2
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.9
pytest==7.1.3
semantic-version==2.10.0
setuptools-rust==1.5.2
six==1.16.0
tomli==2.0.1
tox==3.26.0
typing_extensions==4.4.0
virtualenv==20.16.5

View File

@@ -1,25 +0,0 @@
[bumpversion]
current_version = 0.1.0
commit = True
tag = True
[bumpversion:file:setup.py]
search = version='{current_version}'
replace = version='{new_version}'
[bumpversion:file:facho/__init__.py]
search = __version__ = '{current_version}'
replace = __version__ = '{new_version}'
[bdist_wheel]
universal = 1
[flake8]
exclude = docs
[aliases]
# Define setup.py command aliases here
test = pytest
[tool:pytest]
addopts = --ignore=setup.py

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"""The setup script."""
from setuptools import setup, find_packages
with open('README.rst') as readme_file:
readme = readme_file.read()
with open('HISTORY.rst') as history_file:
history = history_file.read()
requirements = ['Click>=8.1.7',
'zeep==4.2.1',
'lxml==5.2.2',
'cryptography==3.3.2',
'pyOpenSSL==20.0.1',
'xmlsig==0.1.7',
'xades==1.0.0',
'xmlsec==1.3.14',
'python-dateutil==2.9.0.post0',
# usamos esta dependencia en runtime
# para forzar uso de policy_id de archivo local
'mock>=5.1.0',
'xmlschema>=3.0.0']
"""
Listado de Versiones Anteriores
requirements = ['Click>=6.0',
'zeep==4.0.0',
'lxml==4.6.3',
'cryptography==3.3.2',
'pyOpenSSL==20.0.1',
'xmlsig==0.1.7',
'xades==0.2.2',
'xmlsec==1.3.12',
# usamos esta dependencia en runtime
# para forzar uso de policy_id de archivo local
'mock>=2.0.0',
'xmlschema>=1.8']
"""
setup_requirements = ['pytest-runner', ]
test_requirements = ['pytest', ]
setup(
author="Jovany Leandro G.C",
author_email='bit4bit@riseup.net',
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Natural Language :: English',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
],
description="Facturacion Electronica Colombia",
entry_points={
'console_scripts': [
'facho=facho.cli:main',
],
},
install_requires=requirements,
license="GNU General Public License v3",
long_description=readme + '\n\n' + history,
long_description_content_type='text/x-rst',
include_package_data=True,
package_data = {
# If any package contains *.txt or *.rst files, include them:
'': ['*.gc', '*.xsd', 'politicadefirmav2.pdf']
},
keywords='facho',
name='facho',
packages=find_packages(exclude=("tests",)),
setup_requires=setup_requirements,
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/bit4bit/facho',
<<<<<<< HEAD
version='0.2.0',
=======
version='0.2.1',
>>>>>>> morfo
zip_safe=False,
)

View File

@@ -2,6 +2,7 @@ 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(
@@ -36,14 +37,22 @@ 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(form.InvoiceDocumentReference('1234', 'xx', datetime.now())) inv = form.CreditNote(
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(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234')) inv.set_payment_mean(
form.PaymentMean(
form.PaymentMean.DEBIT,
'41',
datetime.now(),
'1234'))
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
name='facho-supplier', name='facho-supplier',
ident=form.PartyIdentification('123', '', '31'), ident=form.PartyIdentification('123', '', '31'),
@@ -68,6 +77,7 @@ 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()
@@ -75,7 +85,12 @@ 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(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234')) inv.set_payment_mean(
form.PaymentMean(
form.PaymentMean.DEBIT,
'41',
datetime.now(),
'1234'))
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
name='facho-supplier', name='facho-supplier',
ident=form.PartyIdentification('123', '', '31'), ident=form.PartyIdentification('123', '', '31'),
@@ -108,7 +123,12 @@ 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(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), ' 1234')) inv.set_payment_mean(
form.PaymentMean(
form.PaymentMean.DEBIT,
'41',
datetime.now(),
' 1234'))
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
name='facho-supplier', name='facho-supplier',
ident=form.PartyIdentification('123', '', '31'), ident=form.PartyIdentification('123', '', '31'),

View File

@@ -14,6 +14,7 @@ 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)
@@ -22,6 +23,7 @@ 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
@@ -30,16 +32,20 @@ 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 par Mantener el dígito menos significativo # 5, y el segundo dígito siguiente al dígito menos significativo es cero o
# par Mantener el dígito menos significativo
assert str(form.Amount(1.1542).round(2)) == str(form.Amount(1.15)) assert str(form.Amount(1.1542).round(2)) == str(form.Amount(1.15))
# 5, y el segundo dígito siguiente al dígito menos significativo es impar Incrementar el dígito menos significativo # 5, y el segundo dígito siguiente al dígito menos significativo es impar
# Incrementar el dígito menos significativo
assert str(form.Amount(1.1563).round(2)) == str(form.Amount(1.16)) 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'
@@ -49,5 +55,6 @@ 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

@@ -1,21 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import pytest
from facho.fe import form_xml
from fixtures import simple_invoice
simple_invoice = simple_invoice
def test_application_response(simple_invoice):
doc = form_xml.ApplicationResponse(simple_invoice)
xml = doc.toFachoXML()
with open("application_response.xml", "w") as fh:
fh.write(xml.tostring())
# raise Exception(xml.tostring())
# assert xml.get_element_text(
# './apr:ApplicationResponse')

View File

@@ -4,122 +4,13 @@
# 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
simple_invoice = simple_invoice # import helpers
def test_xml_with_required_elements(simple_invoice):
xml_header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' # def test_xml_with_required_elements():
DIANInvoiceXML = form_xml.DIANInvoiceXML( # doc = form_xml.AttachedDocument(id='123')
simple_invoice) # xml = doc.toFachoXML()
# 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,12 +4,7 @@
# 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,18 +20,19 @@ 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>'
invoice_line = xml.find_or_create_element('/root/Invoice/Line') xml.find_or_create_element('/root/Invoice/Line')
assert xml.tostring() == '<root><Invoice>Test<Line/></Invoice></root>' 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')
invoice = xml.find_or_create_element('/root/Invoice[id=123]') xml.find_or_create_element('/root/Invoice[id=123]')
assert xml.tostring() == '<root><Invoice id="123"/></root>' 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')
invoice = xml.find_or_create_element('/root/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')
@@ -46,6 +47,7 @@ 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')
@@ -54,31 +56,42 @@ 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('/root/ext:UBLExtensions/ext:UBLExtension/' invoiceAuthorization = xml.find_or_create_element(
'/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() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:UBLExtensions>'\ assert xml.tostring().strip() == (
'<ext:UBLExtension>'\ '<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
'<ext:ExtensionContent>'\ '<ext:UBLExtensions>'
'<sts:DianExtensions>'\ '<ext:UBLExtension>'
'<sts:InvoiceControl>'\ '<ext:ExtensionContent>'
'<sts:InvoiceAuthorization/>'\ '<sts:DianExtensions>'
'</sts:InvoiceControl></sts:DianExtensions></ext:ExtensionContent></ext:UBLExtension></ext:UBLExtensions></root>' '<sts:InvoiceControl>'
'<sts:InvoiceAuthorization/>'
'</sts:InvoiceControl></sts:DianExtensions>'
'</ext:ExtensionContent></ext:UBLExtension>'
'</ext:UBLExtensions></root>')
invoiceAuthorization.text = '123456789' invoiceAuthorization.text = '123456789'
assert xml.tostring().strip() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:UBLExtensions>'\ assert xml.tostring().strip() == (
'<ext:UBLExtension>'\ '<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
'<ext:ExtensionContent>'\ '<ext:UBLExtensions>'
'<sts:DianExtensions>'\ '<ext:UBLExtension>'
'<sts:InvoiceControl>'\ '<ext:ExtensionContent>'
'<sts:InvoiceAuthorization>123456789</sts:InvoiceAuthorization>'\ '<sts:DianExtensions>'
'</sts:InvoiceControl></sts:DianExtensions></ext:ExtensionContent></ext:UBLExtension></ext:UBLExtensions></root>' '<sts:InvoiceControl>'
'<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={
@@ -87,16 +100,22 @@ 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() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:Extension><sts:Sotoros/></ext:Extension></root>' assert xml.tostring() == (
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
'<ext:Extension><sts:Sotoros/></ext:Extension></root>')
xml.find_or_create_element('/root/ext:Extension/sts:Sotoros') xml.find_or_create_element('/root/ext:Extension/sts:Sotoros')
assert xml.tostring() == '<root xmlns:ext="https://ext" xmlns:sts="https://sts"><ext:Extension><sts:Sotoros/></ext:Extension></root>' assert xml.tostring() == (
'<root xmlns:ext="https://ext" xmlns:sts="https://sts">'
'<ext:Extension><sts:Sotoros/></ext:Extension></root>')
def test_facho_xml_set_element_with_format(): def test_facho_xml_set_element_with_format():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
invoice = xml.set_element('/root/Invoice', 1, format_='%02d') xml.set_element('/root/Invoice', 1, format_='%02d')
assert xml.tostring() == '<root><Invoice>01</Invoice></root>' 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')
@@ -116,7 +135,10 @@ 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() == '<Invoice><Line><Id>1</Id></Line><Line><Id>2</Id></Line><Line><Id>3</Id></Line></Invoice>' assert xml.tostring() == (
'<Invoice><Line><Id>1</Id></Line><Line><Id>2</Id></Line>'
'<Line><Id>3</Id></Line></Invoice>')
def test_facho_xml_nested_fragments(): def test_facho_xml_nested_fragments():
xml = facho.FachoXML('Invoice') xml = facho.FachoXML('Invoice')
@@ -128,7 +150,11 @@ def test_facho_xml_nested_fragments():
party.set_element('/Party/LastName', 'test') party.set_element('/Party/LastName', 'test')
assert xml.tostring() == '<Invoice><Party><Name>test</Name><Address><Line>line 1</Line></Address><LastName>test</LastName></Party></Invoice>' assert xml.tostring() == (
'<Invoice><Party><Name>test</Name>'
'<Address><Line>line 1</Line></Address>'
'<LastName>test</LastName></Party></Invoice>')
def test_facho_xml_get_element_text_of_fragment(): def test_facho_xml_get_element_text_of_fragment():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -137,6 +163,7 @@ 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')
@@ -147,6 +174,7 @@ 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')
@@ -166,25 +194,32 @@ 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('{%s}Invoice' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'), xml = facho.FachoXML(
nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'}) '{%s}Invoice' %
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
nsmap={
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
xml.set_element('./ID', 'ABC123') 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')
@@ -192,37 +227,47 @@ 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) as e: with pytest.raises(facho.FachoValueInvalid):
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) as e: with pytest.raises(facho.FachoValueInvalid):
xml.set_element_validator('./Id', lambda text, attrs: attrs['code'] == 'ABC') xml.set_element_validator(
'./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('./Id', lambda text, attrs: attrs['code'] == 'ABC') xml.set_element_validator(
'./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')
@@ -233,6 +278,7 @@ 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')
@@ -241,6 +287,7 @@ 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')
@@ -250,6 +297,7 @@ 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')
@@ -259,6 +307,7 @@ 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')
@@ -294,6 +343,7 @@ 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')
@@ -306,7 +356,9 @@ def test_facho_xml_placeholder_with_fragment():
AA = xml.fragment('./AA/Child', append=True) AA = xml.fragment('./AA/Child', append=True)
assert xml.tostring() == '<root><A/><AA><Child><B/><B/></Child><Child/></AA><AAA/></root>' assert xml.tostring() == (
'<root><A/><AA><Child><B/><B/></Child><Child/></AA><AAA/></root>')
def test_facho_xml_create_on_first_append(): def test_facho_xml_create_on_first_append():
xml = facho.FachoXML('root') xml = facho.FachoXML('root')
@@ -314,6 +366,7 @@ 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')
@@ -324,6 +377,7 @@ 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')
@@ -333,6 +387,7 @@ 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')
@@ -346,6 +401,7 @@ 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')
@@ -354,6 +410,7 @@ 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')
@@ -365,17 +422,19 @@ def test_facho_xml_placeholder_optional_and_fragment_with_set_element():
assert xml.tostring() == '<root><A><AA prueba="OK"/></A></root>' assert xml.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') == False assert xml.exist_element('/root/A') is 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') == True assert xml.exist_element('/root/A')
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')
@@ -384,6 +443,7 @@ def test_facho_xml_query_element_text_or_attribute():
assert xml.get_element_text_or_attribute('/root/A') == 'contenido' assert xml.get_element_text_or_attribute('/root/A') == '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')
@@ -392,6 +452,7 @@ def test_facho_xml_query_element_text_or_attribute_from_fragment():
assert invoice.get_element_text_or_attribute('/Invoice/A') == 'contenido' 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')
@@ -400,18 +461,23 @@ 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('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'), xml = facho.FachoXML(
nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'}) '{%s}root' %
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
nsmap={
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
xpath = xml.xpath_from_root('/A') 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('{%s}root' % ('http://www.dian.gov.co/contratos/facturaelectronica/v1'), xml = facho.FachoXML(
nsmap={'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'}) '{%s}root' %
('http://www.dian.gov.co/contratos/facturaelectronica/v1'),
nsmap={
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1'})
invoice = xml.fragment('/root/Invoice') 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,16 +4,14 @@
# 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 -newkey rsa:2048 -keyout example.key -out example.pem # openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1
# openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem # -newkey rsa:2048 -keyout example.key -out example.pem
# openssl pkcs12 -export -out example.p12 -inkey example.key -in
# example.pem
signer = fe.DianXMLExtensionSigner('./tests/example.p12') signer = fe.DianXMLExtensionSigner('./tests/example.p12')
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
@@ -21,68 +19,90 @@ def test_xmlsigned_build(monkeypatch):
signer.sign_xml_element(xml.root) signer.sign_xml_element(xml.root)
elem = xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature') elem = xml.find_or_create_element(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/ds:Signature')
assert elem is not None assert elem 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 -newkey rsa:2048 -keyout example.key -out example.pem # openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1
#openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem # -newkey rsa:2048 -keyout example.key -out example.pem
signer = fe.DianXMLExtensionSigner('./tests/example-with-passphrase.p12', 'test') # openssl pkcs12 -export -out example.p12 -inkey example.key -in
# example.pem
signer = fe.DianXMLExtensionSigner(
'./tests/example-with-passphrase.p12', 'test')
xml = fe.FeXML('Invoice', 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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature') elem = xml.find_or_create_element(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/ds:Signature')
assert elem is not None assert elem 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('idsoftware', '1234', '1') security_code = fe.DianXMLExtensionSoftwareSecurityCode(
'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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode') content = xml.get_element_text(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/sts:DianExtensions/sts:SoftwareSecurityCode')
assert content is not None 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(invoice_authorization, inv_auth_ext = fe.DianXMLExtensionInvoiceAuthorization(
datetime(2017, 2, 23), invoice_authorization, datetime(
datetime(2019, 8, 23), 2017, 2, 23), datetime(
'MD', 100001, 174999) 2019, 8, 23), '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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl/sts:InvoiceAuthorization') auth = xml.get_element_text(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/sts:DianExtensions/sts:InvoiceControl/sts:InvoiceAuthorization')
assert auth == invoice_authorization 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(nit, '', id_software) software_provider_extension = fe.DianXMLExtensionSoftwareProvider(
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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider/sts:ProviderID') give_nit = xml.get_element_text(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent'
'/sts:DianExtensions/sts:SoftwareProvider/sts:ProviderID')
assert nit == give_nit 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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') dian_nit = xml.get_element_text(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/'
'ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/'
'sts:AuthorizationProviderID')
assert dian_nit == '800197268' 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')
@@ -93,9 +113,13 @@ 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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') xml.find_or_create_element(
ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True) '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') ublextension = xml.fragment(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension',
append=True)
ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
xmlstring = xml.tostring() xmlstring = xml.tostring()
print(xmlstring) print(xmlstring)
@@ -103,12 +127,17 @@ 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('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') xml.find_or_create_element(
ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True) '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') ublextension = xml.fragment(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension',
append=True)
ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
xmlstring = xml.tostring() xmlstring = xml.tostring()
p12_data = open('./tests/example.p12', 'rb').read() p12_data = open('./tests/example.p12', 'rb').read()
@@ -117,6 +146,7 @@ def test_xml_sign_dian_using_bytes(monkeypatch):
xmlsigned = signer.sign_xml_string(xmlstring) 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',
@@ -130,3 +160,6 @@ 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,11 +41,13 @@ 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:Name') '/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc'
':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:Name') '/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc'
':Name')
assert customer_name == simple_invoice.invoice_customer.name assert customer_name == simple_invoice.invoice_customer.name
@@ -66,7 +68,8 @@ def test_invoicesimple_xml_signed(monkeypatch, simple_invoice):
xml.add_extension(signer) xml.add_extension(signer)
elem = xml.get_element( elem = xml.get_element(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature') '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext'
':ExtensionContent/ds:Signature')
assert elem.text is not None assert elem.text is not None
@@ -75,14 +78,16 @@ 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(simple_invoice.invoice_ident, str(xml_invoice)) name_invoice = dianzip.add_invoice_xml(
simple_invoice.invoice_ident, str(xml_invoice))
# el zip ademas de archivar debe comprimir los archivos # 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, "se espera el zip comprimido" assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, (
"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')
@@ -122,7 +127,8 @@ 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('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') simple_invoice.invoice_issue = datetime.strptime(
'2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_supplier.ident = '700085371' simple_invoice.invoice_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(
@@ -222,7 +228,8 @@ def test_invoice_cufe(simple_invoice_without_lines):
assert formatVars[12] == '800199436', "NumAdq" assert formatVars[12] == '800199436', "NumAdq"
# ClTec # ClTec
assert formatVars[13] == '693ff6f2a553c3646a063436fd4dd9ded0311471', "ClTec" assert formatVars[13] == (
'693ff6f2a553c3646a063436fd4dd9ded0311471'), "ClTec"
# TipoAmbiente # TipoAmbiente
assert formatVars[14] == '1', "TipoAmbiente" assert formatVars[14] == '1', "TipoAmbiente"
@@ -323,8 +330,13 @@ def test_debit_note_cude(simple_debit_note_without_lines):
assert build_vars['TipoAmb'] == 2 assert build_vars['TipoAmb'] == 2
cude_composicion = "".join(cude_extension.formatVars()) cude_composicion = "".join(cude_extension.formatVars())
assert cude_composicion == 'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.0090019726410254102102012' assert cude_composicion == (
'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.009'
'001'
'9726410254102102012')
xml_invoice.add_extension(cude_extension) 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 == '3fa73a86d57d9341c536afde1f85c4efd9d4591c2c22bce4dfb0e6b0d2e83b8f047a8bde7098292e9d2493e60d1c31da' assert cude == (
'3fa73a86d57d9341c536afde1f85c4efd9d4591c2c22bce4dfb0e6b0d2e83b8f047a8'
'bde7098292e9d2493e60d1c31da')

View File

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

View File

@@ -6,10 +6,7 @@
"""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 *

View File

@@ -1,601 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"""Tests for `facho` package."""
import pytest
import facho.model
import facho.model.fields as fields
def test_model_to_element():
class Person(facho.model.Model):
__name__ = 'Person'
person = Person()
assert "<Person/>" == person.to_xml()
def test_model_to_element_with_attribute():
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Attribute('id')
person = Person()
person.id = 33
personb = Person()
personb.id = 44
assert "<Person id=\"33\"/>" == person.to_xml()
assert "<Person id=\"44\"/>" == personb.to_xml()
def test_model_to_element_with_attribute_as_element():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID)
person = Person()
person.id = 33
assert "<Person><ID>33</ID></Person>" == person.to_xml()
def test_many2one_with_custom_attributes():
class TaxAmount(facho.model.Model):
__name__ = 'TaxAmount'
currencyID = fields.Attribute('currencyID')
class TaxTotal(facho.model.Model):
__name__ = 'TaxTotal'
amount = fields.Many2One(TaxAmount)
tax_total = TaxTotal()
tax_total.amount = 3333
tax_total.amount.currencyID = 'COP'
assert '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml()
def test_many2one_with_custom_setter():
class PhysicalLocation(facho.model.Model):
__name__ = 'PhysicalLocation'
id = fields.Attribute('ID')
class Party(facho.model.Model):
__name__ = 'Party'
location = fields.Many2One(PhysicalLocation, setter='location_setter')
def location_setter(self, field, value):
field.id = value
party = Party()
party.location = 99
assert '<Party><PhysicalLocation ID="99"/></Party>' == party.to_xml()
def test_many2one_always_create():
class Name(facho.model.Model):
__name__ = 'Name'
class Person(facho.model.Model):
__name__ = 'Person'
name = fields.Many2One(Name, default='facho')
person = Person()
assert '<Person><Name>facho</Name></Person>' == person.to_xml()
def test_many2one_nested_always_create():
class Name(facho.model.Model):
__name__ = 'Name'
class Contact(facho.model.Model):
__name__ = 'Contact'
name = fields.Many2One(Name, default='facho')
class Person(facho.model.Model):
__name__ = 'Person'
contact = fields.Many2One(Contact, create=True)
person = Person()
assert '<Person><Contact><Name>facho</Name></Contact></Person>' == person.to_xml()
def test_many2one_auto_create():
class TaxAmount(facho.model.Model):
__name__ = 'TaxAmount'
currencyID = fields.Attribute('currencyID')
class TaxTotal(facho.model.Model):
__name__ = 'TaxTotal'
amount = fields.Many2One(TaxAmount)
tax_total = TaxTotal()
tax_total.amount.currencyID = 'COP'
tax_total.amount = 3333
assert '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml()
def test_field_model():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID)
person = Person()
person.id = ID()
person.id = 33
assert "<Person><ID>33</ID></Person>" == person.to_xml()
def test_field_multiple_model():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID)
id2 = fields.Many2One(ID)
person = Person()
person.id = 33
person.id2 = 44
assert "<Person><ID>33</ID><ID>44</ID></Person>" == person.to_xml()
def test_field_model_failed_initialization():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID)
person = Person()
person.id = 33
assert "<Person><ID>33</ID></Person>" == person.to_xml()
def test_field_model_with_custom_name():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID, name='DID')
person = Person()
person.id = 33
assert "<Person><DID>33</DID></Person>" == person.to_xml()
def test_field_model_default_initialization_with_attributes():
class ID(facho.model.Model):
__name__ = 'ID'
reference = fields.Attribute('REFERENCE')
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID)
person = Person()
person.id = 33
person.id.reference = 'haber'
assert '<Person><ID REFERENCE="haber">33</ID></Person>' == person.to_xml()
def test_model_with_xml_namespace():
class Person(facho.model.Model):
__name__ = 'Person'
__namespace__ = {
'facho': 'http://lib.facho.cyou'
}
person = Person()
assert '<Person xmlns:facho="http://lib.facho.cyou"/>'
def test_model_with_xml_namespace_nested():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
__namespace__ = {
'facho': 'http://lib.facho.cyou'
}
id = fields.Many2One(ID, namespace='facho')
person = Person()
person.id = 33
assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == person.to_xml()
def test_model_with_xml_namespace_nested_nested():
class ID(facho.model.Model):
__name__ = 'ID'
class Party(facho.model.Model):
__name__ = 'Party'
id = fields.Many2One(ID, namespace='party')
def __default_set__(self, value):
self.id = value
class Person(facho.model.Model):
__name__ = 'Person'
__namespace__ = {
'person': 'http://lib.facho.cyou',
'party': 'http://lib.facho.cyou'
}
id = fields.Many2One(Party, namespace='person')
person = Person()
person.id = 33
assert '<Person xmlns:person="http://lib.facho.cyou" xmlns:party="http://lib.facho.cyou"><person:Party><party:ID>33</party:ID></person:Party></Person>' == person.to_xml()
def test_model_with_xml_namespace_nested_one_many():
class Name(facho.model.Model):
__name__ = 'Name'
class Contact(facho.model.Model):
__name__ = 'Contact'
name = fields.Many2One(Name, namespace='contact')
class Person(facho.model.Model):
__name__ = 'Person'
__namespace__ = {
'facho': 'http://lib.facho.cyou',
'contact': 'http://lib.facho.cyou'
}
contacts = fields.One2Many(Contact, namespace='facho')
person = Person()
contact = person.contacts.create()
contact.name = 'contact1'
contact = person.contacts.create()
contact.name = 'contact2'
assert '<Person xmlns:facho="http://lib.facho.cyou" xmlns:contact="http://lib.facho.cyou"><facho:Contact><contact:Name>contact1</contact:Name></facho:Contact><facho:Contact><contact:Name>contact2</contact:Name></facho:Contact></Person>' == person.to_xml()
def test_field_model_with_namespace():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
__namespace__ = {
"facho": "http://lib.facho.cyou"
}
id = fields.Many2One(ID, namespace="facho")
person = Person()
person.id = 33
assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == person.to_xml()
def test_field_hook_before_xml():
class Hash(facho.model.Model):
__name__ = 'Hash'
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Many2One(Hash)
def __before_xml__(self):
self.hash = "calculate"
person = Person()
assert "<Person><Hash>calculate</Hash></Person>" == person.to_xml()
def test_field_function_with_attribute():
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Function(fields.Attribute('hash'), getter='get_hash')
def get_hash(self, name, field):
return 'calculate'
person = Person()
assert '<Person hash="calculate"/>'
def test_field_function_with_model():
class Hash(facho.model.Model):
__name__ = 'Hash'
id = fields.Attribute('id')
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Function(fields.Many2One(Hash), getter='get_hash')
def get_hash(self, name, field):
field.id = 'calculate'
person = Person()
assert person.hash.id == 'calculate'
assert '<Person/>'
def test_field_function_setter():
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Attribute('hash')
password = fields.Virtual(setter='set_hash')
def set_hash(self, name, value):
self.hash = "%s+2" % (value)
person = Person()
person.password = 'calculate'
assert '<Person hash="calculate+2"/>' == person.to_xml()
def test_field_function_only_setter():
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Attribute('hash')
password = fields.Virtual(setter='set_hash')
def set_hash(self, name, value):
self.hash = "%s+2" % (value)
person = Person()
person.password = 'calculate'
assert '<Person hash="calculate+2"/>' == person.to_xml()
def test_model_set_default_setter():
class Hash(facho.model.Model):
__name__ = 'Hash'
def __default_set__(self, value):
return "%s+3" % (value)
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Many2One(Hash)
person = Person()
person.hash = 'hola'
assert '<Person><Hash>hola+3</Hash></Person>' == person.to_xml()
def test_field_virtual():
class Person(facho.model.Model):
__name__ = 'Person'
age = fields.Virtual()
person = Person()
person.age = 55
assert person.age == 55
assert "<Person/>" == person.to_xml()
def test_field_inserted_default_attribute():
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Attribute('hash', default='calculate')
person = Person()
assert '<Person hash="calculate"/>' == person.to_xml()
def test_field_function_inserted_default_attribute():
class Person(facho.model.Model):
__name__ = 'Person'
hash = fields.Function(fields.Attribute('hash'), default='calculate')
person = Person()
assert '<Person hash="calculate"/>' == person.to_xml()
def test_field_inserted_default_many2one():
class ID(facho.model.Model):
__name__ = 'ID'
key = fields.Attribute('key')
def __default_set__(self, value):
self.key = value
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID, default="oe")
person = Person()
assert '<Person><ID key="oe"/></Person>' == person.to_xml()
def test_field_inserted_default_nested_many2one():
class ID(facho.model.Model):
__name__ = 'ID'
class Person(facho.model.Model):
__name__ = 'Person'
id = fields.Many2One(ID, default="ole")
person = Person()
assert '<Person><ID>ole</ID></Person>' == person.to_xml()
def test_model_on_change_field():
class Hash(facho.model.Model):
__name__ = 'Hash'
class Person(facho.model.Model):
__name__ = 'Person'
react = fields.Attribute('react')
hash = fields.Many2One(Hash)
@fields.on_change(['hash'])
def on_change_react(self, name, value):
assert name == 'hash'
self.react = "%s+4" % (value)
person = Person()
person.hash = 'hola'
assert '<Person react="hola+4"><Hash>hola</Hash></Person>' == person.to_xml()
def test_model_on_change_field_attribute():
class Person(facho.model.Model):
__name__ = 'Person'
react = fields.Attribute('react')
hash = fields.Attribute('Hash')
@fields.on_change(['hash'])
def on_react(self, name, value):
assert name == 'hash'
self.react = "%s+4" % (value)
person = Person()
person.hash = 'hola'
assert '<Person react="hola+4" Hash="hola"/>' == person.to_xml()
def test_model_one2many():
class Line(facho.model.Model):
__name__ = 'Line'
quantity = fields.Attribute('quantity')
class Invoice(facho.model.Model):
__name__ = 'Invoice'
lines = fields.One2Many(Line)
invoice = Invoice()
line = invoice.lines.create()
line.quantity = 3
line = invoice.lines.create()
line.quantity = 5
assert '<Invoice><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml()
def test_model_one2many_with_on_changes():
class Line(facho.model.Model):
__name__ = 'Line'
quantity = fields.Attribute('quantity')
class Invoice(facho.model.Model):
__name__ = 'Invoice'
lines = fields.One2Many(Line)
count = fields.Attribute('count', default=0)
@fields.on_change(['lines'])
def refresh_count(self, name, value):
self.count = len(self.lines)
invoice = Invoice()
line = invoice.lines.create()
line.quantity = 3
line = invoice.lines.create()
line.quantity = 5
assert len(invoice.lines) == 2
assert '<Invoice count="2"><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml()
def test_model_one2many_as_list():
class Line(facho.model.Model):
__name__ = 'Line'
quantity = fields.Attribute('quantity')
class Invoice(facho.model.Model):
__name__ = 'Invoice'
lines = fields.One2Many(Line)
invoice = Invoice()
line = invoice.lines.create()
line.quantity = 3
line = invoice.lines.create()
line.quantity = 5
lines = list(invoice.lines)
assert len(list(invoice.lines)) == 2
for line in lines:
assert isinstance(line, Line)
assert '<Invoice><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml()
def test_model_attributes_order():
class Line(facho.model.Model):
__name__ = 'Line'
quantity = fields.Attribute('quantity')
class Invoice(facho.model.Model):
__name__ = 'Invoice'
line1 = fields.Many2One(Line, name='Line1')
line2 = fields.Many2One(Line, name='Line2')
line3 = fields.Many2One(Line, name='Line3')
invoice = Invoice()
invoice.line2.quantity = 2
invoice.line3.quantity = 3
invoice.line1.quantity = 1
assert '<Invoice><Line1 quantity="1"/><Line2 quantity="2"/><Line3 quantity="3"/></Invoice>' == invoice.to_xml()
def test_field_amount():
class Line(facho.model.Model):
__name__ = 'Line'
amount = fields.Amount(name='Amount', precision=1)
amount_as_attribute = fields.Attribute('amount')
@fields.on_change(['amount'])
def on_amount(self, name, value):
self.amount_as_attribute = self.amount
line = Line()
line.amount = 33
assert '<Line amount="33.0"/>' == line.to_xml()
def test_model_setup():
class Line(facho.model.Model):
__name__ = 'Line'
amount = fields.Attribute(name='amount')
def __setup__(self):
self.amount = 23
line = Line()
assert '<Line amount="23"/>' == line.to_xml()

View File

@@ -1,119 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"""Nuevo esquema para modelar segun decreto"""
from datetime import datetime
import pytest
from lxml import etree
import facho.fe.model as model
import facho.fe.form as form
from facho import fe
import helpers
def simple_invoice():
invoice = model.Invoice()
invoice.dian.software_security_code = '12345'
invoice.dian.software_provider.provider_id = 'provider-id'
invoice.dian.software_provider.software_id = 'facho'
invoice.dian.control.prefix = 'SETP'
invoice.dian.control.from_range = '1000'
invoice.dian.control.to_range = '1000'
invoice.id = '323200000129'
invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
invoice.supplier.party.id = '700085371'
invoice.customer.party.id = '800199436'
line = invoice.lines.create()
line.add_tax(model.Taxes.Iva(19.0))
# TODO(bit4bit) acoplamiento temporal
# se debe crear primero el subotatl
# para poder calcularse al cambiar el precio
line.quantity = 1
line.price = 1_500_000
return invoice
def test_simple_invoice_cufe():
token = '693ff6f2a553c3646a063436fd4dd9ded0311471'
environment = fe.AMBIENTE_PRODUCCION
invoice = simple_invoice()
assert invoice.cufe(token, environment) == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4'
def test_simple_invoice_sign_dian(monkeypatch):
invoice = simple_invoice()
xmlstring = invoice.to_xml()
p12_data = open('./tests/example.p12', 'rb').read()
signer = fe.DianXMLExtensionSigner.from_bytes(p12_data)
with monkeypatch.context() as m:
helpers.mock_urlopen(m)
xmlsigned = signer.sign_xml_string(xmlstring)
assert "Signature" in xmlsigned
def test_dian_extension_authorization_provider():
invoice = simple_invoice()
xml = fe.FeXML.from_string(invoice.to_xml())
provider_id = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID')
assert provider_id.attrib['schemeID'] == '4'
assert provider_id.attrib['schemeName'] == '31'
assert provider_id.attrib['schemeAgencyName'] == 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)'
assert provider_id.attrib['schemeAgencyID'] == '195'
assert provider_id.text == '800197268'
def test_invoicesimple_xml_signed_using_fexml(monkeypatch):
invoice = simple_invoice()
xml = fe.FeXML.from_string(invoice.to_xml())
signer = fe.DianXMLExtensionSigner('./tests/example.p12')
print(xml.tostring())
with monkeypatch.context() as m:
import helpers
helpers.mock_urlopen(m)
xml.add_extension(signer)
elem = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature')
assert elem.text is not None
def test_invoice_supplier_party():
invoice = simple_invoice()
invoice.supplier.party.name = 'superfacho'
invoice.supplier.party.tax_scheme.registration_name = 'legal-superfacho'
invoice.supplier.party.contact.email = 'superfacho@etrivial.net'
xml = fe.FeXML.from_string(invoice.to_xml())
name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name')
assert name.text == 'superfacho'
name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName')
assert name.text == 'legal-superfacho'
name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicEmail')
assert name.text == 'superfacho@etrivial.net'
def test_invoice_customer_party():
invoice = simple_invoice()
invoice.customer.party.name = 'superfacho-customer'
invoice.customer.party.tax_scheme.registration_name = 'legal-superfacho-customer'
invoice.customer.party.contact.email = 'superfacho@etrivial.net'
xml = fe.FeXML.from_string(invoice.to_xml())
name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name')
assert name.text == 'superfacho-customer'
name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName')
assert name.text == 'legal-superfacho-customer'
name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicEmail')
assert name.text == 'superfacho@etrivial.net'

View File

@@ -28,8 +28,12 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30' # assert
# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00' # xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico',
# 'DiasTrabajados') == '30'
# assert
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico',
# 'SueldoTrabajado') == '1000000.00'
# def test_adicionar_devengado_transporte(): # def test_adicionar_devengado_transporte():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -40,7 +44,9 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte', 'AuxilioTransporte') == '2000000.0' # assert
# xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte',
# 'AuxilioTransporte') == '2000000.0'
# def test_adicionar_devengado_comprobante_total(): # def test_adicionar_devengado_comprobante_total():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -58,7 +64,9 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '1000000.00' # assert
# xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') ==
# '1000000.00'
# def test_adicionar_devengado_comprobante_total_cero(): # def test_adicionar_devengado_comprobante_total_cero():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -75,7 +83,8 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00' # assert
# xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00'
# def test_adicionar_devengado_transporte_muchos(): # def test_adicionar_devengado_transporte_muchos():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -90,7 +99,8 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml) # print(xml)
# assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal') == '5000000.00' # assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal')
# == '5000000.00'
# def test_adicionar_deduccion_salud(): # def test_adicionar_deduccion_salud():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -107,7 +117,9 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml) # print(xml)
# assert xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') == '1000.00' # assert
# xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') ==
# '1000.00'
# def test_nomina_obligatorios_segun_anexo_tecnico(): # def test_nomina_obligatorios_segun_anexo_tecnico():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -213,26 +225,62 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab' # expected_cune =
# assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune # 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484
# assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102' # b5da15ff585e773b0b0ab'
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001' # assert
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001' # xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral',
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO' # 'CUNE') == expected_cune
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05' # assert
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001' # xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral',
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999' # 'TipoXML') == '102'
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2' # assert
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx' # xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaX
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None # ML/@Numero') == 'N00001'
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') == f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_cune}" # assert
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT') == '700085371' # xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaX
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436' # ML/@Consecutivo') == '00001'
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True' # assert
# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov') == 'N0111' # xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX
# ML/@Pais') == 'CO'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX
# ML/@DepartamentoEstado') == '05'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionX
# ML/@MunicipioCiudad') == '05001'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NI
# T') == '999999'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV
# ') == '2'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@So
# ftwareID') == 'xx'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@So
# ftwareSC') is not None
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') ==
# f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_
# cune}"
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT')
# == '700085371'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@Nume
# roDocumento') == '800199436'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') ==
# 'True'
# assert
# xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov
# ') == 'N0111'
# # confirmar el namespace # # confirmar el namespace
# assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in xml.tostring() # assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in
# xml.tostring()
# def test_asignar_pago(): # def test_asignar_pago():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
@@ -249,11 +297,11 @@
# xml.add_extension(signer) # xml.add_extension(signer)
# print(xml.tostring()) # print(xml.tostring())
# elem = xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature') # elem =
# xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension
# /ext:ExtensionContent/ds:Signature')
# assert elem is not None # 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()
@@ -277,7 +325,9 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN', multiple=True) # extras =
# xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('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'
@@ -312,7 +362,9 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN', multiple=True) # extras =
# xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('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'
@@ -327,7 +379,9 @@
# 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',
@@ -347,7 +401,9 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF', multiple=True) # extras =
# xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('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,7 +418,9 @@
# 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',
@@ -382,7 +440,9 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF', multiple=True) # extras =
# xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('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'
@@ -398,7 +458,9 @@
# 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',
@@ -418,7 +480,9 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF', multiple=True) # extras =
# xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('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'
@@ -433,7 +497,9 @@
# 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',
@@ -453,7 +519,9 @@
# )) # ))
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF', multiple=True) # extras =
# xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF',
# multiple=True)
# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # assert extras[0].get('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,7 +101,9 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/InformacionGeneral', 'TipoXML') == '103' # assert
# xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar
# /InformacionGeneral', 'TipoXML') == '103'
# def test_adicionar_reemplazar_devengado_comprobante_total(): # def test_adicionar_reemplazar_devengado_comprobante_total():
@@ -119,13 +121,17 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ComprobanteTotal') == '1000000.00' # assert
# xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Comp
# robanteTotal') == '1000000.00'
# def test_adicionar_reemplazar_asignar_predecesor(): # 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'
@@ -133,15 +139,23 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@NumeroPred') == '123456' # assert
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456' # xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16' # emplazar/ReemplazandoPredecesor/@NumeroPred') == '123456'
# assert
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re
# emplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456'
# assert
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Re
# emplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16'
# def test_adicionar_reemplazar_eliminar_predecesor_opcional(): # 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'
@@ -150,13 +164,19 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None # assert
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is None # xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Reemplaza
# ndoPredecesor') is not None
# assert
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoP
# redecesor') is None
# def test_adicionar_eliminar_reemplazar_predecesor_opcional(): # 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'
@@ -164,8 +184,12 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None # assert
# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is None # xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoP
# redecesor') is not None
# assert
# xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/Reemplaza
# ndoPredecesor') is None
# def test_adicionar_eliminar_devengado_comprobante_total(): # def test_adicionar_eliminar_devengado_comprobante_total():
# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar()
@@ -182,12 +206,16 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/ComprobanteTotal') == '1000000.00' # assert
# xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/Compro
# banteTotal') == '1000000.00'
# def test_adicionar_eliminar_asignar_predecesor(): # 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'
@@ -195,9 +223,15 @@
# xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
# print(xml.tostring()) # print(xml.tostring())
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@NumeroPred') == '123456' # assert
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456' # xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El
# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16' # iminar/EliminandoPredecesor/@NumeroPred') == '123456'
# assert
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El
# iminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456'
# assert
# xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/El
# iminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16'
# def test_nomina_devengado_horas_extras_diarias(): # def test_nomina_devengado_horas_extras_diarias():
# nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()

22
tox.ini
View File

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