71 Commits

Author SHA1 Message Date
4eaf0c052f Merge branch 'morfo' 2025-12-04 15:48:00 -05:00
c49e67b8a6 Fix: Test incluir ApplicationResponse 2025-01-01 18:40:05 -05:00
7672ae4b7f Fix: Delete escribir archivo para ver salida 2025-01-01 18:39:22 -05:00
25c5fda3f0 Fix: Delete archivo para ver salida 2025-01-01 18:37:09 -05:00
a758b8678b Feat: Se agrega ApplicationResponse a AttachedDocument 2025-01-01 16:56:15 -05:00
3a385c63e3 Feat: Attachment, ParentDocumentLine 2024-12-30 19:00:51 -05:00
9b33b4486c Fix: Se elimina simple_invoice arg 2024-12-30 11:01:22 -05:00
612aae1f86 Fix: Se reemplazan datos de prueba por objeto Invoice issue #4 2024-12-27 21:04:35 -05:00
4fe82daac6 AttachedDocument(WIP): Se escribe envoltorio AttachedDocument issue #4 2024-12-27 18:54:56 -05:00
e237d1b45f Fix: Se descomenta test AttachedDocument 2024-12-27 00:10:03 -05:00
8cc6146be2 Merge remote-tracking branch 'origin/documento_soporte' 2024-12-26 23:20:16 -05:00
f98ab98c9c Fix: Se agrega dateutil 2024-08-28 16:48:41 -05:00
d04596ed3a Fix: SigningTime tz='America/Bogota' 2024-08-28 16:35:34 -05:00
9297be22e0 Fix: UPDATE Dependencies 2024-08-28 13:10:49 -05:00
cosmos
30773e042b Add test signartime - timezone America/Bogota 2024-08-23 11:03:38 -05:00
fb44498e53 Fix: Formateo PEP8 2024-08-14 12:50:53 -05:00
9f7349ccee file(requirements_dev): Pruebas Tox 2024-08-07 00:29:48 -05:00
9cc41e1c5d files: cude.txt, cufe.txt 2024-08-07 00:17:58 -05:00
fc75126ca0 FIX: Formateo PEP8, Test OK 2024-08-07 00:17:16 -05:00
d061077b30 fix(WIP): Reemplazando OpenSsl.crypto,pkcs12 2024-08-06 16:41:14 -05:00
a3d2176068 update(test_data): Anexo 1.9 2024-08-06 15:29:47 -05:00
98677bc162 update(TipoDocumento): Anexo 1.9 2024 2024-08-06 15:25:46 -05:00
398d27d049 Fix: Se importan fixtures 2024-08-06 15:21:26 -05:00
8765a3d2c8 Fix(form_xml/invoice): Se agrega defaultdict 2024-08-06 14:38:36 -05:00
1935ed3048 style(test_form.py): Formateo PEP8 se agrega withholding 2024-08-06 14:33:21 -05:00
097cf97fc3 style(test_form_xml): Formateado PEP8 2024-08-06 14:09:52 -05:00
de99633211 Fix: Formateado PEP8 se elimina responsabilidad O-07 segun anexo 1.9 2024-08-06 14:02:40 -05:00
Rodia
028cf8b687 Merge pull request 'FachoStyle' (#3) from FachoStyle into MigrationPython312Test
Reviewed-on: #3
2024-08-06 13:35:10 -05:00
e7a3976b14 style(test_query): Formateado PEP8 2024-08-06 13:30:34 -05:00
f08954ee43 style(__init__): Formateado PEP8 2024-08-06 13:28:59 -05:00
a0321020c7 Fix(TipoResponsabilidad-2.1.gc): UPDATE Anexo 1.9 2024 2024-08-06 13:27:48 -05:00
dde24b9739 Fix: Dependencies 2024-06-12 10:06:59 -05:00
6e5d358c73 Fix: Se actualizan dependencias para python3.12 y primeros pasos con pruebas 2024-06-11 15:59:44 -05:00
bit4bit
48d1ba3735 Create new branch named "morfo"
FossilOrigin-Name: 49206404a73c744c29382b1a54ea35227b248c483f44dd09a77d7a7fbdefa691
2021-10-06 02:10:01 +00:00
bit4bit
2e130d39e6 se adicionan mas campos a Invoice.Party
FossilOrigin-Name: 033c6a3c0297c83c1d0bf2bb246f55fa7ce0d8aaa752f2ae3ec9515c763d61bf
2021-10-06 01:08:01 +00:00
bit4bit
6716efd121 se actualiza a 0.2.1
FossilOrigin-Name: 65ab09bc6e7823c31d3b201825edc868c46e92ecca963a2c6a0bb9aec9957661
2021-08-09 01:47:30 +00:00
bit4bit
302812328e se adicionan comentarios
FossilOrigin-Name: 7137f93d5b3081e2d8f3d1245c97f4aa109c2c671744eef5a4abc0f7551183d0
2021-08-09 01:16:44 +00:00
bit4bit
3a89c6d3e5 se adicionan comentarios
FossilOrigin-Name: 13a8728e99e2a715d68bee47a54adcfef5a1f0043abfdfe6aabb3afa99ca9f7f
2021-08-09 01:15:51 +00:00
bit4bit
3a349a746e se crea parcialmente Address, Country
FossilOrigin-Name: 9db11d9b88e2e0065960ac07efe88e8dd8d87706bda88074845f6bfcfecc737b
2021-08-09 00:29:37 +00:00
bit4bit
efe93ecc3c mas pruebas y algunos pequenos cambios
FossilOrigin-Name: 02bc90719bb17216a749568086887a7d27878fdbd81febfb88a9fb1e68aa8205
2021-08-09 00:21:03 +00:00
bit4bit
64b312a432 se adiciona extensions para la dian
FossilOrigin-Name: f5521ddbfb903915de88a26ba5197b67efa1ebfd66337061ee9e3653c59dd217
2021-08-08 22:00:08 +00:00
bit4bit
088fa9e6e0 se adiciona namespaces a invoice
FossilOrigin-Name: caf85d4a30e7945ca2a6fc3a9430855fa8623442b1a30ae824f742e2a93e2956
2021-08-08 21:20:54 +00:00
bit4bit
ddee0e45c1 el uso de __default_set__ y __default_get__ facilitan la creacion de un objeto como tipo
FossilOrigin-Name: e7fa7e3a3b312ad6b88a85b508b496308e5bb54b1d51ab5d50f4d4207830f175
2021-07-31 18:31:38 +00:00
bit4bit
69a74c0714 generacion de cufe desde invoice
FossilOrigin-Name: d3494f20063452571b1e86d505f211e61fdf435aa43b870408136e3e9302bc17
2021-07-31 17:09:42 +00:00
bit4bit
a1a9746353 se adiciona __setup__ para inicializar modelos
FossilOrigin-Name: 504ad84bee1c708c2b55fdde3552d39980bf1efad3a51de8050018e4d1f387f3
2021-07-09 01:03:51 +00:00
bit4bit
b3e4a088b7 se adicion campo fields.Amount
FossilOrigin-Name: b23b2c243daaf0788cf47736015d75eecae9f4eb55cd4d31b9e063d7fa9a0691
2021-07-03 21:50:56 +00:00
bit4bit
507ddbe558 se crea legal monetary total
FossilOrigin-Name: 8ae3dfadfe9b90b8cc5ad59d2a73f4c8a987c5aac498573e56754a2d32e9e2ae
2021-06-27 02:16:16 +00:00
bit4bit
2e8aa35b29 prueba que confirma el orden del model y xml
FossilOrigin-Name: fc039dec57eec4287d1c2352f8fd0cc6fceaf1f409af02e115c2d99a96bdc7bd
2021-06-26 23:25:51 +00:00
bit4bit
ba908b938c se refleja el orden de los atributos del modelo en el xml
FossilOrigin-Name: 5ff5ebc397a11977916b7008ab4d5104a375290a5c5d0356098b69242378f1f8
2021-06-26 23:23:25 +00:00
bit4bit
4f15926656 se adiciona mas modelos a nuevo esquema
FossilOrigin-Name: acac57e60f808abdd89937be338d819f4f6fa9f8b4dda725569f445f96c982d3
2021-06-26 22:05:30 +00:00
bit4bit
1d6d1e2601 One2Many se puede iterar como lista
FossilOrigin-Name: 1de9daae362feb6693b928c4a58c11423127eb1783fdf509f0ef3083b5563b24
2021-06-26 21:32:24 +00:00
bit4bit
47a0dd33e2 se adiciona notificaciones de cambios para fields.One2Many
FossilOrigin-Name: b422aa912c7c7873edcbbecf1914e9ff21a24cab3fc7e2a788e00efc16fe2f51
2021-06-26 21:28:02 +00:00
bit4bit
53b5207e35 fields.on_changes no requiere un nombre especifico para su ejecucion
FossilOrigin-Name: 55d11605df9d1228737da18bf04b242fff3b08939021488f169ad2b042330d6f
2021-06-26 20:25:07 +00:00
bit4bit
bd25bef21f se continua con el nuevo modelado de facturacion
FossilOrigin-Name: 68ecac65b7ee5c2884161943e120df58ad596ffd3be82c3ce107ecf00eae6afa
2021-06-26 00:11:57 +00:00
bit4bit
5f5a6182c9 se adiciona fields.One2Many
FossilOrigin-Name: 94c1cca50451a46c417d925b27fdd53d8199b8dc58783e600c84179eac666a36
2021-06-25 23:55:36 +00:00
bit4bit
ab462a6ca5 se retiran archivos no usados
FossilOrigin-Name: 05431311b7cfe21cbde218f35b5adaef4171a78ed19c87b9e8bc8119ea091e45
2021-06-25 23:23:07 +00:00
bit4bit
c694603505 se adiciona @fields.on_change para ejecutar funcion cuando se cambian los varoles de algun attributo
FossilOrigin-Name: bee19b201f8c1a6b972c2a9abfe5fb57a558a67be6ecddce4f7f07b5b6980215
2021-06-25 23:21:04 +00:00
bit4bit
b6219bd171 fields.Function manejan default
FossilOrigin-Name: ec954ac9253429b99095fbcce443a2691f516f603282aec1ee59c4b7cbbd6c4a
2021-06-25 01:56:49 +00:00
bit4bit
a9dde83e81 se adiciona atributo default a many2one y attribute
FossilOrigin-Name: 9ddb1d1b8bebef24da17cc47d8fc70392f6015bb61866f251992aea518ed3d0f
2021-06-25 01:39:02 +00:00
bit4bit
3eacb29afa se separa responsabilidad de fields.Function
FossilOrigin-Name: 4d5daa47a75a0e283e86bf992126bf60f3a8a14287e9acc437d5f2f3eca43150
2021-06-24 23:38:28 +00:00
bit4bit
f630a544c2 se inicia modelo de facturacion usando nuevo esquema
FossilOrigin-Name: 8e6c23e7baa837c64b81baaed342b07eaab7ab631302cd2a8fa86f4989227d07
2021-06-24 02:11:33 +00:00
bit4bit
ba4e3d546f se depreca fields.Model port fields.Many2One
FossilOrigin-Name: 73d74488ca7458ff7dc84898ff76fa9b16b427cc6bc77540d7c81450e4f33869
2021-06-24 02:10:46 +00:00
bit4bit
92bae58e51 se instancia modelo en caso de no existir para Many2One
FossilOrigin-Name: 006f6a780ae0436649addd2abe89eb6a9bfc5ad573ee1a1835a8f65ab039fd26
2021-06-24 01:51:05 +00:00
bit4bit
58e7387292 fields.Model se adiciona __default_set__ para remplaza comportamiento de asignacion directa
FossilOrigin-Name: 436c5483cf534c8d457fb403302e511e7aad4b220d66569612f7ceb2da8d8cf8
2021-06-24 01:37:05 +00:00
bit4bit
a015a9361b fields.Function no requiere getter
FossilOrigin-Name: 47f9b9427ef55c688678001361260e5d00ea53d82977ea13e3414ed04878fb36
2021-06-24 01:28:07 +00:00
bit4bit
0216d0141a se adiciona archivo faltante
FossilOrigin-Name: 9b0c4d69d898ddbcd9279b3055e72df525feaefb0d46700dc6c019acdba01e80
2021-06-24 01:23:51 +00:00
bit4bit
6cc4610b45 fields.Model se permite cambiar el nombre de la etiqueta
FossilOrigin-Name: 896b797629e426a5e366d5be76fc00c3cc272299d6749e40f8317893b1545a9e
2021-06-24 01:18:27 +00:00
bit4bit
49feee8809 se adiciona mas pruebas
FossilOrigin-Name: 68e716388ee3328b1b451997eca99dc1f20b47db4ebe3dfc761daec6fec3c8d6
2021-06-24 01:15:16 +00:00
bit4bit
d78a429711 se adiciona field.Function
FossilOrigin-Name: 45a288bc30ad9b25fed59cd01c89bd2f7632926083384a7853c3b753a4d7f95b
2021-06-24 01:04:49 +00:00
bit4bit
84996066fa se implementa un esquema para modelar el xml
FossilOrigin-Name: e4de658f60fe8fcbb330923e14958a5d8f8e0e6395db4f992ec7da45062fa193
2021-06-23 23:04:00 +00:00
bit4bit
7d060e1786 Create new branch named "model"
FossilOrigin-Name: 55404ca978d7476847f2b6de1d9e8d78bd2ad67200d63695501e1a3a22265eb5
2021-06-23 13:55:41 +00:00
53 changed files with 4775 additions and 1999 deletions

1
.gitignore vendored
View File

@@ -215,3 +215,4 @@ tags
pyvenv.cfg pyvenv.cfg
.venv .venv
pip-selfcheck.json pip-selfcheck.json
invoice.xml

View File

@@ -7,18 +7,18 @@ 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.7 python3.7-distutils python3.7-dev \
python3.8 python3.8-distutils python3.8-dev \
python3.9 python3.9-distutils python3.9-dev \ 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.12 python3-setuptools python3.12-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.7 get-pip.py pip==22.2.2 \ && python3.9 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.8 get-pip.py pip==22.2.2 \ && python3.10 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.9 get-pip.py pip==22.2.2 \ && python3.11 get-pip.py pip==23.2.1 --break-system-packages \
&& python3.10 get-pip.py pip==22.2.2 \ && python3.12 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 \
@@ -27,14 +27,14 @@ RUN apt-get install -y --no-install-recommends \
build-essential \ build-essential \
zip zip
RUN python3.7 --version
RUN python3.8 --version
RUN python3.9 --version RUN python3.9 --version
RUN python3.10 --version RUN python3.10 --version
RUN python3.11 --version
RUN python3.12 --version
RUN pip3.7 install setuptools setuptools-rust
RUN pip3.8 install setuptools setuptools-rust
RUN pip3.9 install setuptools setuptools-rust 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.12 install setuptools setuptools-rust --break-system-packages
RUN pip3 install tox pytest RUN pip3 install tox pytest --break-system-packages

View File

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

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.7 setup.py test' docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.12 setup.py test'
tox: tox:
docker run -it -v $(PWD)/:/app -w /app facho tox docker run -it -v $(PWD)/:/app -w /app facho tox

View File

@@ -14,7 +14,7 @@ from facho.fe import fe
from datetime import datetime, date from datetime import datetime, date
# 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 INVOICE_AUTHORIZATION = '181360000001' # Número suministrado por la Dian en el momento de la creación del SET de Pruebas
ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas
PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas
CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por la Dian en el momento de la creación del SET de Pruebas CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por la Dian en el momento de la creación del SET de Pruebas
@@ -36,6 +36,7 @@ def extensions(inv):
'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():
# factura de venta nacional # factura de venta nacional
inv = form.Invoice('01') inv = form.Invoice('01')
@@ -49,16 +50,17 @@ def invoice():
inv.set_operation_type('10') inv.set_operation_type('10')
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
legal_name = 'Nombre registrado de la empresa', legal_name = 'Nombre registrado de la empresa',
name = 'Nombre comercial o él mismo nombre registrado', name='Nombre comercial o él mismo nombre registrado',
ident = form.PartyIdentification('nit_empresa', 'digito_verificación', '31'), ident=form.PartyIdentification(
'nit_empresa', 'digito_verificación', '31'),
# obligaciones del contribuyente ver DIAN:FAK26 # obligaciones del contribuyente ver DIAN:FAK26
responsability_code = form.Responsability(['O-07', 'O-14', 'O-48']), responsability_code=form.Responsability(['ZZ', 'O-14', 'O-48']),
# ver DIAN:FAJ28 # ver DIAN:FAJ28
responsability_regime_code = '48', responsability_regime_code='48',
# tipo de organizacion juridica ver DIAN:6.2.3 # tipo de organizacion juridica ver DIAN:6.2.3
organization_code = '1', organization_code='1',
email = "correoempresa@correoempresa.correo", email="correoempresa@correoempresa.correo",
address = form.Address( address=form.Address(
'', '', form.City('05001', 'Medellín'), '', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'), form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')), form.CountrySubentity('05', 'Antioquia')),
@@ -76,42 +78,43 @@ def invoice():
'', '', form.City('05001', 'Medellín'), '', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'), form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')), form.CountrySubentity('05', 'Antioquia')),
#tax_scheme = form.TaxScheme('01', 'IVA') # tax_scheme = form.TaxScheme('01', 'IVA')
)) ))
# asignar metodo de pago # asignar metodo de pago
inv.set_payment_mean(form.PaymentMean( inv.set_payment_mean(form.PaymentMean(
# metodo de pago ver DIAN:3.4.1 # metodo de pago ver DIAN:3.4.1
id = '1', id='1',
# codigo correspondiente al medio de pago ver DIAN:3.4.2 # codigocorrespondientealmediodepagoverDIAN:3.4.2
code = '20', code='20',
# fecha de vencimiento de la factura # fechadevencimientodelafactura
due_at = datetime.now(), due_at=datetime.now(),
# identificador numerico # identificadornumerico
payment_id = '2' payment_id='2'
)) ))
# adicionar una linea al documento # adicionar una linea al documento
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(
quantity = form.Quantity(int(20.5), '94'), form.InvoiceLine(
quantity=form.Quantity(int(20.5), '94'),
# item general de codigo 999 # item general de codigo 999
description = 'productO3', description='productO3',
item = form.StandardItem('test', 9999), sitem=form.StandardItem('test', 9999),
price = form.Price( price=form.Price(
# precio base del item (sin iva) # precio base del item (sin iva)
amount = form.Amount(200.00), amount=form.Amount(200.00),
# ver DIAN:6.3.5.1 # ver DIAN:6.3.5.1
type_code = '01', type_code='01',
type = 'x' type='x'
), ),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
percent = 19.00, percent=19.00,
scheme=form.TaxScheme('01') scheme=form.TaxScheme('01')
) )]
]
) )
)) ))
return inv return inv
def document_xml(): def document_xml():
return form_xml.DIANInvoiceXML return form_xml.DIANInvoiceXML

View File

@@ -1,109 +1,127 @@
# importar libreria de modelos # importar libreria de modelos
from facho import fe, form_xml
import facho.fe.form as form import facho.fe.form as form
import facho.fe.form_xml import datetime
PRIVATE_KEY_PATH = 'ruta a mi llave privada'
PRIVATE_PASSPHRASE = 'clave de la llave privada'
PRIVATE_KEY_PATH='ruta a mi llave privada'
PRIVATE_PASSPHRASE='clave de la llave privada'
# consultar las extensiones necesarias # consultar las extensiones necesarias
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, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS, cufe = fe.DianXMLExtensionCUFE(
inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS,
'clave tecnica') 'clave tecnica')
nit = form.PartyIdentification('nit', '5', '31') nit = form.PartyIdentification('nit', '5', '31')
software_provider = fe.DianXMLExtensionSoftwareProvider(nit, nit.dv, 'id software') software_provider = fe.DianXMLExtensionSoftwareProvider(
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization('invoice autorization', nit, nit.dv, 'id software')
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(
'invoice autorization',
datetime(2019, 1, 19), datetime(2019, 1, 19),
datetime(2030, 1, 19), datetime(2030, 1, 19),
'SETP', 990000001, 995000000) 'SETP', 990000001, 995000000)
return [security_code, authorization_provider, cufe, software_provider, inv_authorization] return [
security_code,
authorization_provider,
cufe, software_provider,
inv_authorization
]
# generar documento desde modelo a ruta indicada # generar documento desde modelo a ruta indicada
def generate_document(invoice, filepath): def generate_document(invoice, filepath):
xml = form_xml.DIANInvoiceXML(invoice) xml = form_xml.DIANInvoiceXML(invoice)
for extension in extensions(invoice): for extension in extensions(invoice):
xml.add_extension(extension) xml.add_extension(extension)
form_xml.utils.DIANWriteSigned(xml, filepath, PRIVATE_KEY_PATH, PRIVATE_PASSPHRASE, True) form_xml.utils.DIANWriteSigned(
xml, filepath, PRIVATE_KEY_PATH, PRIVATE_PASSPHRASE, True)
# Modelars las facturas # Modelars las facturas
# ... # ...
# factura de venta nacional # factura de venta nacional
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
# asignar periodo de facturacion # asignar periodo de facturacion
inv.set_period(datetime.now(), datetime.now()) inv.set_period(datetime.now(), datetime.now())
# asignar fecha de emision de la factura # asignar fecha de emision de la factura
inv.set_issue(datetime.now()) inv.set_issue(datetime.now())
# asignar prefijo y numero del documento # asignar prefijo y numero del documento
inv.set_ident('SETP990003033') inv.set_ident('SETP990003033')
# asignar tipo de operacion ver DIAN:6.1.5 # asignar tipo de operacion ver DIAN:6.1.5
inv.set_operation_type('10') inv.set_operation_type('10')
# asignar proveedor # asignar proveedor
inv.set_supplier(form.Party( inv.set_supplier(form.Party(
legal_name = 'FACHO SOS', legal_name='FACHOSOS',
name = 'FACHO SOS', name='FACHOSOS',
ident = form.PartyIdentification('900579212', '5', '31'), ident=form.PartyIdentification('900579212', '5', '31'),
# obligaciones del contribuyente ver DIAN:FAK26 # obligaciones del contribuyente ver DIAN:FAK26
responsability_code = form.Responsability(['O-07', 'O-09', 'O-14', 'O-48']), responsability_code=form.Responsability(['ZZ', 'O-09', 'O-14', 'O-48']),
# ver DIAN:FAJ28 # ver DIAN:FAJ28
responsability_regime_code = '48', responsability_regime_code='48',
# tipo de organizacion juridica ver DIAN:6.2.3 # tipo de organizacion juridica ver DIAN:6.2.3
organization_code = '1', organization_code='1',
email = "sdds@sd.com", email="sdds@sd.com",
address = form.Address( address=form.Address(
name = '', name='',
street = '', street='',
city = form.City('05001', 'Medellín'), city=form.City('05001', 'Medellín'),
country = form.Country('CO', 'Colombia'), country=form.Country('CO', 'Colombia'),
countrysubentity = form.CountrySubentity('05', 'Antioquia')) countrysubentity=form.CountrySubentity('05', 'Antioquia'))
)) ))
inv.set_customer(form.Party( inv.set_customer(form.Party(
legal_name = 'facho-customer', legal_name='facho-customer',
name = 'facho-customer', name='facho-customer',
ident = form.PartyIdentification('999999999', '', '13'), ident=form.PartyIdentification('999999999', '', '13'),
responsability_code = form.Responsability(['R-99-PN']), responsability_code=form.Responsability(['R-99-PN']),
responsability_regime_code = '49', responsability_regime_code='49',
organization_code = '2', organization_code='2',
email = "sdds@sd.com", email="sdds@sd.com",
address = form.Address( address=form.Address(
name = '', name='',
street = '', street='',
city = form.City('05001', 'Medellín'), city=form.City('05001', 'Medellín'),
country = form.Country('CO', 'Colombia'), country=form.Country('CO', 'Colombia'),
countrysubentity = form.CountrySubentity('05', 'Antioquia')) countrysubentity=form.CountrySubentity('05', 'Antioquia'))
)) ))
# asignar metodo de pago # asignar metodo de pago
inv.set_payment_mean(form.PaymentMean( inv.set_payment_mean(form.PaymentMean(
# metodo de pago ver DIAN:3.4.1 # metodo de pago ver DIAN:3.4.1
id = '1', id='1',
# codigo correspondiente al medio de pago ver DIAN:3.4.2 # codigo correspondiente al medio de pago ver DIAN:3.4.2
code = '10', code='10',
# fecha de vencimiento de la factura # fecha de vencimiento de la factura
due_at = datetime.now(), due_at=datetime.now(),
# identificador numerico # identificador numerico
payment_id = '1' payment_id='1'
)) ))
# adicionar una linea al documento # adicionar una linea al documento
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto facho', description='producto facho',
# item general de codigo 999 # item general de codigo 999
item = form.StandardItem('test', 9999), item=form.StandardItem('test', 9999),
price = form.Price( price=form.Price(
# precio base del tiem # precio base del tiem
amount = form.Amount(100.00), amount=form.Amount(100.00),
# ver DIAN:6.3.5.1 # ver DIAN:6.3.5.1
type_code = '01', type_code='01',
type = 'x' type='x'
), ),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
percent = 19.00, percent=19.00,
) )]
]
) )
)) ))

View File

@@ -76,7 +76,7 @@
<cac:PartyTaxScheme> <cac:PartyTaxScheme>
<cbc:RegistrationName>NEUROTEC TECNOLOGIA S.A.S</cbc:RegistrationName> <cbc:RegistrationName>NEUROTEC TECNOLOGIA S.A.S</cbc:RegistrationName>
<cbc:CompanyID schemeAgencyName="CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)" schemeAgencyID="195" schemeID="5" schemeName="31">900579212</cbc:CompanyID> <cbc:CompanyID schemeAgencyName="CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)" schemeAgencyID="195" schemeID="5" schemeName="31">900579212</cbc:CompanyID>
<cbc:TaxLevelCode listName="48">O-07;O-09;O-14;O-48</cbc:TaxLevelCode> <cbc:TaxLevelCode listName="48">ZZ;O-09;O-14;O-48</cbc:TaxLevelCode>
<cac:TaxScheme/> <cac:TaxScheme/>
</cac:PartyTaxScheme> </cac:PartyTaxScheme>
<cac:Contact> <cac:Contact>

View File

@@ -389,94 +389,12 @@ class FachoXML:
def get_element(self, xpath, multiple=False): def get_element(self, xpath, multiple=False):
xpath = self.fragment_prefix + self._path_xpath_for(xpath) xpath = self.fragment_prefix + self._path_xpath_for(xpath)
return self.builder.xpath(self.root, xpath, multiple=multiple) elem = self.builder.xpath(self.root, xpath)
def get_element_text(self, xpath, format_=str, multiple=False):
xpath = self.fragment_prefix + self._path_xpath_for(xpath)
# MACHETE(bit4bit) al usar ./ queda ../
xpath = re.sub(r'^\.\.+', '.', xpath)
elem = self.builder.xpath(self.root, xpath, multiple=multiple)
if multiple:
vals = []
for e in elem:
text = self.builder.get_text(e)
if text is not None:
vals.append(format_(text))
return vals
else:
text = self.builder.get_text(elem)
if text is None:
return None
return format_(text)
def get_element_text_or_attribute(
self, xpath, default=None, multiple=False, raise_on_fail=False):
parts = xpath.split('/')
is_attribute = parts[-1].startswith('@')
if is_attribute:
attribute_name = parts.pop(-1).lstrip('@')
element_path = "/".join(parts)
try:
val = self.get_element_attribute(
element_path, attribute_name, multiple=multiple)
if val is None:
return default
return val
except KeyError as e:
if raise_on_fail:
raise e
return default
except ValueError as e:
if raise_on_fail:
raise e
return default
else:
try:
val = self.get_element_text(xpath, multiple=multiple)
if val is None:
return default
return val
except ValueError as e:
if raise_on_fail:
raise e
return default
def get_elements_text_or_attributes(self, xpaths, raise_on_fail=True):
"""
returna el contenido o attributos de un conjunto de XPATHS
si algun XPATH es una tupla se retorna el primer elemento del mismo.
"""
vals = []
for xpath in xpaths:
if isinstance(xpath, tuple):
val = xpath[0]
else:
val = self.get_element_text_or_attribute(
xpath, raise_on_fail=raise_on_fail)
vals.append(val)
return vals
def exist_element(self, xpath):
elem = self.get_element(xpath)
# no se encontro elemento
if elem is None: if elem is None:
return False raise AttributeError('xpath %s invalid' % (xpath))
# el placeholder no ha sido populado text = self.builder.get_text(elem)
if elem.get('facho_placeholder') == 'True': return str(text)
return False
# el valor opcional no ha sido populado
if elem.get('facho_optional') == 'True':
return False
return True
def _remove_facho_attributes(self, elem):
self.builder.remove_attributes(
elem, ['facho_optional', 'facho_placeholder'])
def tostring(self, **kw): def tostring(self, **kw):
return self.builder.tostring(self.root, **kw) return self.builder.tostring(self.root, **kw)

View File

@@ -96,27 +96,5 @@
<SimpleValue>Exclusivo en referencias a documentos (elementos DocumentReference)</SimpleValue> <SimpleValue>Exclusivo en referencias a documentos (elementos DocumentReference)</SimpleValue>
</Value> </Value>
</Row> </Row>
<Row>
<Value ColumnRef="code">
<SimpleValue>05</SimpleValue>
</Value>
<Value ColumnRef="name">
<SimpleValue>Documento soporte en adquisiciones efectuadas a sujetos no obligados a expedir factura o documento equivalente</SimpleValue>
</Value>
<Value ColumnRef="description">
<SimpleValue>Tipo de documento</SimpleValue>
</Value>
</Row>
<Row>
<Value ColumnRef="code">
<SimpleValue>95</SimpleValue>
</Value>
<Value ColumnRef="name">
<SimpleValue>Nota de ajuste al documento soporte en adquisiciones efectuadas a sujetos no obligados a expedir factura o documento equivalente</SimpleValue>
</Value>
<Value ColumnRef="description">
<SimpleValue>Exclusivo en referencias a documentos (elementos DocumentReference)</SimpleValue>
</Value>
</Row>
</SimpleCodeList> </SimpleCodeList>
</gc:CodeList> </gc:CodeList>

View File

@@ -64,26 +64,10 @@
</Row> </Row>
<Row> <Row>
<Value ColumnRef="code"> <Value ColumnRef="code">
<SimpleValue>O-48</SimpleValue> <SimpleValue>ZZ</SimpleValue>
</Value> </Value>
<Value ColumnRef="name"> <Value ColumnRef="name">
<SimpleValue>Impuesto sobre las ventas IVA</SimpleValue> <SimpleValue>No aplica</SimpleValue>
</Value>
</Row>
<Row>
<Value ColumnRef="code">
<SimpleValue>O-49</SimpleValue>
</Value>
<Value ColumnRef="name">
<SimpleValue>No responsable de IVA</SimpleValue>
</Value>
</Row>
<Row>
<Value ColumnRef="code">
<SimpleValue>R-99-PN</SimpleValue>
</Value>
<Value ColumnRef="name">
<SimpleValue>No responsable</SimpleValue>
</Value> </Value>
</Row> </Row>
</SimpleCodeList> </SimpleCodeList>

View File

@@ -1,6 +1,5 @@
# This file is part of facho. The COPYRIGHT file at the top level of # This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from ..facho import FachoXML, FachoXMLExtension, LXMLBuilder from ..facho import FachoXML, FachoXMLExtension, LXMLBuilder
import uuid import uuid
import xmlsig import xmlsig
@@ -8,13 +7,16 @@ import xades
from datetime import datetime from datetime import datetime
import OpenSSL import OpenSSL
import zipfile import zipfile
import warnings # import warnings
import hashlib import hashlib
from contextlib import contextmanager from contextlib import contextmanager
from .data.dian import codelist from .data.dian import codelist
from . import form from . import form
from collections import defaultdict from collections import defaultdict
from pathlib import Path # from pathlib import Path
from dateutil import tz
from cryptography.hazmat.primitives.serialization import 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']
@@ -30,7 +32,7 @@ SCHEME_AGENCY_ATTRS = {
POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf' POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf'
POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.' POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.'
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',
@@ -55,6 +57,8 @@ POLICY_NAME = u'Política de firma para facturas electrónicas de la República
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': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
@@ -68,11 +72,11 @@ NAMESPACES = {
} }
def fe_from_string(document: str) -> FachoXML: def fe_from_string(document: str) -> FachoXML:
return FeXML.from_string(document) return FeXML.from_string(document)
from contextlib import contextmanager
# from contextlib import contextmanager
@contextmanager @contextmanager
def mock_xades_policy(): def mock_xades_policy():
from mock import patch from mock import patch
@@ -104,22 +108,25 @@ class FeXML(FachoXML):
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 namespace
urn_oasis = {
'AttachedDocument': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
'Invoice': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
'CreditNote': 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2',
'ApplicationResponse': 'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2'
}
root_namespace = self.root_namespace() root_namespace = self.root_namespace()
root_localname = self.root_localname() root_localname = self.root_localname()
xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace] xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace]
if root_localname == 'Invoice':
urn_oasis = 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
if root_localname == 'CreditNote':
urn_oasis = 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2'
return super().tostring(**kw)\ return super().tostring(**kw)\
.replace(xmlns_name + ':', '')\ .replace(xmlns_name + ':', '')\
.replace('xmlns:'+xmlns_name, 'xmlns')\ .replace('xmlns:'+xmlns_name, 'xmlns')\
.replace(root_namespace, urn_oasis) .replace(root_namespace, urn_oasis[root_localname])
class DianXMLExtensionCUDFE(FachoXMLExtension): class DianXMLExtensionCUDFE(FachoXMLExtension):
def __init__(self, invoice, tipo_ambiente=AMBIENTE_PRUEBAS):
def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente self.tipo_ambiente = tipo_ambiente
self.invoice = invoice self.invoice = invoice
@@ -157,9 +164,11 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
# #DIAN 1.8.-2021: FAD03 # #DIAN 1.8.-2021: FAD03
# fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') # fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta')
fachoxml.set_element('./cbc:ProfileExecutionID', self._tipo_ambiente_int()) fachoxml.set_element(
'./cbc:ProfileExecutionID', self._tipo_ambiente_int())
#DIAN 1.7.-2020: FAB36 #DIAN 1.7.-2020: FAB36
fachoxml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode', fachoxml.set_element(
'./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_):
@@ -176,7 +185,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
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'] = invoice.invoice_legal_monetary_total.payable_amount build_vars['ValorTotalPagar'
] = invoice.invoice_legal_monetary_total.payable_amount
ValorImpuestoPara = defaultdict(lambda: form.Amount(0.0)) ValorImpuestoPara = defaultdict(lambda: form.Amount(0.0))
build_vars['CodImpuesto1'] = '01' build_vars['CodImpuesto1'] = '01'
build_vars['CodImpuesto2'] = '04' build_vars['CodImpuesto2'] = '04'
@@ -206,7 +216,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
class DianXMLExtensionCUFE(DianXMLExtensionCUDFE): class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
def __init__(self, invoice, clave_tecnica = '', tipo_ambiente = AMBIENTE_PRUEBAS): def __init__(
self, invoice, clave_tecnica='', tipo_ambiente=AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente self.tipo_ambiente = tipo_ambiente
self.clave_tecnica = clave_tecnica self.clave_tecnica = clave_tecnica
self.invoice = invoice self.invoice = invoice
@@ -242,6 +253,7 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
'%d' % build_vars['TipoAmb'], '%d' % build_vars['TipoAmb'],
] ]
class DianXMLExtensionCUDE(DianXMLExtensionCUDFE): class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS): def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente self.tipo_ambiente = tipo_ambiente
@@ -279,6 +291,7 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
'%d' % build_vars['TipoAmb'], '%d' % build_vars['TipoAmb'],
] ]
class DianXMLExtensionCUDS(DianXMLExtensionCUDFE): class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS): def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente self.tipo_ambiente = tipo_ambiente
@@ -312,6 +325,7 @@ class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
'%d' % build_vars['TipoAmb'], '%d' % build_vars['TipoAmb'],
] ]
class DianXMLExtensionSoftwareProvider(FachoXMLExtension): class DianXMLExtensionSoftwareProvider(FachoXMLExtension):
# RESOLUCION 0004: pagina 108 # RESOLUCION 0004: pagina 108
@@ -321,7 +335,8 @@ class DianXMLExtensionSoftwareProvider(FachoXMLExtension):
self.id_software = id_software self.id_software = id_software
def build(self, fexml): def build(self, fexml):
software_provider = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') software_provider = fexml.fragment(
'./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
@@ -351,7 +366,6 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
class DianXMLExtensionSigner: class DianXMLExtensionSigner:
def __init__(self, pkcs12_path, passphrase=None, localpolicy=True): def __init__(self, pkcs12_path, passphrase=None, localpolicy=True):
self._pkcs12_data = open(pkcs12_path, 'rb').read() self._pkcs12_data = open(pkcs12_path, 'rb').read()
self._passphrase = None self._passphrase = None
@@ -362,7 +376,6 @@ class DianXMLExtensionSigner:
@classmethod @classmethod
def from_bytes(cls, data, passphrase=None, localpolicy=True): def from_bytes(cls, data, passphrase=None, localpolicy=True):
self = cls.__new__(cls) self = cls.__new__(cls)
self._pkcs12_data = data self._pkcs12_data = data
self._passphrase = None self._passphrase = None
self._localpolicy = localpolicy self._localpolicy = localpolicy
@@ -372,7 +385,9 @@ class DianXMLExtensionSigner:
return self return self
def _element_extension_content(self, fachoxml): def _element_extension_content(self, fachoxml):
return fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent') return fachoxml.builder.xpath(
fachoxml.root,
'./ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent')
def sign_xml_string(self, document): def sign_xml_string(self, document):
xml = LXMLBuilder.from_string(document) xml = LXMLBuilder.from_string(document)
@@ -394,7 +409,6 @@ 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)
) )
@@ -402,14 +416,16 @@ class DianXMLExtensionSigner:
id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid) id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid)
xmlsig.template.add_reference( xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_keyinfo), name="xmldsig-%s-ref1" % (id_uuid), signature, xmlsig.constants.TransformSha256, uri="#%s" % (
id_keyinfo), name="xmldsig-%s-ref1" % (id_uuid),
) )
ki = xmlsig.template.ensure_key_info(signature, name=id_keyinfo) ki = xmlsig.template.ensure_key_info(signature, name=id_keyinfo)
data = xmlsig.template.add_x509_data(ki) data = xmlsig.template.add_x509_data(ki)
xmlsig.template.x509_data_add_certificate(data) xmlsig.template.x509_data_add_certificate(data)
xmlsig.template.add_key_value(ki) xmlsig.template.add_key_value(ki)
qualifying = xades.template.create_qualifying_properties(signature, 'XadesObjects', 'xades') qualifying = xades.template.create_qualifying_properties(
signature, 'XadesObjects', 'xades')
xades.utils.ensure_id(qualifying) xades.utils.ensure_id(qualifying)
id_props = "xmldsig-%s-signedprops" % (id_uuid) id_props = "xmldsig-%s-signedprops" % (id_uuid)
@@ -417,10 +433,12 @@ class DianXMLExtensionSigner:
signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_props), signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_props),
uri_type="http://uri.etsi.org/01903#SignedProperties" uri_type="http://uri.etsi.org/01903#SignedProperties"
) )
xmlsig.template.add_transform(props_ref, xmlsig.constants.TransformInclC14N) xmlsig.template.add_transform(
props_ref, xmlsig.constants.TransformInclC14N)
# TODO assert with http://www.sic.gov.co/hora-legal-colombiana # TODO assert with http://www.sic.gov.co/hora-legal-colombiana
props = xades.template.create_signed_properties(qualifying, name=id_props, datetime=datetime.now()) props = xades.template.create_signed_properties(
qualifying, name=id_props, datetime=datetime.now(tz=Bogota))
xades.template.add_claimed_role(props, "supplier") xades.template.add_claimed_role(props, "supplier")
policy = xades.policy.GenericPolicyId( policy = xades.policy.GenericPolicyId(
@@ -428,9 +446,13 @@ class DianXMLExtensionSigner:
POLICY_NAME, POLICY_NAME,
xmlsig.constants.TransformSha256) xmlsig.constants.TransformSha256)
ctx = xades.XAdESContext(policy) ctx = xades.XAdESContext(policy)
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(self._pkcs12_data, ctx.load_pkcs12(pkcs12.load_key_and_certificates(
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)
@@ -438,7 +460,7 @@ class DianXMLExtensionSigner:
else: else:
ctx.sign(signature) ctx.sign(signature)
ctx.verify(signature) ctx.verify(signature)
#xmlsig take parent root # xmlsig take parent root
xml.remove(signature) xml.remove(signature)
return signature return signature
@@ -454,19 +476,18 @@ 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)
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:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode'
fexml.set_element(dian_path, 'CO', fexml.set_element(
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")
@@ -499,16 +520,15 @@ class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To', invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To',
self.to) self.to)
fexml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode', fexml.set_element(
'./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:
@@ -517,7 +537,8 @@ class DianZIP:
MAX_FILES = 50 MAX_FILES = 50
def __init__(self, file_like): def __init__(self, file_like):
self.zipfile = zipfile.ZipFile(file_like, mode='w', compression=zipfile.ZIP_DEFLATED) self.zipfile = zipfile.ZipFile(
file_like, mode='w', compression=zipfile.ZIP_DEFLATED)
self.num_files = 0 self.num_files = 0
def add_xml(self, name, xml_data): def add_xml(self, name, xml_data):
@@ -538,7 +559,6 @@ class DianZIP:
def __enter__(self): def __enter__(self):
""" """
Facilita el uso de esta manera: Facilita el uso de esta manera:
f = open('xxx', 'rb') f = open('xxx', 'rb')
with DianZIP(f) as zip: with DianZIP(f) as zip:
zip.add_invoice_xml('name', 'data xml') zip.add_invoice_xml('name', 'data xml')
@@ -561,7 +581,7 @@ class DianXMLExtensionSignerVerifier:
def verify_string(self, document): def verify_string(self, document):
# Obtener FachoXML # Obtener FachoXML
xml = LXMLBuilder.from_string(document) xml = LXMLBuilder.from_string(document)
fachoxml = FachoXML(xml,nsmap=NAMESPACES) fachoxml = FachoXML(xml, nsmap=NAMESPACES)
# Obtener Signature # Obtener Signature
signature = fachoxml.builder.xpath(fachoxml.root, '//ds:Signature') signature = fachoxml.builder.xpath(fachoxml.root, '//ds:Signature')

View File

@@ -1,12 +1,14 @@
# This file is part of facho. The COPYRIGHT file at the top level of # This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
import hashlib
from functools import reduce # import hashlib
import copy # from functools import reduce
# import copy
import dataclasses import dataclasses
from dataclasses import dataclass from dataclasses import dataclass, field
from datetime import datetime, date from datetime import datetime, date
from collections import defaultdict # from collections import defaultdict
import decimal import decimal
from decimal import Decimal from decimal import Decimal
import typing import typing
@@ -14,9 +16,11 @@ from ..data.dian import codelist
DECIMAL_PRECISION = 6 DECIMAL_PRECISION = 6
class AmountCurrencyError(TypeError): class AmountCurrencyError(TypeError):
pass pass
@dataclass @dataclass
class Currency: class Currency:
code: str code: str
@@ -27,6 +31,7 @@ class Currency:
def __str__(self): def __str__(self):
return self.code return self.code
class Collection: class Collection:
def __init__(self, array): def __init__(self, array):
@@ -43,6 +48,7 @@ class Collection:
def sum(self): def sum(self):
return sum(self.array) return sum(self.array)
class AmountCollection(Collection): class AmountCollection(Collection):
def sum(self): def sum(self):
@@ -51,10 +57,14 @@ class AmountCollection(Collection):
total += v total += v
return total return total
class Amount:
def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')):
#DIAN 1.7.-2020: 1.2.3.1 class Amount:
def __init__(
self, amount: int or float or str,
currency: Currency = Currency('COP'),
precision=DECIMAL_PRECISION):
self.precision = precision
# 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):
raise ValueError('amount must be positive >= 0') raise ValueError('amount must be positive >= 0')
@@ -65,9 +75,12 @@ class Amount:
if float(amount) < 0: if float(amount) < 0:
raise ValueError('amount must be positive >= 0') raise ValueError('amount must be positive >= 0')
self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, self.amount = Decimal(
#DIAN 1.7.-2020: 1.2.1.1 amount, decimal.Context(
rounding=decimal.ROUND_HALF_EVEN )) prec=self.precision,
# DIAN 1.7.-2020: 1.2.1.1
rounding=decimal.ROUND_HALF_EVEN)
)
self.currency = currency self.currency = currency
def fromNumber(self, val): def fromNumber(self, val):
@@ -85,19 +98,24 @@ class Amount:
def __lt__(self, other): def __lt__(self, other):
if not self.is_same_currency(other): if not self.is_same_currency(other):
raise AmountCurrencyError() raise AmountCurrencyError()
return round(self.amount, DECIMAL_PRECISION) < round(other, 2) return round(self.amount, self.precision) < round(other, 2)
def __eq__(self, other): def __eq__(self, other):
if not self.is_same_currency(other): if not self.is_same_currency(other):
raise AmountCurrencyError() raise AmountCurrencyError()
return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION) return round(self.amount, self.precision) == round(
other.amount, self.precision)
def _cast(self, val): def _cast(self, val):
if type(val) in [int, float]: if type(val) in [int, float]:
return self.fromNumber(val) return self.fromNumber(val)
if isinstance(val, Amount): if isinstance(val, Amount):
return val return val
raise TypeError("cant cast to amount")
if isinstance(val, Decimal):
return self.fromNumber(float(val))
raise TypeError("cant cast %s to amount" % (type(val)))
def __add__(self, rother): def __add__(self, rother):
other = self._cast(rother) other = self._cast(rother)
@@ -122,7 +140,7 @@ class Amount:
def truncate_as_string(self, prec): def truncate_as_string(self, prec):
parts = str(self.float()).split('.', 1) parts = str(self.float()).split('.', 1)
return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0')) return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec, '0'))
def float(self): def float(self):
return float(round(self.amount, DECIMAL_PRECISION)) return float(round(self.amount, DECIMAL_PRECISION))
@@ -151,6 +169,7 @@ class Quantity:
def __repr__(self): def __repr__(self):
return str(self) return str(self)
@dataclass @dataclass
class Item: class Item:
scheme_name: str scheme_name: str
@@ -188,6 +207,7 @@ class Country:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.Paises[self.code]['name'] self.name = codelist.Paises[self.code]['name']
@dataclass @dataclass
class CountrySubentity: class CountrySubentity:
code: str code: str
@@ -198,6 +218,7 @@ class CountrySubentity:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.Departamento[self.code]['name'] self.name = codelist.Departamento[self.code]['name']
@dataclass @dataclass
class City: class City:
code: str code: str
@@ -208,18 +229,22 @@ class City:
raise ValueError("code [%s] not found" % (self.code)) raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.Municipio[self.code]['name'] self.name = codelist.Municipio[self.code]['name']
@dataclass @dataclass
class PostalZone: class PostalZone:
code: str = '' code: str = ''
@dataclass @dataclass
class Address: class Address:
name: str name: str
street: str = '' street: str = ''
city: City = City('05001') city: City = field(default_factory=lambda: City('05001'))
country: Country = Country('CO') country: Country = field(default_factory=lambda: Country('CO'))
countrysubentity: CountrySubentity = CountrySubentity('05') countrysubentity: CountrySubentity = field(
postalzone: PostalZone = PostalZone('') default_factory=lambda: CountrySubentity('05'))
postalzone: PostalZone = field(default_factory=lambda: PostalZone(''))
@dataclass @dataclass
class PartyIdentification: class PartyIdentification:
@@ -240,6 +265,7 @@ class PartyIdentification:
if self.type_fiscal not in codelist.TipoIdFiscal: if self.type_fiscal not in codelist.TipoIdFiscal:
raise ValueError("type_fiscal [%s] not found" % (self.type_fiscal)) raise ValueError("type_fiscal [%s] not found" % (self.type_fiscal))
@dataclass @dataclass
class Responsability: class Responsability:
codes: list codes: list
@@ -269,6 +295,7 @@ class TaxScheme:
raise ValueError("code not found") raise ValueError("code not found")
self.name = codelist.TipoImpuesto[self.code]['name'] self.name = codelist.TipoImpuesto[self.code]['name']
@dataclass @dataclass
class Party: class Party:
name: str name: str
@@ -276,10 +303,10 @@ class Party:
responsability_code: typing.List[Responsability] responsability_code: typing.List[Responsability]
responsability_regime_code: str responsability_regime_code: str
organization_code: str organization_code: str
tax_scheme: TaxScheme = TaxScheme('01') tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01'))
phone: str = '' phone: str = ''
address: Address = Address('') address: Address = field(default_factory=lambda: Address(''))
email: str = '' email: str = ''
legal_name: str = '' legal_name: str = ''
legal_company_ident: str = '' legal_company_ident: str = ''
@@ -307,7 +334,7 @@ class TaxScheme:
class TaxSubTotal: class TaxSubTotal:
percent: float percent: float
scheme: typing.Optional[TaxScheme] = None scheme: typing.Optional[TaxScheme] = None
tax_amount: Amount = Amount(0.0) tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline): def calculate(self, invline):
if self.percent is not None: if self.percent is not None:
@@ -317,8 +344,8 @@ class TaxSubTotal:
@dataclass @dataclass
class TaxTotal: class TaxTotal:
subtotals: list subtotals: list
tax_amount: Amount = Amount(0.0) tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
taxable_amount: Amount = Amount(0.0) taxable_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline): def calculate(self, invline):
self.taxable_amount = invline.total_amount self.taxable_amount = invline.total_amount
@@ -334,21 +361,23 @@ class TaxTotalOmit(TaxTotal):
def calculate(self, invline): def calculate(self, invline):
pass pass
@dataclass @dataclass
class WithholdingTaxSubTotal: class WithholdingTaxSubTotal:
percent: float percent: float
scheme: typing.Optional[TaxScheme] = None scheme: typing.Optional[TaxScheme] = None
tax_amount: Amount = Amount(0.0) tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline): def calculate(self, invline):
if self.percent is not None: if self.percent is not None:
self.tax_amount = invline.total_amount * Amount(self.percent / 100) self.tax_amount = invline.total_amount * Amount(self.percent / 100)
@dataclass @dataclass
class WithholdingTaxTotal: class WithholdingTaxTotal:
subtotals: list subtotals: list
tax_amount: Amount = Amount(0.0) tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
taxable_amount: Amount = Amount(0.0) taxable_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline): def calculate(self, invline):
self.taxable_amount = invline.total_amount self.taxable_amount = invline.total_amount
@@ -357,6 +386,7 @@ class WithholdingTaxTotal:
subtax.calculate(invline) subtax.calculate(invline)
self.tax_amount += subtax.tax_amount self.tax_amount += subtax.tax_amount
class WithholdingTaxTotalOmit(WithholdingTaxTotal): class WithholdingTaxTotalOmit(WithholdingTaxTotal):
def __init__(self): def __init__(self):
super().__init__([]) super().__init__([])
@@ -364,6 +394,7 @@ class WithholdingTaxTotalOmit(WithholdingTaxTotal):
def calculate(self, invline): def calculate(self, invline):
pass pass
@dataclass @dataclass
class Price: class Price:
amount: Amount amount: Amount
@@ -379,6 +410,7 @@ class Price:
self.amount *= self.quantity self.amount *= self.quantity
@dataclass @dataclass
class PaymentMean: class PaymentMean:
DEBIT = '01' DEBIT = '01'
@@ -396,8 +428,9 @@ class PaymentMean:
@dataclass @dataclass
class PrePaidPayment: class PrePaidPayment:
#DIAN 1.7.-2020: FBD03 # DIAN 1.7.-2020: FBD03
paid_amount: Amount = Amount(0.0) paid_amount: Amount = field(default_factory=lambda: Amount(0.0))
@dataclass @dataclass
class BillingResponse: class BillingResponse:
@@ -405,6 +438,7 @@ class BillingResponse:
code: str code: str
description: str description: str
class SupportDocumentCreditNoteResponse(BillingResponse): class SupportDocumentCreditNoteResponse(BillingResponse):
""" """
ReferenceID: Identifica la sección del Documento ReferenceID: Identifica la sección del Documento
@@ -414,13 +448,13 @@ class SupportDocumentCreditNoteResponse(BillingResponse):
""" """
@dataclass @dataclass
class BillingReference: class BillingReference:
ident: str ident: str
uuid: str uuid: str
date: date date: date
class CreditNoteDocumentReference(BillingReference): class CreditNoteDocumentReference(BillingReference):
""" """
ident: Prefijo + Numero de la factura relacionada ident: Prefijo + Numero de la factura relacionada
@@ -428,6 +462,7 @@ class CreditNoteDocumentReference(BillingReference):
date: fecha de emision de la factura relacionada date: fecha de emision de la factura relacionada
""" """
class DebitNoteDocumentReference(BillingReference): class DebitNoteDocumentReference(BillingReference):
""" """
ident: Prefijo + Numero de la factura relacionada ident: Prefijo + Numero de la factura relacionada
@@ -435,6 +470,7 @@ class DebitNoteDocumentReference(BillingReference):
date: fecha de emision de la factura relacionada date: fecha de emision de la factura relacionada
""" """
class InvoiceDocumentReference(BillingReference): class InvoiceDocumentReference(BillingReference):
""" """
ident: Prefijo + Numero de la nota credito relacionada ident: Prefijo + Numero de la nota credito relacionada
@@ -442,6 +478,7 @@ class InvoiceDocumentReference(BillingReference):
date: fecha de emision de la nota credito relacionada date: fecha de emision de la nota credito relacionada
""" """
@dataclass @dataclass
class AllowanceChargeReason: class AllowanceChargeReason:
code: str code: str
@@ -454,22 +491,26 @@ class AllowanceChargeReason:
@dataclass @dataclass
class AllowanceCharge: class AllowanceCharge:
#DIAN 1.7.-2020: FAQ03 # DIAN 1.7.-2020: FAQ03
charge_indicator: bool = True charge_indicator: bool = True
amount: Amount = Amount(0.0) amount: Amount = field(default_factory=lambda: Amount(0.0))
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] = Amount(0.0) base_amount: typing.Optional[Amount] = field(
default_factory=lambda: Amount(0.0))
# Porcentaje: Porcentaje que aplicar. # Porcentaje: Porcentaje que aplicar.
multiplier_factor_numeric: Amount = Amount(1.0) multiplier_factor_numeric: Amount = field(
default_factory=lambda: Amount(1.0))
def isCharge(self): def isCharge(self):
return self.charge_indicator == True charge_indicator = self.charge_indicator is True
return charge_indicator
def isDiscount(self): def isDiscount(self):
return self.charge_indicator == False charge_indicator = self.charge_indicator is False
return charge_indicator
def asCharge(self): def asCharge(self):
self.charge_indicator = True self.charge_indicator = True
@@ -483,11 +524,13 @@ class AllowanceCharge:
def set_base_amount(self, amount): def set_base_amount(self, amount):
self.base_amount = amount self.base_amount = amount
class AllowanceChargeAsDiscount(AllowanceCharge): class AllowanceChargeAsDiscount(AllowanceCharge):
def __init__(self, amount: Amount = Amount(0.0)): def __init__(self, amount: Amount = Amount(0.0)):
self.charge_indicator = False self.charge_indicator = False
self.amount = amount self.amount = amount
@dataclass @dataclass
class InvoiceLine: class InvoiceLine:
# RESOLUCION 0004: pagina 155 # RESOLUCION 0004: pagina 155
@@ -501,7 +544,8 @@ class InvoiceLine:
# de subtotal # de subtotal
tax: typing.Optional[TaxTotal] tax: typing.Optional[TaxTotal]
withholding: typing.Optional[WithholdingTaxTotal] withholding: typing.Optional[WithholdingTaxTotal]
allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list) allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(
default_factory=list)
def add_allowance_charge(self, charge): def add_allowance_charge(self, charge):
if not isinstance(charge, AllowanceCharge): if not isinstance(charge, AllowanceCharge):
@@ -566,18 +610,19 @@ class InvoiceLine:
if self.withholding is None: if self.withholding is None:
self.withholding = WithholdingTaxTotalOmit() self.withholding = WithholdingTaxTotalOmit()
@dataclass @dataclass
class LegalMonetaryTotal: class LegalMonetaryTotal:
line_extension_amount: Amount = Amount(0.0) line_extension_amount: Amount = field(default_factory=lambda: Amount(0.0))
tax_exclusive_amount: Amount = Amount(0.0) tax_exclusive_amount: Amount = field(default_factory=lambda: Amount(0.0))
tax_inclusive_amount: Amount = Amount(0.0) tax_inclusive_amount: Amount = field(default_factory=lambda: Amount(0.0))
charge_total_amount: Amount = Amount(0.0) charge_total_amount: Amount = field(default_factory=lambda: Amount(0.0))
allowance_total_amount: Amount = Amount(0.0) allowance_total_amount: Amount = field(default_factory=lambda: Amount(0.0))
payable_amount: Amount = Amount(0.0) payable_amount: Amount = field(default_factory=lambda: Amount(0.0))
prepaid_amount: Amount = Amount(0.0) prepaid_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self): def calculate(self):
#DIAN 1.7.-2020: FAU14 # DIAN 1.7.-2020: FAU14
self.payable_amount = \ self.payable_amount = \
self.tax_inclusive_amount \ self.tax_inclusive_amount \
+ self.allowance_total_amount \ + self.allowance_total_amount \
@@ -585,26 +630,29 @@ class LegalMonetaryTotal:
- self.prepaid_amount - self.prepaid_amount
class NationalSalesInvoiceDocumentType(str): class NationalSalesInvoiceDocumentType(str):
def __str__(self): def __str__(self):
# 6.1.3 # 6.1.3
return '01' return '01'
class CreditNoteDocumentType(str): class CreditNoteDocumentType(str):
def __str__(self): def __str__(self):
# 6.1.3 # 6.1.3
return '91' return '91'
class DebitNoteDocumentType(str): class DebitNoteDocumentType(str):
def __str__(self): def __str__(self):
# 6.1.3 # 6.1.3
return '92' return '92'
class CreditNoteSupportDocumentType(str): class CreditNoteSupportDocumentType(str):
def __str__(self): def __str__(self):
return '95' return '95'
class Invoice: class Invoice:
def __init__(self, type_code: str): def __init__(self, type_code: str):
if str(type_code) not in codelist.TipoDocumento: if str(type_code) not in codelist.TipoDocumento:
@@ -650,7 +698,8 @@ class Invoice:
if len(prefix) <= 4: if len(prefix) <= 4:
self.invoice_ident_prefix = prefix self.invoice_ident_prefix = prefix
else: else:
raise ValueError('ident prefix failed to get, expected 0 to 4 chars') raise ValueError(
'ident prefix failed to get, expected 0 to 4 chars')
def set_ident(self, ident: str): def set_ident(self, ident: str):
""" """
@@ -703,7 +752,6 @@ class Invoice:
def set_discrepancy_response(self, billing_response: BillingResponse): def set_discrepancy_response(self, billing_response: BillingResponse):
self.invoice_discrepancy_response = billing_response self.invoice_discrepancy_response = billing_response
def accept(self, visitor): def accept(self, visitor):
visitor.visit_payment_mean(self.invoice_payment_mean) visitor.visit_payment_mean(self.invoice_payment_mean)
visitor.visit_customer(self.invoice_customer) visitor.visit_customer(self.invoice_customer)
@@ -715,29 +763,34 @@ class Invoice:
def _calculate_legal_monetary_total(self): def _calculate_legal_monetary_total(self):
for invline in self.invoice_lines: for invline in self.invoice_lines:
self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount self.invoice_legal_monetary_total.line_extension_amount +=\
self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount invline.total_amount
#DIAN 1.7.-2020: FAU6 self.invoice_legal_monetary_total.tax_exclusive_amount +=\
self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount invline.total_tax_exclusive_amount
# DIAN 1.7.-2020: FAU6
self.invoice_legal_monetary_total.tax_inclusive_amount +=\
invline.total_tax_inclusive_amount
#DIAN 1.7.-2020: FAU08 # DIAN 1.7.-2020: FAU08
self.invoice_legal_monetary_total.allowance_total_amount = AmountCollection(self.invoice_allowance_charge)\ self.invoice_legal_monetary_total.allowance_total_amount =\
AmountCollection(self.invoice_allowance_charge)\
.filter(lambda charge: charge.isDiscount())\ .filter(lambda charge: charge.isDiscount())\
.map(lambda charge: charge.amount)\ .map(lambda charge: charge.amount)\
.sum() .sum()
#DIAN 1.7.-2020: FAU10 # DIAN 1.7.-2020: FAU10
self.invoice_legal_monetary_total.charge_total_amount = AmountCollection(self.invoice_allowance_charge)\ self.invoice_legal_monetary_total.charge_total_amount =\
AmountCollection(self.invoice_allowance_charge)\
.filter(lambda charge: charge.isCharge())\ .filter(lambda charge: charge.isCharge())\
.map(lambda charge: charge.amount)\ .map(lambda charge: charge.amount)\
.sum() .sum()
#DIAN 1.7.-2020: FAU12 # DIAN 1.7.-2020: FAU12
self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(self.invoice_prepaid_payment)\ self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(
.map(lambda paid: paid.paid_amount)\ self.invoice_prepaid_payment).map(
.sum() lambda paid: paid.paid_amount).sum()
#DIAN 1.7.-2020: FAU14 # DIAN 1.7.-2020: FAU14
self.invoice_legal_monetary_total.calculate() self.invoice_legal_monetary_total.calculate()
def _refresh_charges_base_amount(self): def _refresh_charges_base_amount(self):
@@ -745,11 +798,13 @@ class Invoice:
for invline in self.invoice_lines: for invline in self.invoice_lines:
if invline.allowance_charge: if invline.allowance_charge:
# TODO actualmente solo uno de los cargos es permitido # TODO actualmente solo uno de los cargos es permitido
raise ValueError('allowance charge in invoice exclude invoice line') raise ValueError(
'allowance charge in invoice exclude invoice line')
# cargos a nivel de factura # cargos a nivel de factura
for charge in self.invoice_allowance_charge: for charge in self.invoice_allowance_charge:
charge.set_base_amount(self.invoice_legal_monetary_total.line_extension_amount) charge.set_base_amount(
self.invoice_legal_monetary_total.line_extension_amount)
def calculate(self): def calculate(self):
for invline in self.invoice_lines: for invline in self.invoice_lines:
@@ -757,6 +812,7 @@ class Invoice:
self._calculate_legal_monetary_total() self._calculate_legal_monetary_total()
self._refresh_charges_base_amount() self._refresh_charges_base_amount()
class NationalSalesInvoice(Invoice): class NationalSalesInvoice(Invoice):
def __init__(self): def __init__(self):
super().__init__(NationalSalesInvoiceDocumentType()) super().__init__(NationalSalesInvoiceDocumentType())
@@ -801,11 +857,14 @@ class DebitNote(Invoice):
if not self.invoice_ident_prefix: if not self.invoice_ident_prefix:
self.invoice_ident_prefix = self.invoice_ident[0:6] self.invoice_ident_prefix = self.invoice_ident[0:6]
class SupportDocument(Invoice): class SupportDocument(Invoice):
pass pass
class SupportDocumentCreditNote(SupportDocument): class SupportDocumentCreditNote(SupportDocument):
def __init__(self, invoice_document_reference: BillingReference, def __init__(
self, invoice_document_reference: BillingReference,
invoice_discrepancy_response: BillingResponse): invoice_discrepancy_response: BillingResponse):
super().__init__(CreditNoteSupportDocumentType()) super().__init__(CreditNoteSupportDocumentType())

View File

@@ -2,6 +2,7 @@ from .invoice import *
from .credit_note import * from .credit_note import *
from .debit_note import * from .debit_note import *
from .utils import * from .utils import *
from .attached_document import *
from .support_document import * from .support_document import *
from .support_document_credit_note import * from .support_document_credit_note import *
from .attached_document import *
from .application_response import *

View File

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

View File

@@ -1,14 +1,227 @@
from .. import fe from .. import fe
from .application_response import ApplicationResponse
__all__ = ['AttachedDocument'] __all__ = ['AttachedDocument']
class AttachedDocument(): class AttachedDocument():
def __init__(self, id): def __init__(self, invoice, DIANInvoiceXML, id):
schema = 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2' self.schema =\
self.fexml = fe.FeXML('AttachedDocument', schema) 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2'
self.fexml.set_element('./cbc:ID', id) self.id = id
self.invoice = invoice
self.DIANInvoiceXML = DIANInvoiceXML
self.attached_document_invoice = self.attached_document_invoice()
def attached_document_invoice(self):
self.fexml = fe.FeXML(
'AttachedDocument', self.schema)
# DIAN 1.9.-2023: AE02
self.fexml.set_element(
'./cbc:UBLVersionID', 'UBL 2.1')
# DIAN 1.9.-2023: AE03
self.fexml.set_element(
'./cbc:CustomizationID', 'Documentos adjuntos')
# DIAN 1.9.-2023: AE04
self.fexml.set_element(
'./cbc:ProfileID', 'Factura Electrónica de Venta')
# DIAN 1.9.-2023: AE04a
self.fexml.set_element(
'./cbc:ProfileExecutionID', '1')
# DIAN 1.9.-2023: AE04b
self.fexml.set_element(
'./cbc:ID', self.id)
# DIAN 1.9.-2023: AE05
self.fexml.set_element(
'./cbc:IssueDate',
self.invoice.invoice_issue.strftime('%Y-%m-%d'))
# DIAN 1.9.-2023: AE06
self.fexml.set_element(
'./cbc:IssueTime', self.invoice.invoice_issue.strftime(
'%H:%M:%S-05:00'))
# DIAN 1.9.-2023: AE08
self.fexml.set_element(
'./cbc:DocumentType', 'Contenedor de Factura Electrónica')
# DIAN 1.9.-2023: AE08a
self.fexml.set_element(
'./cbc:ParentDocumentID', self.invoice.invoice_ident)
# DIAN 1.9.-2023: AE09
self.set_sender_party()
# DIAN 1.9.-2023: AE20
self.set_receiver_party()
# DIAN 1.9.-2023: AE33
self.set_attachment()
self.set_parent_document_line_reference()
def set_sender_party(self):
# DIAN 1.9.-2023: AE09
self.fexml.placeholder_for(
'./cac:SenderParty')
# DIAN 1.9.-2023: AE10
self.fexml.placeholder_for(
'./cac:SenderParty/cac:PartyTaxScheme')
# DIAN 1.9.-2023: AE11
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cbc:RegistrationName',
self.invoice.invoice_supplier.name)
# DIAN 1.9.-2023: AE12
# DIAN 1.9.-2023: AE13
# DIAN 1.9.-2023: AE14
# DIAN 1.9.-2023: AE15
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cbc:CompanyID',
self.invoice.invoice_supplier.ident,
schemeAgencyID='195',
schemeID=self.invoice.invoice_supplier.ident.dv,
schemeName=self.invoice.invoice_supplier.ident.type_fiscal)
# DIAN 1.9.-2023: AE16
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
self.invoice.invoice_supplier.responsability_code)
# DIAN 1.9.-2023: AE18
self.fexml.placeholder_for(
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme')
# DIAN 1.9.-2023: AE19
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
self.invoice.invoice_supplier.tax_scheme.code)
# DIAN 1.9.-2023: AE20
self.fexml.set_element(
'./cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
self.invoice.invoice_supplier.tax_scheme.name)
def set_receiver_party(self):
# DIAN 1.9.-2023: AE21
self.fexml.placeholder_for(
'./cac:ReceiverParty')
# DIAN 1.9.-2023: AE22
self.fexml.placeholder_for(
'./cac:ReceiverParty/cac:PartyTaxScheme')
# DIAN 1.9.-2023: AE23
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:RegistrationName',
self.invoice.invoice_customer.name)
# DIAN 1.9.-2023: AE24
# DIAN 1.9.-2023: AE25
# DIAN 1.9.-2023: AE26
# DIAN 1.9.-2023: AE27
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:CompanyID',
self.invoice.invoice_customer.ident,
schemeAgencyID='195',
schemeID=self.invoice.invoice_customer.ident.dv,
schemeName=self.invoice.invoice_customer.ident.type_fiscal)
# DIAN 1.9.-2023: AE28
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cbc:TaxLevelCode',
self.invoice.invoice_customer.responsability_code)
# DIAN 1.9.-2023: AE30
self.fexml.placeholder_for(
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme')
# DIAN 1.9.-2023: AE31
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
self.invoice.invoice_customer.tax_scheme.code)
# DIAN 1.9.-2023: AE32
self.fexml.set_element(
'./cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
self.invoice.invoice_customer.tax_scheme.name)
def set_attachment(self):
# DIAN 1.9.-2023: AE33
self.fexml.placeholder_for(
'./cac:Attachment')
# DIAN 1.9.-2023: AE34
self.fexml.placeholder_for(
'./cac:Attachment/cac:ExternalReference')
# DIAN 1.9.-2023: AE35
self.fexml.set_element(
'./cac:Attachment/cac:ExternalReference/cbc:MimeCode',
'text/xml')
# DIAN 1.9.-2023: AE36
self.fexml.set_element(
'./cac:Attachment/cac:ExternalReference/cbc:EncodingCode',
'UTF-8')
# DIAN 1.9.-2023: AE37
self.fexml.set_element(
'./cac:Attachment/cac:ExternalReference/cbc:Description',
self._build_attachment(self.DIANInvoiceXML)
)
def set_parent_document_line_reference(self):
self.fexml.placeholder_for(
'./cac:ParentDocumentLineReference')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cbc:LineID', 1)
self.fexml.placeholder_for(
'./cac:ParentDocumentLineReference/cac:DocumentReference')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:ID',
'1234')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:UUID',
'1234',
schemeName="CUFE-SHA384")
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:IssueDate',
'2024-11-28')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cbc:DocumentType',
'ApplicationResponse')
self.fexml.placeholder_for(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:MimeCode',
'text/xml')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:EncodingCode',
'UTF-8')
application_response = ApplicationResponse(
self.invoice).toFachoXML()
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:Description',
self._build_attachment(application_response))
self.fexml.placeholder_for(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidatorID',
'Unidad Especial Dirección de Impuestos y Aduanas Nacionales')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidationResultCode',
'02')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidationDate',
'2024-11-28')
self.fexml.set_element(
'./cac:ParentDocumentLineReference/cac:DocumentReference/cac:ResultOfVerification/cbc:ValidationTime',
'10:35:11-05:00')
def _build_attachment(self, DIANInvoiceXML):
document = (
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
) + DIANInvoiceXML.tostring()
attachment = "<![CDATA[{}]]>".format(
document)
return attachment
def toFachoXML(self): def toFachoXML(self):
return self.fexml return self.fexml

View File

@@ -1,9 +1,10 @@
from .. import fe # from .. import fe
from ..form import * # from ..form import *
from .invoice import DIANInvoiceXML from .invoice import DIANInvoiceXML
__all__ = ['DIANCreditNoteXML'] __all__ = ['DIANCreditNoteXML']
class DIANCreditNoteXML(DIANInvoiceXML): class DIANCreditNoteXML(DIANInvoiceXML):
""" """
DianInvoiceXML mapea objeto form.Invoice a XML segun DianInvoiceXML mapea objeto form.Invoice a XML segun

View File

@@ -1,9 +1,10 @@
from .. import fe # from .. import fe
from ..form import * # from ..form import *
from .invoice import DIANInvoiceXML from .invoice import DIANInvoiceXML
__all__ = ['DIANDebitNoteXML'] __all__ = ['DIANDebitNoteXML']
class DIANDebitNoteXML(DIANInvoiceXML): class DIANDebitNoteXML(DIANInvoiceXML):
""" """
DianInvoiceXML mapea objeto form.Invoice a XML segun DianInvoiceXML mapea objeto form.Invoice a XML segun
@@ -19,19 +20,24 @@ class DIANDebitNoteXML(DIANInvoiceXML):
def tag_document_concilied(fexml): def tag_document_concilied(fexml):
return 'Debited' return 'Debited'
#DIAN 1.7.-2020: DAU03 # DIAN 1.7.-2020: DAU03
def set_legal_monetary(fexml, invoice): def set_legal_monetary(fexml, invoice):
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:LineExtensionAmount', fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:LineExtensionAmount',
invoice.invoice_legal_monetary_total.line_extension_amount) invoice.invoice_legal_monetary_total.line_extension_amount)
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:TaxExclusiveAmount', fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:TaxExclusiveAmount',
invoice.invoice_legal_monetary_total.tax_exclusive_amount) invoice.invoice_legal_monetary_total.tax_exclusive_amount)
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:TaxInclusiveAmount', fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:TaxInclusiveAmount',
invoice.invoice_legal_monetary_total.tax_inclusive_amount) invoice.invoice_legal_monetary_total.tax_inclusive_amount)
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:ChargeTotalAmount', fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:ChargeTotalAmount',
invoice.invoice_legal_monetary_total.charge_total_amount) invoice.invoice_legal_monetary_total.charge_total_amount)
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:PayableAmount', fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:PayableAmount',
invoice.invoice_legal_monetary_total.payable_amount) invoice.invoice_legal_monetary_total.payable_amount)

View File

@@ -1,5 +1,7 @@
from .. import fe from .. import fe
from ..form import * from ..form import *
from collections import defaultdict
from .attached_document import AttachedDocument
__all__ = ['DIANInvoiceXML'] __all__ = ['DIANInvoiceXML']
@@ -9,7 +11,7 @@ class DIANInvoiceXML(fe.FeXML):
lo indicado para la facturacion electronica. lo indicado para la facturacion electronica.
""" """
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')
self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl') self.placeholder_for('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts: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:InvoiceSource')
@@ -20,343 +22,467 @@ class DIANInvoiceXML(fe.FeXML):
# ZE02 se requiere existencia para firmar # ZE02 se requiere existencia para firmar
ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True) ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True)
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice(invoice) self.attach_invoice = self.attach_invoice(invoice)
# self.attach_document = self.attached_document_invoice(attach_invoice)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None"""
fexml.placeholder_for('./ext:UBLExtensions')
fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1')
fexml.set_element(
'./cbc:CustomizationID', invoice.invoice_operation_type)
fexml.placeholder_for('./cbc:ProfileID')
fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
# DIAN 1.7.-2020: FAD10
fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00'))
fexml.set_element(
'./cbc:%sTypeCode' % (fexml.tag_document()),
invoice.invoice_type_code,
listAgencyID='195',
listAgencyName='No matching global declaration available for the validation root',
listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
if fexml.tag_document() == 'Invoice':
fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (
fexml.tag_document()),
invoice.invoice_period_start.strftime(
'%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (
fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_billing_reference(invoice)
fexml.customize(invoice)
fexml.set_supplier(invoice)
fexml.set_customer(invoice)
fexml.set_payment_mean(invoice)
fexml.set_invoice_totals(invoice)
fexml.set_legal_monetary(invoice)
fexml.set_invoice_lines(invoice)
fexml.set_allowance_charge(invoice)
return fexml
def attached_document_invoice(fexml, invoice):
attach_invoice = fexml.attach_invoice(invoice)
attached_document = AttachedDocument(
invoice, '123', attach_invoice)
return attached_document
def set_supplier(fexml, invoice): def set_supplier(fexml, invoice):
fexml.placeholder_for('./cac:AccountingSupplierParty') fexml.placeholder_for('./cac:AccountingSupplierParty')
#DIAN 1.7.-2020: CAJ02 # DIAN 1.7.-2020: CAJ02
#DIAN 1.7.-2020: FAJ02 # DIAN 1.7.-2020: FAJ02
fexml.set_element('./cac:AccountingSupplierParty/cbc:AdditionalAccountID', fexml.set_element(
'./cac:AccountingSupplierParty/cbc:AdditionalAccountID',
invoice.invoice_supplier.organization_code) invoice.invoice_supplier.organization_code)
#DIAN 1.7.-2020: CAJ06 # DIAN 1.7.-2020: CAJ06
#DIAN 1.7.-2020: FAJ06 # DIAN 1.7.-2020: FAJ06
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name', fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name',
invoice.invoice_supplier.name) invoice.invoice_supplier.name)
#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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address') fexml.placeholder_for(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode', fexml.set_element(
'./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('./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.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({'schemeID': invoice.invoice_supplier.ident.dv, supplier_company_id_attrs.update({
'schemeName': invoice.invoice_supplier.ident.type_fiscal}) 'schemeID': invoice.invoice_supplier.ident.dv,
'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
fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme') fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme')
#DIAN 1.7.-2020: FAJ20 # DIAN 1.7.-2020: FAJ20
#DIAN 1.7.-2020: CAJ20 # DIAN 1.7.-2020: CAJ20
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode', fexml.set_element(
#DIAN 1.7.-2020: FAJ26 './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
#DIAN 1.7.-2020: CAJ26 # DIAN 1.7.-2020: FAJ26
# DIAN 1.7.-2020: CAJ26
invoice.invoice_supplier.responsability_code, invoice.invoice_supplier.responsability_code,
#DIAN 1.7.-2020: FAJ27 # DIAN 1.7.-2020: FAJ27
#DIAN 1.7.-2020: CAJ27 # DIAN 1.7.-2020: CAJ27
listName=invoice.invoice_supplier.responsability_regime_code) listName=invoice.invoice_supplier.responsability_regime_code)
#DIAN 1.7.-2020: FAJ28 # DIAN 1.7.-2020: FAJ28
#DIAN 1.7.-2020: CAJ28 # DIAN 1.7.-2020: CAJ28
fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress') fexml.placeholder_for(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName', invoice.invoice_supplier.address.city.name) fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme') fexml.placeholder_for(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', fexml.set_element(
'./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
#DIAN 1.7.-2020: CAJ42 # DIAN 1.7.-2020: CAJ42
fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity') fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity')
#DIAN 1.7.-2020: FAJ43 # DIAN 1.7.-2020: FAJ43
#DIAN 1.7.-2020: CAJ43 # DIAN 1.7.-2020: CAJ43
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme') fexml.placeholder_for(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme/cbc:ID', fexml.set_element(
'./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
fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:Contact') fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:Contact')
#DIAN 1.7.-2020: FAJ71 # DIAN 1.7.-2020: FAJ71
#DIAN 1.7.-2020: CAJ71 # DIAN 1.7.-2020: CAJ71
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicMail', fexml.set_element(
'./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):
fexml.placeholder_for('./cac:AccountingCustomerParty') fexml.placeholder_for('./cac:AccountingCustomerParty')
fexml.set_element('./cac:AccountingCustomerParty/cbc:AdditionalAccountID', fexml.set_element(
'./cac:AccountingCustomerParty/cbc:AdditionalAccountID',
invoice.invoice_customer.organization_code) invoice.invoice_customer.organization_code)
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID', fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID',
invoice.invoice_customer.ident) invoice.invoice_customer.ident)
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name', fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name',
invoice.invoice_customer.name) invoice.invoice_customer.name)
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation') fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation')
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({'schemeID': invoice.invoice_customer.ident.dv, customer_company_id_attrs.update({
'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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address') fexml.placeholder_for(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', invoice.invoice_customer.address.city.name) fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name', fexml.set_element(
'./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
languageID='es') languageID='es')
#DIAN 1.7.-2020: FAK17,FAK19 # DIAN 1.7.-2020: FAK17,FAK19
#DIAN 1.7.-2020: CAK19 # DIAN 1.7.-2020: CAK19
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme') fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme')
#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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode', fexml.set_element(
#DIAN 1.7.-2020: FAK26 './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
# 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
#DIAN 1.7.-2020: CAK27 # DIAN 1.7.-2020: CAK27
listName=invoice.invoice_customer.responsability_regime_code) listName=invoice.invoice_customer.responsability_regime_code)
#DIAN 1.7.-2020: FAK28 # DIAN 1.7.-2020: FAK28
#DIAN 1.7.-2020: CAK28 # DIAN 1.7.-2020: CAK28
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress') fexml.placeholder_for(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine') fexml.placeholder_for(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country') fexml.placeholder_for(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name', invoice.invoice_customer.address.country.name) fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme') fexml.placeholder_for(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', fexml.set_element(
'./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: CAK42
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
#DIAN 1.7.-2020: FAK43 # DIAN 1.7.-2020: FAK42
#DIAN 1.7.-2020: CAK43 # DIAN 1.7.-2020: CAK42
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName', fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
# DIAN 1.7.-2020: FAK43
# DIAN 1.7.-2020: CAK43
fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID', fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
**customer_company_id_attrs) **customer_company_id_attrs)
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity') fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
#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('./cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicMail', fexml.set_element(
'./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:PaymentMeansCode', payment_mean.code) './cac:PaymentMeans/cbc:ID', payment_mean.id)
fexml.set_element('./cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d')) fexml.set_element(
fexml.set_element('./cac:PaymentMeans/cbc:PaymentID', payment_mean.payment_id) './cac:PaymentMeans/cbc:PaymentMeansCode', payment_mean.code)
fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d'))
fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentID', payment_mean.payment_id)
def set_element_amount_for(fexml, xml, xpath, amount): def set_element_amount_for(fexml, xml, xpath, amount):
if not isinstance(amount, Amount): if not isinstance(amount, Amount):
@@ -504,37 +630,44 @@ class DIANInvoiceXML(fe.FeXML):
cod_impuesto, amount_of = item cod_impuesto, amount_of = item
next_append = index > 0 next_append = index > 0
#DIAN 1.7.-2020: FAS01 # DIAN 1.7.-2020: FAS01
line = fexml.fragment('./cac:WithholdingTaxTotal', append=next_append) line = fexml.fragment(
#DIAN 1.7.-2020: FAU06 './cac:WithholdingTaxTotal', append=next_append)
# DIAN 1.7.-2020: FAU06
tax_amount = amount_of['tax_amount'] tax_amount = amount_of['tax_amount']
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
line,
'/cac:WithholdingTaxTotal/cbc:TaxAmount', '/cac:WithholdingTaxTotal/cbc:TaxAmount',
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:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', '/cac:WithholdingTaxTotal/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:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
amount_of['tax_amount']) amount_of['tax_amount'])
#DIAN 1.7.-2020: FAS07 # DIAN 1.7.-2020: FAS07
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:Percent', line.set_element(
'/cac:WithholdingTaxTotal/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:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
cod_impuesto) cod_impuesto)
line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', line.set_element(
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
'ReteRenta') 'ReteRenta')
# abstract method # abstract method
@@ -546,140 +679,141 @@ class DIANInvoiceXML(fe.FeXML):
return 'Invoiced' return 'Invoiced'
def set_invoice_line_tax(fexml, line, invoice_line): def set_invoice_line_tax(fexml, line, invoice_line):
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
line,
'./cac:TaxTotal/cbc:TaxAmount', './cac:TaxTotal/cbc:TaxAmount',
invoice_line.tax_amount) invoice_line.tax_amount)
#DIAN 1.7.-2020: FAX05 # DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
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('./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP') line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
subtotal.tax_amount,
currencyID='COP')
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2)) line.set_element(
'./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('./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:ID', subtotal.scheme.code)
line.set_element(
'./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(line, fexml.set_element_amount_for(
line,
'./cac:WithholdingTaxTotal/cbc:TaxAmount', './cac:WithholdingTaxTotal/cbc:TaxAmount',
invoice_line.withholding_amount) invoice_line.withholding_amount)
#DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(line, # DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(
line,
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', './cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.withholding_taxable_amount) invoice_line.withholding_taxable_amount)
for subtotal in invoice_line.withholding.subtotals: for subtotal in invoice_line.withholding.subtotals:
line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP') line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
subtotal.tax_amount,
currencyID='COP')
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2)) line.set_element(
'./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('./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:ID', subtotal.scheme.code)
line.set_element(
'./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('./cac:%sLine' % (fexml.tag_document()), append=next_append) line = fexml.fragment(
'./cac:%sLine' % (fexml.tag_document()),
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('./cbc:%sQuantity' % (fexml.tag_document_concilied()), invoice_line.quantity, unitCode = 'NAR') line.set_element(
fexml.set_element_amount_for(line, './cbc:%sQuantity' % (
fexml.tag_document_concilied()),
invoice_line.quantity,
unitCode='NAR')
fexml.set_element_amount_for(
line,
'./cbc:LineExtensionAmount', './cbc:LineExtensionAmount',
invoice_line.total_amount) invoice_line.total_amount)
if not isinstance(invoice_line.tax, TaxTotalOmit): if not isinstance(
invoice_line.tax, TaxTotalOmit):
fexml.set_invoice_line_tax(line, invoice_line) fexml.set_invoice_line_tax(line, invoice_line)
if not isinstance(invoice_line.withholding, WithholdingTaxTotalOmit): if not isinstance(
invoice_line.withholding, WithholdingTaxTotalOmit):
fexml.set_invoice_line_withholding(line, invoice_line) fexml.set_invoice_line_withholding(line, invoice_line)
line.set_element('./cac:Item/cbc:Description', invoice_line.item.description) line.set_element(
'./cac:Item/cbc:Description', invoice_line.item.description)
line.set_element('./cac:Item/cac:StandardItemIdentification/cbc:ID', line.set_element(
'./cac:Item/cac:StandardItemIdentification/cbc:ID',
invoice_line.item.id, invoice_line.item.id,
schemeID=invoice_line.item.scheme_id, schemeID=invoice_line.item.scheme_id,
schemeName=invoice_line.item.scheme_name, schemeName=invoice_line.item.scheme_name,
schemeAgencyID=invoice_line.item.scheme_agency_id) schemeAgencyID=invoice_line.item.scheme_agency_id)
line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code) line.set_element(
#DIAN 1.7.-2020: FBB04 './cac:Price/cbc:PriceAmount',
line.set_element('./cac:Price/cbc:BaseQuantity', invoice_line.price.amount,
currencyID=invoice_line.price.amount.currency.code)
# DIAN 1.7.-2020: FBB04
line.set_element(
'./cac:Price/cbc:BaseQuantity',
invoice_line.price.quantity, invoice_line.price.quantity,
unitCode=invoice_line.quantity.code) unitCode=invoice_line.quantity.code)
for idx, charge in enumerate(invoice_line.allowance_charge): for idx, charge in enumerate(invoice_line.allowance_charge):
next_append_charge = idx > 0 next_append_charge = idx > 0
fexml.append_allowance_charge(line, index + 1, charge, append=next_append_charge) fexml.append_allowance_charge(
line, index + 1, charge, append=next_append_charge)
def set_allowance_charge(fexml, invoice): def set_allowance_charge(fexml, invoice):
for idx, charge in enumerate(invoice.invoice_allowance_charge): for idx, charge in enumerate(invoice.invoice_allowance_charge):
next_append = idx > 0 next_append = idx > 0
fexml.append_allowance_charge(fexml, idx + 1, charge, append=next_append) fexml.append_allowance_charge(
fexml, idx + 1, charge, append=next_append)
def append_allowance_charge(fexml, parent, idx, charge, append=False): def append_allowance_charge(fexml, parent, idx, charge, append=False):
line = parent.fragment('./cac:AllowanceCharge', append=append) line = parent.fragment('./cac:AllowanceCharge', append=append)
#DIAN 1.7.-2020: FAQ02 # DIAN 1.7.-2020: FAQ02
line.set_element('./cbc:ID', idx) line.set_element('./cbc:ID', idx)
#DIAN 1.7.-2020: FAQ03 # DIAN 1.7.-2020: FAQ03
line.set_element('./cbc:ChargeIndicator', str(charge.charge_indicator).lower()) line.set_element('./cbc:ChargeIndicator', str(
charge.charge_indicator).lower())
if charge.reason: if charge.reason:
line.set_element('./cbc:AllowanceChargeReasonCode', charge.reason.code) line.set_element(
line.set_element('./cbc:allowanceChargeReason', charge.reason.reason) './cbc:AllowanceChargeReasonCode', charge.reason.code)
line.set_element('./cbc:MultiplierFactorNumeric', str(round(charge.multiplier_factor_numeric, 2))) line.set_element(
fexml.set_element_amount_for(line, './cbc:Amount', charge.amount) './cbc:allowanceChargeReason', charge.reason.reason)
fexml.set_element_amount_for(line, './cbc:BaseAmount', charge.base_amount) line.set_element(
'./cbc:MultiplierFactorNumeric', str(
def attach_invoice(fexml, invoice): round(charge.multiplier_factor_numeric, 2)))
"""adiciona etiquetas a FEXML y retorna FEXML fexml.set_element_amount_for(
en caso de fallar validacion retorna None""" line, './cbc:Amount', charge.amount)
fexml.set_element_amount_for(
fexml.placeholder_for('./ext:UBLExtensions') line, './cbc:BaseAmount', charge.base_amount)
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

View File

@@ -1,219 +1,263 @@
from .. import fe from .. import fe
from ..form import * from ..form import (
from datetime import datetime, date Amount, DebitNoteDocumentReference, CreditNoteDocumentReference,
from .attached_document import * InvoiceDocumentReference, TaxTotalOmit, WithholdingTaxTotalOmit
)
from collections import defaultdict
from datetime import datetime
# from .attached_document import *
__all__ = ['DIANSupportDocumentXML'] __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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl') self.placeholder_for(
'./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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource') self.placeholder_for(
'./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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') self.placeholder_for(
'./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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode') self.placeholder_for(
'./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('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') 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
#DIAN 1.1.-2021: DSAA02 DSAB01 # DIAN 1.1.-2021: DSAA02 DSAB01
#DIAN 1.1.-2021: NSAA02 NSAB01 # DIAN 1.1.-2021: NSAA02 NSAB01
ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True) ublextension = self.fragment(
#DIAN 1.1.-2021: DSAB02 './ext:UBLExtensions/ext:UBLExtension', append=True)
#DIAN 1.1.-2021: NSAB02 # DIAN 1.1.-2021: DSAB02
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') # DIAN 1.1.-2021: NSAB02
extcontent = ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice(invoice) self.attach_invoice(invoice)
def set_supplier(fexml, invoice): def set_supplier(fexml, invoice):
#DIAN 1.1.-2021: DSAJ01 # DIAN 1.1.-2021: DSAJ01
#DIAN 1.1.-2021: NSAB01 # DIAN 1.1.-2021: NSAB01
fexml.placeholder_for('./cac:AccountingSupplierParty') fexml.placeholder_for('./cac:AccountingSupplierParty')
#DIAN 1.1.-2021: DSAJ02 # DIAN 1.1.-2021: DSAJ02
#DIAN 1.1.-2021: NSAJ02 # DIAN 1.1.-2021: NSAJ02
fexml.set_element('./cac:AccountingSupplierParty/cbc:AdditionalAccountID', fexml.set_element(
'./cac:AccountingSupplierParty/cbc:AdditionalAccountID',
invoice.invoice_supplier.organization_code) invoice.invoice_supplier.organization_code)
#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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address') fexml.placeholder_for(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:PostalZone', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode', fexml.set_element(
'./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
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({'schemeID': invoice.invoice_supplier.ident.dv, supplier_company_id_attrs.update(
{
'schemeID': invoice.invoice_supplier.ident.dv,
'schemeName': invoice.invoice_supplier.ident.type_fiscal}) 'schemeName': invoice.invoice_supplier.ident.type_fiscal})
#DIAN 1.1.-2021: DSAJ19 # DIAN 1.1.-2021: DSAJ19
#DIAN 1.1.-2021: NSAJ19 # DIAN 1.1.-2021: NSAJ19
fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme') fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme')
#DIAN 1.1.-2021: DSAJ20 # DIAN 1.1.-2021: DSAJ20
#DIAN 1.1.-2021: NSAJ20 # DIAN 1.1.-2021: NSAJ20
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', fexml.set_element(
'./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
**supplier_company_id_attrs) **supplier_company_id_attrs)
#DIAN 1.1.-2021: DSAJ26 # DIAN 1.1.-2021: DSAJ26
#DIAN 1.1.-2021: NSAJ26 # DIAN 1.1.-2021: NSAJ26
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme') fexml.placeholder_for(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', fexml.set_element(
'./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('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', fexml.set_element(
'./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):
#DIAN 1.1.-2021: DSAK01 # DIAN 1.1.-2021: DSAK01
#DIAN 1.1.-2021: NSAK01 # DIAN 1.1.-2021: NSAK01
fexml.placeholder_for('./cac:AccountingCustomerParty') fexml.placeholder_for('./cac:AccountingCustomerParty')
#DIAN 1.1.-2021: DSAK02 # DIAN 1.1.-2021: DSAK02
#DIAN 1.1.-2021: NSAK02 # DIAN 1.1.-2021: NSAK02
fexml.set_element('./cac:AccountingCustomerParty/cbc:AdditionalAccountID', fexml.set_element(
'./cac:AccountingCustomerParty/cbc:AdditionalAccountID',
invoice.invoice_customer.organization_code) invoice.invoice_customer.organization_code)
#DIAN 1.1.-2021: DSAK03 # DIAN 1.1.-2021: DSAK03
#DIAN 1.1.-2021: NSAK03 # DIAN 1.1.-2021: NSAK03
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party') fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party')
#DIAN 1.1.-2021: DSAK19 # DIAN 1.1.-2021: DSAK19
#DIAN 1.1.-2021: NSAK19 # DIAN 1.1.-2021: NSAK19
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme') fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme')
#DIAN 1.1.-2021: DSAK20 # DIAN 1.1.-2021: DSAK20
#DIAN 1.1.-2021: NSAK20 # DIAN 1.1.-2021: NSAK20
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName', fexml.set_element(
'./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()
customer_company_id_attrs.update({'schemeID': invoice.invoice_customer.ident.dv, customer_company_id_attrs.update(
{
'schemeID': invoice.invoice_customer.ident.dv,
'schemeName': invoice.invoice_customer.ident.type_fiscal}) 'schemeName': invoice.invoice_customer.ident.type_fiscal})
#DIAN 1.1.-2021: DSAK21 # DIAN 1.1.-2021: DSAK21
#DIAN 1.1.-2021: NSAK21 # DIAN 1.1.-2021: NSAK21
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', fexml.set_element(
'./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
**customer_company_id_attrs) **customer_company_id_attrs)
#DIAN 1.1.-2021: DSAK26 # DIAN 1.1.-2021: DSAK26
#DIAN 1.1.-2021: NSAK26 # DIAN 1.1.-2021: NSAK26
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme') fexml.placeholder_for(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', fexml.set_element(
'./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('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', fexml.set_element(
'./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):
payment_mean = invoice.invoice_payment_mean payment_mean = invoice.invoice_payment_mean
#DIAN 1.1.-2021: DSAN01 DSAN02 # DIAN 1.1.-2021: DSAN01 DSAN02
#DIAN 1.1.-2021: NSAN02 NSAN02 # DIAN 1.1.-2021: NSAN02 NSAN02
fexml.set_element('./cac:PaymentMeans/cbc:ID', payment_mean.id) fexml.set_element('./cac:PaymentMeans/cbc:ID', payment_mean.id)
#DIAN 1.1.-2021: DSAN03 # DIAN 1.1.-2021: DSAN03
#DIAN 1.1.-2021: NSAN03 # DIAN 1.1.-2021: NSAN03
fexml.set_element('./cac:PaymentMeans/cbc:PaymentMeansCode', payment_mean.code) fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentMeansCode',
payment_mean.code)
#DIAN 1.1.-2021: DSAN04 # DIAN 1.1.-2021: DSAN04
#DIAN 1.1.-2021: NSAN04 # DIAN 1.1.-2021: NSAN04
fexml.set_element('./cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d')) fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentDueDate',
payment_mean.due_at.strftime('%Y-%m-%d'))
#DIAN 1.1.-2021: DSAN05 # DIAN 1.1.-2021: DSAN05
#DIAN 1.1.-2021: NSAN05 # DIAN 1.1.-2021: NSAN05
fexml.set_element('./cac:PaymentMeans/cbc:PaymentID', payment_mean.payment_id) fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentID',
payment_mean.payment_id)
def set_element_amount_for(fexml, xml, xpath, amount): def set_element_amount_for(fexml, xml, xpath, amount):
if not isinstance(amount, Amount): if not isinstance(amount, Amount):
@@ -228,40 +272,47 @@ class DIANSupportDocumentXML(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):
#DIAN 1.1.-2021: DSAU01 DSAU02 DSAU03 # DIAN 1.1.-2021: DSAU01 DSAU02 DSAU03
#DIAN 1.1.-2021: NSAU01 NSAU02 NSAU03 # DIAN 1.1.-2021: NSAU01 NSAU02 NSAU03
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)
#DIAN 1.1.-2021: DSAU04 DSAU05 # DIAN 1.1.-2021: DSAU04 DSAU05
#DIAN 1.1.-2021: NSAU04 NSAU05 # DIAN 1.1.-2021: NSAU04 NSAU05
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)
#DIAN 1.1.-2021: DSAU06 DSAU07 # DIAN 1.1.-2021: DSAU06 DSAU07
#DIAN 1.1.-2021: NSAU06 DSAU07 # DIAN 1.1.-2021: NSAU06 DSAU07
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)
#DIAN 1.1.-2021: DSAU10 DSAU11 # DIAN 1.1.-2021: DSAU10 DSAU11
#DIAN 1.1.-2021: NSAU10 DSAU11 # DIAN 1.1.-2021: NSAU10 DSAU11
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)
#DIAN 1.1.-2021: DSAU14 DSAU15 # DIAN 1.1.-2021: DSAU14 DSAU15
#DIAN 1.1.-2021: NSAU14 DSAU15 # DIAN 1.1.-2021: NSAU14 DSAU15
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):
@@ -270,12 +321,14 @@ class DIANSupportDocumentXML(fe.FeXML):
else: else:
schemeName = 'CUDS-SHA384' schemeName = 'CUDS-SHA384'
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=schemeName) schemeName=schemeName)
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):
@@ -319,60 +372,64 @@ class DIANSupportDocumentXML(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 tax_amount_for[
tax_amount_for[subtotal.scheme.code]['taxable_amount'] += invoice_line.taxable_amount subtotal.scheme.code][
'tax_amount'] += subtotal.tax_amount
tax_amount_for[subtotal.scheme.code][
'taxable_amount'] += invoice_line.taxable_amount
# 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
total_tax_amount += subtotal.tax_amount total_tax_amount += subtotal.tax_amount
if total_tax_amount != Amount(0.0): if total_tax_amount != Amount(0.0):
fexml.placeholder_for('./cac:TaxTotal') fexml.placeholder_for('./cac:TaxTotal')
fexml.set_element_amount('./cac:TaxTotal/cbc:TaxAmount', fexml.set_element_amount('./cac:TaxTotal/cbc:TaxAmount',
total_tax_amount) total_tax_amount)
for index, item in enumerate(tax_amount_for.items()): for index, item in enumerate(tax_amount_for.items()):
cod_impuesto, amount_of = item cod_impuesto, amount_of = item
next_append = index > 0 next_append = index > 0
#DIAN 1.7.-2020: FAS01 # DIAN 1.7.-2020: FAS01
line = fexml.fragment('./cac:TaxTotal', append=next_append) line = fexml.fragment('./cac:TaxTotal', 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(line, fexml.set_element_amount_for(line,
'/cac:TaxTotal/cbc:TaxAmount', '/cac:TaxTotal/cbc:TaxAmount',
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'])
#DIAN 1.7.-2020: FAS07 # DIAN 1.7.-2020: FAS07
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
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(
'IVA') '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', 'IVA')
# abstract method # abstract method
def tag_document(fexml): def tag_document(fexml):
return 'Invoice' return 'Invoice'
@@ -384,50 +441,83 @@ class DIANSupportDocumentXML(fe.FeXML):
fexml.set_element_amount_for(line, fexml.set_element_amount_for(line,
'./cac:WithholdingTaxTotal/cbc:TaxAmount', './cac:WithholdingTaxTotal/cbc:TaxAmount',
invoice_line.withholding_amount) invoice_line.withholding_amount)
#DIAN 1.7.-2020: FAX05 # DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
line,
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', './cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.withholding_taxable_amount) invoice_line.withholding_taxable_amount)
for subtotal in invoice_line.withholding.subtotals: for subtotal in invoice_line.withholding.subtotals:
line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP') line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
subtotal.tax_amount,
currencyID='COP')
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2)) line.set_element(
'./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('./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:ID',
subtotal.scheme.code)
line.set_element(
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
subtotal.scheme.name)
def set_invoice_line_tax(fexml, line, invoice_line): def set_invoice_line_tax(fexml, line, invoice_line):
fexml.set_element_amount_for(line, fexml.set_element_amount_for(line,
'./cac:TaxTotal/cbc:TaxAmount', './cac:TaxTotal/cbc:TaxAmount',
invoice_line.tax_amount) invoice_line.tax_amount)
#DIAN 1.7.-2020: FAX05 # DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(line, fexml.set_element_amount_for(
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('./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP') line.set_element(
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
subtotal.tax_amount,
currencyID='COP')
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2)) line.set_element(
'./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('./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:ID',
subtotal.scheme.code)
line.set_element(
'./cac:TaxTotal/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('./cac:%sLine' % (fexml.tag_document()), append=next_append) line = fexml.fragment(
'./cac:%sLine' %
(fexml.tag_document()),
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('./cbc:%sQuantity' % (fexml.tag_document_concilied()), invoice_line.quantity, unitCode = 'NAR') line.set_element(
'./cbc:%sQuantity' %
(fexml.tag_document_concilied()),
invoice_line.quantity,
unitCode='NAR')
fexml.set_element_amount_for(line, fexml.set_element_amount_for(line,
'./cbc:LineExtensionAmount', './cbc:LineExtensionAmount',
invoice_line.total_amount) invoice_line.total_amount)
@@ -435,53 +525,69 @@ class DIANSupportDocumentXML(fe.FeXML):
period = line.fragment('./cac:InvoicePeriod') period = line.fragment('./cac:InvoicePeriod')
period.set_element('./cbc:StartDate', period.set_element('./cbc:StartDate',
datetime.now().strftime('%Y-%m-%d')) datetime.now().strftime('%Y-%m-%d'))
period.set_element('./cbc:DescriptionCode', period.set_element(
'1') './cbc:DescriptionCode', '1')
period.set_element('./cbc:Description', period.set_element('./cbc:Description',
'Por operación') 'Por operación')
if not isinstance(invoice_line.tax, TaxTotalOmit): if not isinstance(invoice_line.tax, TaxTotalOmit):
fexml.set_invoice_line_tax(line, invoice_line) fexml.set_invoice_line_tax(line, invoice_line)
if not isinstance(invoice_line.withholding, WithholdingTaxTotalOmit): if not isinstance(
invoice_line.withholding,
WithholdingTaxTotalOmit):
fexml.set_invoice_line_withholding(line, invoice_line) fexml.set_invoice_line_withholding(line, invoice_line)
line.set_element(
'./cac:Item/cbc:Description',
invoice_line.item.description)
line.set_element('./cac:Item/cbc:Description', invoice_line.item.description) line.set_element(
'./cac:Item/cac:StandardItemIdentification/cbc:ID',
line.set_element('./cac:Item/cac:StandardItemIdentification/cbc:ID',
invoice_line.item.id, invoice_line.item.id,
schemeID=invoice_line.item.scheme_id, schemeID=invoice_line.item.scheme_id,
schemeName=invoice_line.item.scheme_name, schemeName=invoice_line.item.scheme_name,
schemeAgencyID=invoice_line.item.scheme_agency_id) schemeAgencyID=invoice_line.item.scheme_agency_id)
line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code) line.set_element(
#DIAN 1.7.-2020: FBB04 './cac:Price/cbc:PriceAmount',
invoice_line.price.amount,
currencyID=invoice_line.price.amount.currency.code)
# DIAN 1.7.-2020: FBB04
line.set_element('./cac:Price/cbc:BaseQuantity', line.set_element('./cac:Price/cbc:BaseQuantity',
invoice_line.quantity, invoice_line.quantity,
unitCode=invoice_line.quantity.code) unitCode=invoice_line.quantity.code)
for idx, charge in enumerate(invoice_line.allowance_charge): for idx, charge in enumerate(invoice_line.allowance_charge):
next_append_charge = idx > 0 next_append_charge = idx > 0
fexml.append_allowance_charge(line, index + 1, charge, append=next_append_charge) fexml.append_allowance_charge(
line, index + 1, charge, append=next_append_charge)
def set_allowance_charge(fexml, invoice): def set_allowance_charge(fexml, invoice):
for idx, charge in enumerate(invoice.invoice_allowance_charge): for idx, charge in enumerate(invoice.invoice_allowance_charge):
next_append = idx > 0 next_append = idx > 0
fexml.append_allowance_charge(fexml, idx + 1, charge, append=next_append) fexml.append_allowance_charge(
fexml, idx + 1, charge, append=next_append)
def append_allowance_charge(fexml, parent, idx, charge, append=False): def append_allowance_charge(fexml, parent, idx, charge, append=False):
line = parent.fragment('./cac:AllowanceCharge', append=append) line = parent.fragment('./cac:AllowanceCharge', append=append)
#DIAN 1.7.-2020: FAQ02 # DIAN 1.7.-2020: FAQ02
line.set_element('./cbc:ID', idx) line.set_element('./cbc:ID', idx)
#DIAN 1.7.-2020: FAQ03 # DIAN 1.7.-2020: FAQ03
line.set_element('./cbc:ChargeIndicator', str(charge.charge_indicator).lower()) line.set_element('./cbc:ChargeIndicator',
str(charge.charge_indicator).lower())
if charge.reason: if charge.reason:
line.set_element('./cbc:AllowanceChargeReasonCode', charge.reason.code) line.set_element(
line.set_element('./cbc:allowanceChargeReason', charge.reason.reason) './cbc:AllowanceChargeReasonCode',
line.set_element('./cbc:MultiplierFactorNumeric', str(round(charge.multiplier_factor_numeric, 2))) charge.reason.code)
line.set_element(
'./cbc:allowanceChargeReason',
charge.reason.reason)
line.set_element('./cbc:MultiplierFactorNumeric',
str(round(charge.multiplier_factor_numeric, 2)))
fexml.set_element_amount_for(line, './cbc:Amount', charge.amount) fexml.set_element_amount_for(line, './cbc:Amount', charge.amount)
fexml.set_element_amount_for(line, './cbc:BaseAmount', charge.base_amount) fexml.set_element_amount_for(
line, './cbc:BaseAmount', charge.base_amount)
def attach_invoice(fexml, invoice): def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML """adiciona etiquetas a FEXML y retorna FEXML
@@ -489,25 +595,37 @@ class DIANSupportDocumentXML(fe.FeXML):
fexml.placeholder_for('./ext:UBLExtensions') fexml.placeholder_for('./ext:UBLExtensions')
fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1') fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1')
fexml.set_element('./cbc:CustomizationID', invoice.invoice_operation_type) fexml.set_element(
'./cbc:CustomizationID',
invoice.invoice_operation_type)
fexml.placeholder_for('./cbc:ProfileID') fexml.placeholder_for('./cbc:ProfileID')
fexml.placeholder_for('./cbc:ProfileExecutionID') fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident) fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID') fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP') fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d')) fexml.set_element(
#DIAN 1.7.-2020: FAD10 './cbc:IssueDate',
fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00')) invoice.invoice_issue.strftime('%Y-%m-%d'))
fexml.set_element('./cbc:%sTypeCode' % (fexml.tag_document()), # 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, 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('./cac:%sPeriod/cbc:StartDate' % (fexml.tag_document()), fexml.set_element(
'./cac:%sPeriod/cbc:StartDate' %
(fexml.tag_document()),
invoice.invoice_period_start.strftime('%Y-%m-%d')) invoice.invoice_period_start.strftime('%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (fexml.tag_document()), fexml.set_element(
'./cac:%sPeriod/cbc:EndDate' %
(fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d')) invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.customize(invoice) fexml.customize(invoice)

View File

@@ -1,9 +1,10 @@
from .. import fe # from .. import fe
from ..form import * # from ..form import *
from .support_document import DIANSupportDocumentXML from .support_document import DIANSupportDocumentXML
__all__ = ['DIANSupportDocumentCreditNoteXML'] __all__ = ['DIANSupportDocumentCreditNoteXML']
class DIANSupportDocumentCreditNoteXML(DIANSupportDocumentXML): class DIANSupportDocumentCreditNoteXML(DIANSupportDocumentXML):
""" """
DianInvoiceXML mapea objeto form.Invoice a XML segun DianInvoiceXML mapea objeto form.Invoice a XML segun
@@ -11,11 +12,14 @@ class DIANSupportDocumentCreditNoteXML(DIANSupportDocumentXML):
""" """
def __init__(self, invoice): def __init__(self, invoice):
super(DIANSupportDocumentCreditNoteXML, self).__init__(invoice, 'CreditNote') super(
DIANSupportDocumentCreditNoteXML,
self).__init__(
invoice,
'CreditNote')
def tag_document(fexml): def tag_document(fexml):
return 'CreditNote' return 'CreditNote'
def tag_document_concilied(fexml): def tag_document_concilied(fexml):
return 'Credited' return 'Credited'

View File

@@ -2,18 +2,30 @@ from .. import fe
__all__ = ['DIANWrite', 'DIANWriteSigned'] __all__ = ['DIANWrite', 'DIANWriteSigned']
def DIANWrite(xml, filename): def DIANWrite(xml, filename):
document = xml.tostring(xml_declaration=True, encoding='UTF-8') document = xml.tostring(xml_declaration=True, encoding='UTF-8')
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write(document) f.write(document)
def DIANWriteSigned(xml, filename, private_key, passphrase, use_cache_policy=False, dian_signer=None): def DIANWriteSigned(
document = xml.tostring(xml_declaration=True, encoding='UTF-8').encode('utf-8') xml,
filename,
private_key,
passphrase,
use_cache_policy=False,
dian_signer=None):
document = xml.tostring(
xml_declaration=True,
encoding='UTF-8').encode('utf-8')
if dian_signer is None: if dian_signer is None:
dian_signer = fe.DianXMLExtensionSigner dian_signer = fe.DianXMLExtensionSigner
signer = dian_signer(private_key, passphrase=passphrase, localpolicy=use_cache_policy) signer = dian_signer(
private_key,
passphrase=passphrase,
localpolicy=use_cache_policy)
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write(signer.sign_xml_string(document)) f.write(signer.sign_xml_string(document))

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

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

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

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

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

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

View File

@@ -1,4 +1,4 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from ..amount import Amount from ..amount import Amount
@@ -29,7 +29,7 @@ class Trabajador:
codigo_trabajador: str = None codigo_trabajador: str = None
otros_nombres: str = None otros_nombres: str = None
sub_tipo: SubTipoTrabajador = 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',

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

17
requirements_dev.txt Normal file
View File

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

View File

@@ -22,4 +22,4 @@ exclude = docs
test = pytest test = pytest
[tool:pytest] [tool:pytest]
collect_ignore = ['setup.py'] addopts = --ignore=setup.py

View File

@@ -13,6 +13,22 @@ with open('README.rst') as readme_file:
with open('HISTORY.rst') as history_file: with open('HISTORY.rst') as history_file:
history = history_file.read() 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', requirements = ['Click>=6.0',
'zeep==4.0.0', 'zeep==4.0.0',
'lxml==4.6.3', 'lxml==4.6.3',
@@ -26,6 +42,8 @@ requirements = ['Click>=6.0',
'mock>=2.0.0', 'mock>=2.0.0',
'xmlschema>=1.8'] 'xmlschema>=1.8']
"""
setup_requirements = ['pytest-runner', ] setup_requirements = ['pytest-runner', ]
test_requirements = ['pytest', ] test_requirements = ['pytest', ]
@@ -39,10 +57,10 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Natural Language :: English', 'Natural Language :: English',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.12',
], ],
description="Facturacion Electronica Colombia", description="Facturacion Electronica Colombia",
entry_points={ entry_points={
@@ -66,6 +84,10 @@ setup(
test_suite='tests', test_suite='tests',
tests_require=test_requirements, tests_require=test_requirements,
url='https://github.com/bit4bit/facho', url='https://github.com/bit4bit/facho',
<<<<<<< HEAD
version='0.2.0', version='0.2.0',
=======
version='0.2.1',
>>>>>>> morfo
zip_safe=False, zip_safe=False,
) )

1
tests/cude.txt Normal file
View File

@@ -0,0 +1 @@
907e4444decc9e59c160a2fb3b6659b33dc5b632a5008922b9a62f83f757b1c448e47f5867f2b50dbdb96f48c7681168

0
tests/cufe.txt Normal file
View File

View File

@@ -4,30 +4,32 @@ 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('1234', 'xx', datetime.now())) inv = form.DebitNote(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('30') inv.set_operation_type('30')
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'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code='48',
organization_code = '1', organization_code='1',
address = form.Address( address=form.Address(
'', '', form.City('05001', 'Medellín'), '', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'), form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')) form.CountrySubentity('05', 'Antioquia'))
)) ))
inv.set_customer(form.Party( inv.set_customer(form.Party(
name = 'facho-customer', name='facho-customer',
ident = form.PartyIdentification('321', '', '31'), ident=form.PartyIdentification('321', '', '31'),
responsability_code = form.Responsability(['O-07']), responsability_code=form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code='48',
organization_code = '1', organization_code='1',
address = form.Address( address=form.Address(
'', '', form.City('05001', 'Medellín'), '', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'), form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')) form.CountrySubentity('05', 'Antioquia'))
@@ -45,7 +47,7 @@ def simple_credit_note_without_lines():
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'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code = '48',
organization_code = '1', organization_code = '1',
address = form.Address( address = form.Address(
@@ -56,7 +58,7 @@ def simple_credit_note_without_lines():
inv.set_customer(form.Party( inv.set_customer(form.Party(
name = 'facho-customer', name = 'facho-customer',
ident = form.PartyIdentification('321', '', '31'), ident = form.PartyIdentification('321', '', '31'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code = '48',
organization_code = '1', organization_code = '1',
address = form.Address( address = form.Address(
@@ -77,7 +79,7 @@ def simple_invoice_without_lines():
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'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code = '48',
organization_code = '1', organization_code = '1',
address = form.Address( address = form.Address(
@@ -88,7 +90,7 @@ def simple_invoice_without_lines():
inv.set_customer(form.Party( inv.set_customer(form.Party(
name = 'facho-customer', name = 'facho-customer',
ident = form.PartyIdentification('321', '', '31'), ident = form.PartyIdentification('321', '', '31'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code = '48',
organization_code = '1', organization_code = '1',
address = form.Address( address = form.Address(
@@ -98,6 +100,7 @@ def simple_invoice_without_lines():
)) ))
return inv return inv
@pytest.fixture @pytest.fixture
def simple_invoice(): def simple_invoice():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
@@ -109,7 +112,7 @@ def simple_invoice():
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'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code = '48',
organization_code = '1', organization_code = '1',
address = form.Address( address = form.Address(
@@ -120,7 +123,7 @@ def simple_invoice():
inv.set_customer(form.Party( inv.set_customer(form.Party(
name = 'facho-customer', name = 'facho-customer',
ident = form.PartyIdentification('321','', '31'), ident = form.PartyIdentification('321','', '31'),
responsability_code = form.Responsability(['O-07']), responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48', responsability_regime_code = '48',
organization_code = '1', organization_code = '1',
address = form.Address( address = form.Address(
@@ -128,19 +131,20 @@ def simple_invoice():
form.Country('CO', 'Colombia'), form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')) form.CountrySubentity('05', 'Antioquia'))
)) ))
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto facho', description='productofacho',
item = form.StandardItem( 9999), item=form.StandardItem(9999),
price = form.Price(form.Amount(100.0), '01', ''), price=form.Price(form.Amount(100.0),'01',''),
tax = form.TaxTotal( tax=form.TaxTotal(
tax_amount = form.Amount(0.0), tax_amount=form.Amount(0.0),
taxable_amount = form.Amount(0.0), taxable_amount=form.Amount(0.0),
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
percent = 19.0, percent=19.0,
) )]),
] withholding=form.WithholdingTaxTotal(
) subtotals=[])
)) ))
return inv return inv

View File

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

View File

@@ -2,16 +2,124 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of facho. The COPYRIGHT file at the top level of # This file is part of facho. The COPYRIGHT file at the top level of
# 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 import helpers
from fixtures import simple_invoice
def test_xml_with_required_elements(): simple_invoice = simple_invoice
doc = form_xml.AttachedDocument(id='123')
def test_xml_with_required_elements(simple_invoice):
xml_header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
DIANInvoiceXML = form_xml.DIANInvoiceXML(
simple_invoice)
doc = form_xml.AttachedDocument(
simple_invoice,
DIANInvoiceXML,
id='123')
xml = doc.toFachoXML() xml = doc.toFachoXML()
assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123'
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,21 +4,26 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
"""Tests for `facho` package.""" """Tests for `facho` package."""
import pytest
from facho.fe.data.dian import codelist from facho.fe.data.dian import codelist
def test_tiporesponsabilidad(): def test_tiporesponsabilidad():
assert codelist.TipoResponsabilidad.short_name == 'TipoResponsabilidad' assert codelist.TipoResponsabilidad.short_name == 'TipoResponsabilidad'
assert codelist.TipoResponsabilidad.by_name('Autorretenedor')['name'] == 'Autorretenedor' assert codelist.TipoResponsabilidad.by_name(
'Autorretenedor')['name'] == 'Autorretenedor'
def test_tipoorganizacion(): def test_tipoorganizacion():
assert codelist.TipoOrganizacion.short_name == 'TipoOrganizacion' assert codelist.TipoOrganizacion.short_name == 'TipoOrganizacion'
assert codelist.TipoOrganizacion.by_name('Persona Natural')['name'] == 'Persona Natural' assert codelist.TipoOrganizacion.by_name(
'Persona Natural')['name'] == 'Persona Natural'
def test_tipodocumento(): def test_tipodocumento():
assert codelist.TipoDocumento.short_name == 'TipoDocumento' assert codelist.TipoDocumento.short_name == 'TipoDocumento'
assert codelist.TipoDocumento.by_name('Factura de Venta Nacional')['code'] == '01' assert codelist.TipoDocumento.by_name(
'Factura electrónica de Venta')['code'] == '01'
def test_departamento(): def test_departamento():
assert codelist.Departamento['05']['name'] == 'Antioquia' assert codelist.Departamento['05']['name'] == 'Antioquia'

View File

@@ -5,15 +5,15 @@
from datetime import datetime from datetime import datetime
import pytest import pytest
from facho import fe
from facho import fe
import helpers 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 -newkey rsa:2048 -keyout example.key -out example.pem
#openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem # openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem
signer = fe.DianXMLExtensionSigner('./tests/example.p12') signer = fe.DianXMLExtensionSigner('./tests/example.p12')
xml = fe.FeXML('Invoice', xml = fe.FeXML('Invoice',
@@ -116,3 +116,17 @@ 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):
xml = fe.FeXML(
'Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.find_or_create_element(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
ublextension = xml.fragment(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True)
ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
xmlstring = xml.tostring()
signer = fe.DianXMLExtensionSigner('./tests/example.p12')
xmlsigned = signer.sign_xml_string(xmlstring)

View File

@@ -5,24 +5,47 @@
"""Tests for `facho` package.""" """Tests for `facho` package."""
import pytest
from datetime import datetime from datetime import datetime
import io import io
import zipfile import zipfile
import facho.fe.form as form import facho.fe.form as form
from facho import fe from facho import fe
from facho.fe.form_xml import DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML from facho.fe.form_xml import (
DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML)
from fixtures import (
simple_invoice,
simple_invoice_without_lines,
simple_credit_note_without_lines,
simple_debit_note_without_lines)
try:
CUDE_ = open("./tests/cude.txt", 'r').read().strip()
except FileNotFoundError:
raise Exception("Archivo Cude No encontrado")
CUFE_ = (
'8bb918b19ba22a694f1da'
'11c643b5e9de39adf60311c'
'f179179e9b33381030bcd4c3c'
'3f156c506ed5908f9276f5bd9b4')
simple_invoice = simple_invoice
simple_invoice_without_lines = simple_invoice_without_lines
simple_credit_note_without_lines = simple_credit_note_without_lines
simple_debit_note_without_lines = simple_debit_note_without_lines
from fixtures import *
def test_invoicesimple_build(simple_invoice): def test_invoicesimple_build(simple_invoice):
xml = DIANInvoiceXML(simple_invoice) xml = DIANInvoiceXML(simple_invoice)
supplier_name = xml.get_element_text('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') supplier_name = xml.get_element_text(
'/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('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') customer_name = xml.get_element_text(
'/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
@@ -42,9 +65,11 @@ def test_invoicesimple_xml_signed(monkeypatch, simple_invoice):
print(xml.tostring()) print(xml.tostring())
xml.add_extension(signer) xml.add_extension(signer)
elem = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature') elem = xml.get_element(
'/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature')
assert elem.text is not None assert elem.text is not None
def test_invoicesimple_zip(simple_invoice): def test_invoicesimple_zip(simple_invoice):
xml_invoice = DIANInvoiceXML(simple_invoice) xml_invoice = DIANInvoiceXML(simple_invoice)
@@ -66,26 +91,34 @@ def test_invoicesimple_zip(simple_invoice):
def test_bug_cbcid_empty_on_invoice_line(simple_invoice): def test_bug_cbcid_empty_on_invoice_line(simple_invoice):
xml_invoice = DIANInvoiceXML(simple_invoice) xml_invoice = DIANInvoiceXML(simple_invoice)
cbc_id = xml_invoice.get_element_text('/fe:Invoice/cac:InvoiceLine[1]/cbc:ID', format_=int) cbc_id = xml_invoice.get_element_text(
'/fe:Invoice/cac:InvoiceLine[1]/cbc:ID', format_=int)
assert cbc_id == 1 assert cbc_id == 1
def test_invoice_line_count_numeric(simple_invoice): def test_invoice_line_count_numeric(simple_invoice):
xml_invoice = DIANInvoiceXML(simple_invoice) xml_invoice = DIANInvoiceXML(simple_invoice)
count = xml_invoice.get_element_text('/fe:Invoice/cbc:LineCountNumeric', format_=int) count = xml_invoice.get_element_text(
'/fe:Invoice/cbc:LineCountNumeric', format_=int)
assert count == len(simple_invoice.invoice_lines) assert count == len(simple_invoice.invoice_lines)
def test_invoice_profileexecutionid(simple_invoice): def test_invoice_profileexecutionid(simple_invoice):
xml_invoice = DIANInvoiceXML(simple_invoice) xml_invoice = DIANInvoiceXML(simple_invoice)
cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice)
xml_invoice.add_extension(cufe_extension) xml_invoice.add_extension(cufe_extension)
id_ = xml_invoice.get_element_text('/fe:Invoice/cbc:ProfileExecutionID', format_=int) id_ = xml_invoice.get_element_text(
'/fe:Invoice/cbc:ProfileExecutionID', format_=int)
assert id_ == 2 assert id_ == 2
def test_invoice_invoice_type_code(simple_invoice): def test_invoice_invoice_type_code(simple_invoice):
xml_invoice = DIANInvoiceXML(simple_invoice) xml_invoice = DIANInvoiceXML(simple_invoice)
id_ = xml_invoice.get_element_text('/fe:Invoice/cbc:InvoiceTypeCode', format_=int) id_ = xml_invoice.get_element_text(
'/fe:Invoice/cbc:InvoiceTypeCode', format_=int)
assert id_ == 1 assert id_ == 1
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'
@@ -93,39 +126,50 @@ def test_invoice_totals(simple_invoice_without_lines):
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(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto', description='producto',
item = form.StandardItem(9999), item=form.StandardItem(9999),
price = form.Price(form.Amount(1_500_000), '01', ''), price=form.Price(form.Amount(1_500_000), '01', ''),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
scheme = form.TaxScheme('01'), scheme=form.TaxScheme('01'),
percent = 19.0 percent=19.0
)]) )]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
)) ))
simple_invoice.calculate() simple_invoice.calculate()
assert 1 == len(simple_invoice.invoice_lines) assert 1 == len(simple_invoice.invoice_lines)
assert form.Amount(1_500_000) == simple_invoice.invoice_legal_monetary_total.line_extension_amount assert form.Amount(1_500_000) == (
assert form.Amount(1_785_000) == simple_invoice.invoice_legal_monetary_total.payable_amount simple_invoice.invoice_legal_monetary_total.line_extension_amount)
assert form.Amount(1_785_000) == (
simple_invoice.invoice_legal_monetary_total.payable_amount)
def test_invoice_cufe(simple_invoice_without_lines): def test_invoice_cufe(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(
simple_invoice.invoice_supplier.ident = form.PartyIdentification('700085371', '5', '31') '2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31') simple_invoice.invoice_supplier.ident = form.PartyIdentification(
'700085371', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification(
'800199436', '5', '31')
simple_invoice.add_invoice_line(form.InvoiceLine( simple_invoice.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1.00, '94'), quantity=form.Quantity(
description = 'producto', 1.00, '94'),
item = form.StandardItem(111), description='producto',
price = form.Price(form.Amount(1_500_000), '01', ''), item=form.StandardItem(111),
tax = form.TaxTotal( price=form.Price(form.Amount(1_500_000), '01', ''),
subtotals = [ tax=form.TaxTotal(
subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
scheme = form.TaxScheme('01'), scheme=form.TaxScheme('01'),
percent = 19.0 percent=19.0
)]) )]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
)) ))
simple_invoice.calculate() simple_invoice.calculate()
@@ -133,65 +177,86 @@ def test_invoice_cufe(simple_invoice_without_lines):
cufe_extension = fe.DianXMLExtensionCUFE( cufe_extension = fe.DianXMLExtensionCUFE(
simple_invoice, simple_invoice,
tipo_ambiente = fe.AMBIENTE_PRODUCCION, tipo_ambiente=fe.AMBIENTE_PRODUCCION,
clave_tecnica = '693ff6f2a553c3646a063436fd4dd9ded0311471' clave_tecnica='693ff6f2a553c3646a063436fd4dd9ded0311471'
) )
formatVars = cufe_extension.formatVars() formatVars = cufe_extension.formatVars()
#NumFac
# NumFac
assert formatVars[0] == '323200000129', "NumFac" assert formatVars[0] == '323200000129', "NumFac"
#FecFac
# FecFac
assert formatVars[1] == '2019-01-16', "FecFac" assert formatVars[1] == '2019-01-16', "FecFac"
#HoraFac
# HoraFac
assert formatVars[2] == '10:53:10-05:00', "HoraFac" assert formatVars[2] == '10:53:10-05:00', "HoraFac"
#ValorBruto
# ValorBruto
assert formatVars[3] == '1500000.00', "ValorBruto" assert formatVars[3] == '1500000.00', "ValorBruto"
#CodImpuesto1
# CodImpuesto1
assert formatVars[4] == '01', "CodImpuesto1" assert formatVars[4] == '01', "CodImpuesto1"
#ValorImpuesto1
# ValorImpuesto1
assert formatVars[5] == '285000.00', "ValorImpuesto1" assert formatVars[5] == '285000.00', "ValorImpuesto1"
#CodImpuesto2
# CodImpuesto2
assert formatVars[6] == '04', "CodImpuesto2" assert formatVars[6] == '04', "CodImpuesto2"
#ValorImpuesto2
# ValorImpuesto2
assert formatVars[7] == '0.00', "ValorImpuesto2" assert formatVars[7] == '0.00', "ValorImpuesto2"
#CodImpuesto3
# CodImpuesto3
assert formatVars[8] == '03', "CodImpuesto3" assert formatVars[8] == '03', "CodImpuesto3"
#ValorImpuesto3
# ValorImpuesto3
assert formatVars[9] == '0.00', "ValorImpuesto3" assert formatVars[9] == '0.00', "ValorImpuesto3"
#ValTotFac
# ValTotFac
assert formatVars[10] == '1785000.00', "ValTotFac" assert formatVars[10] == '1785000.00', "ValTotFac"
#NitOFE
# NitOFE
assert formatVars[11] == '700085371', "NitOFE" assert formatVars[11] == '700085371', "NitOFE"
#NumAdq
# NumAdq
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"
xml_invoice.add_extension(cufe_extension) xml_invoice.add_extension(cufe_extension)
cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID') cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID')
# RESOLUCION 004: pagina 689 # RESOLUCION 004: pagina 689
assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' assert cufe == CUFE_
def test_credit_note_cude(simple_credit_note_without_lines): def test_credit_note_cude(simple_credit_note_without_lines):
simple_invoice = simple_credit_note_without_lines simple_invoice = simple_credit_note_without_lines
simple_invoice.invoice_ident = '8110007871' simple_invoice.invoice_ident = '8110007871'
simple_invoice.invoice_issue = datetime.strptime('2019-01-12 07:00:00-05:00', '%Y-%m-%d %H:%M:%S%z') simple_invoice.invoice_issue = datetime.strptime(
simple_invoice.invoice_supplier.ident = form.PartyIdentification('900373076', '5', '31') '2019-01-12 07:00:00-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_customer.ident = form.PartyIdentification('8355990', '5', '31') simple_invoice.invoice_supplier.ident = form.PartyIdentification(
'900373076', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification(
'8355990', '5', '31')
simple_invoice.add_invoice_line(form.InvoiceLine( simple_invoice.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(
description = 'producto', 1, '94'),
item = form.StandardItem(111), description='producto',
price = form.Price(form.Amount(5_000), '01', ''), item=form.StandardItem(111),
tax = form.TaxTotal( price=form.Price(
subtotals = [ form.Amount(5_000), '01', ''),
tax=form.TaxTotal(
subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
scheme = form.TaxScheme('01'), scheme=form.TaxScheme('01'),
percent = 19.0 percent=19.0
)]) )]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
)) ))
simple_invoice.calculate() simple_invoice.calculate()
@@ -200,33 +265,39 @@ def test_credit_note_cude(simple_credit_note_without_lines):
cude_extension = fe.DianXMLExtensionCUDE( cude_extension = fe.DianXMLExtensionCUDE(
simple_invoice, simple_invoice,
'12301', '12301',
tipo_ambiente = fe.AMBIENTE_PRODUCCION, tipo_ambiente=fe.AMBIENTE_PRODUCCION,
) )
xml_invoice.add_extension(cude_extension) xml_invoice.add_extension(cude_extension)
cude = xml_invoice.get_element_text('/fe:CreditNote/cbc:UUID') cude = xml_invoice.get_element_text('/fe:CreditNote/cbc:UUID')
# pag 612 # pag 612
assert cude == '907e4444decc9e59c160a2fb3b6659b33dc5b632a5008922b9a62f83f757b1c448e47f5867f2b50dbdb96f48c7681168'
assert cude == CUDE_
# pag 614 # pag 614
def test_debit_note_cude(simple_debit_note_without_lines): def test_debit_note_cude(simple_debit_note_without_lines):
simple_invoice = simple_debit_note_without_lines simple_invoice = simple_debit_note_without_lines
simple_invoice.invoice_ident = 'ND1001' simple_invoice.invoice_ident = 'ND1001'
simple_invoice.invoice_issue = datetime.strptime('2019-01-18 10:58:00-05:00', '%Y-%m-%d %H:%M:%S%z') simple_invoice.invoice_issue = datetime.strptime(
simple_invoice.invoice_supplier.ident = form.PartyIdentification('900197264', '5', '31') '2019-01-18 10:58:00-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_customer.ident = form.PartyIdentification('10254102', '5', '31') simple_invoice.invoice_supplier.ident = form.PartyIdentification(
'900197264', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification(
'10254102', '5', '31')
simple_invoice.add_invoice_line(form.InvoiceLine( simple_invoice.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto', description='producto',
item = form.StandardItem(111), item=form.StandardItem(111),
price = form.Price(form.Amount(30_000), '01', ''), price=form.Price(form.Amount(30_000), '01', ''),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
scheme = form.TaxScheme('04'), scheme=form.TaxScheme('04'),
percent = 8.0 percent=8.0
)]) )]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
)) ))
simple_invoice.calculate() simple_invoice.calculate()
@@ -235,7 +306,7 @@ def test_debit_note_cude(simple_debit_note_without_lines):
cude_extension = fe.DianXMLExtensionCUDE( cude_extension = fe.DianXMLExtensionCUDE(
simple_invoice, simple_invoice,
'10201', '10201',
tipo_ambiente = fe.AMBIENTE_PRUEBAS, tipo_ambiente=fe.AMBIENTE_PRUEBAS,
) )
build_vars = cude_extension.buildVars() build_vars = cude_extension.buildVars()
assert build_vars['NumFac'] == 'ND1001' assert build_vars['NumFac'] == 'ND1001'
@@ -251,7 +322,6 @@ def test_debit_note_cude(simple_debit_note_without_lines):
assert build_vars['Software-PIN'] == '10201' assert build_vars['Software-PIN'] == '10201'
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.0090019726410254102102012'

View File

@@ -6,91 +6,117 @@
"""Tests for `facho` package.""" """Tests for `facho` package."""
import pytest import pytest
from datetime import datetime # from datetime import datetime
import io # import io
import zipfile # import zipfile
import facho.fe.form as form import facho.fe.form as form
from facho import fe # from facho import fe
from fixtures import (
simple_invoice,
simple_invoice_without_lines,
simple_credit_note_without_lines,
simple_debit_note_without_lines)
simple_invoice = simple_invoice
simple_invoice_without_lines = simple_invoice_without_lines
simple_credit_note_without_lines = simple_credit_note_without_lines
simple_debit_note_without_lines = simple_debit_note_without_lines
def test_invoice_legalmonetary(): def test_invoice_legalmonetary():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto facho', description='producto facho',
item = form.StandardItem(9999), item=form.StandardItem(9999),
price = form.Price( price=form.Price(
amount = form.Amount(100.0), amount=form.Amount(100.0),
type_code = '01', type_code='01',
type = 'x' type='x'
), ),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
percent = 19.0, percent=19.0,
) )]),
] withholding=form.WithholdingTaxTotal(
) subtotals=[])
)) ))
inv.calculate() inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) assert inv.invoice_legal_monetary_total.line_extension_amount == (
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == form.Amount(100.0) form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0) assert inv.invoice_legal_monetary_total.tax_exclusive_amount == (
assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(0.0) form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_inclusive_amount == (
form.Amount(119.0))
assert inv.invoice_legal_monetary_total.charge_total_amount == (
form.Amount(0.0))
def test_allowancecharge_as_discount(): def test_allowancecharge_as_discount():
discount = form.AllowanceChargeAsDiscount(amount=form.Amount(1000.0)) discount = form.AllowanceChargeAsDiscount(amount=form.Amount(1000.0))
assert discount.isDiscount() == True
assert discount.isDiscount()
def test_FAU10(): def test_FAU10():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto facho', description='productofacho',
item = form.StandardItem(9999), item=form.StandardItem(9999),
price = form.Price( price=form.Price(
amount = form.Amount(100.0), amount=form.Amount(100.0),
type_code = '01', type_code='01',
type = 'x' type='x'
), ),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
percent = 19.0, percent=19.0)]),
) withholding=form.WithholdingTaxTotal(
] subtotals=[])
)
)) ))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.calculate() inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) assert inv.invoice_legal_monetary_total.line_extension_amount == (
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == form.Amount(100.0) form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0) assert inv.invoice_legal_monetary_total.tax_exclusive_amount == (
assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(19.0) form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_inclusive_amount == (
form.Amount(119.0))
assert inv.invoice_legal_monetary_total.charge_total_amount == (
form.Amount(19.0))
def test_FAU14(): def test_FAU14():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto facho', description='productofacho',
item = form.StandardItem(9999), item=form.StandardItem(9999),
price = form.Price( price=form.Price(
amount = form.Amount(100.0), amount=form.Amount(100.0),
type_code = '01', type_code='01',
type = 'x' type='x'
), ),
tax = form.TaxTotal( tax=form.TaxTotal(
subtotals = [ subtotals=[
form.TaxSubTotal( form.TaxSubTotal(
percent = 19.0, percent=19.0,
) )]),
] withholding=form.WithholdingTaxTotal(
) subtotals=[])
)) ))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) inv.add_allowance_charge(form.AllowanceCharge(
inv.add_prepaid_payment(form.PrePaidPayment(paid_amount = form.Amount(50.0))) amount=form.Amount(19.0)))
inv.add_prepaid_payment(form.PrePaidPayment(
paid_amount=form.Amount(50.0)))
inv.calculate() inv.calculate()
wants = form.Amount(119.0 + 19.0 - 50.0) wants = form.Amount(119.0 + 19.0 - 50.0)
@@ -100,38 +126,42 @@ def test_FAU14():
def test_invalid_tipo_operacion_nota_debito(): def test_invalid_tipo_operacion_nota_debito():
reference = form.InvoiceDocumentReference( reference = form.InvoiceDocumentReference(
ident = '11111', ident='11111',
uuid = '21312312', uuid='21312312',
date = '2020-05-05' date='2020-05-05'
) )
inv = form.DebitNote(reference) inv = form.DebitNote(reference)
with pytest.raises(ValueError): with pytest.raises(ValueError):
inv.set_operation_type(22) inv.set_operation_type(22)
def test_valid_tipo_operacion_nota_debito(): def test_valid_tipo_operacion_nota_debito():
reference = form.InvoiceDocumentReference( reference = form.InvoiceDocumentReference(
ident = '11111', ident='11111',
uuid = '21312312', uuid='21312312',
date = '2020-05-05' date='2020-05-05'
) )
inv = form.DebitNote(reference) inv = form.DebitNote(reference)
inv.set_operation_type('30') inv.set_operation_type('30')
def test_invalid_tipo_operacion_nota_credito(): def test_invalid_tipo_operacion_nota_credito():
reference = form.InvoiceDocumentReference( reference = form.InvoiceDocumentReference(
ident = '11111', ident='11111',
uuid = '21312312', uuid='21312312',
date = '2020-05-05' date='2020-05-05'
) )
inv = form.DebitNote(reference) inv = form.DebitNote(reference)
with pytest.raises(ValueError): with pytest.raises(ValueError):
inv.set_operation_type('990') inv.set_operation_type('990')
def test_valid_tipo_operacion_nota_credito(): def test_valid_tipo_operacion_nota_credito():
reference = form.InvoiceDocumentReference( reference = form.InvoiceDocumentReference(
ident = '11111', ident='11111',
uuid = '21312312', uuid='21312312',
date = '2020-05-05' date='2020-05-05'
) )
inv = form.CreditNote(reference) inv = form.CreditNote(reference)
inv.set_operation_type('20') inv.set_operation_type('20')
@@ -141,41 +171,52 @@ def test_quantity():
quantity1 = form.Quantity(10, '94') quantity1 = form.Quantity(10, '94')
assert quantity1 * form.Amount(3) == form.Amount(30) assert quantity1 * form.Amount(3) == form.Amount(30)
def test_invoice_line_quantity_without_taxes(): def test_invoice_line_quantity_without_taxes():
line = form.InvoiceLine( line = form.InvoiceLine(
quantity = form.Quantity(10, '94'), quantity=form.Quantity(10, '94'),
description = '', description='',
item = form.StandardItem('test', 9999), item=form.StandardItem('test', 9999),
price = form.Price( price=form.Price(
amount = form.Amount(30.00), amount=form.Amount(30.00),
type_code = '01', type_code='01',
type = 'x' type='x'),
), tax=form.TaxTotal(subtotals=[]),
tax = form.TaxTotal(subtotals=[])) withholding=form.WithholdingTaxTotal(
subtotals=[])
)
line.calculate() line.calculate()
assert line.total_amount == form.Amount(300) assert line.total_amount == form.Amount(300)
assert line.tax_amount == form.Amount(0) assert line.tax_amount == form.Amount(0)
def test_invoice_legalmonetary_with_taxes(): def test_invoice_legalmonetary_with_taxes():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
inv.add_invoice_line(form.InvoiceLine( inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'), quantity=form.Quantity(1, '94'),
description = 'producto facho', description='productofacho',
item = form.StandardItem(9999), item=form.StandardItem(9999),
price = form.Price( price=form.Price(
amount = form.Amount(100.0), amount=form.Amount(100.0),
type_code = '01', type_code='01',
type = 'x' type='x'
), ),
tax = form.TaxTotal(subtotals=[]) tax=form.TaxTotal(subtotals=[]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
)) ))
inv.calculate() inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) assert inv.invoice_legal_monetary_total.line_extension_amount == (
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == form.Amount(100.0) form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(100.0) assert inv.invoice_legal_monetary_total.tax_exclusive_amount == (
assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(0.0) form.Amount(100.0))
assert inv.invoice_legal_monetary_total.payable_amount == form.Amount(100.0) assert inv.invoice_legal_monetary_total.tax_inclusive_amount == (
form.Amount(100.0))
assert inv.invoice_legal_monetary_total.charge_total_amount == (
form.Amount(0.0))
assert inv.invoice_legal_monetary_total.payable_amount == (
form.Amount(100.0))
def test_invoice_ident_prefix_automatic_invalid(): def test_invoice_ident_prefix_automatic_invalid():
@@ -183,6 +224,7 @@ def test_invoice_ident_prefix_automatic_invalid():
with pytest.raises(ValueError): with pytest.raises(ValueError):
inv.set_ident('SETPQJQJ1234567') inv.set_ident('SETPQJQJ1234567')
def test_invoice_ident_prefix_automatic(): def test_invoice_ident_prefix_automatic():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
inv.set_ident('SETP1234567') inv.set_ident('SETP1234567')
@@ -200,13 +242,15 @@ def test_invoice_ident_prefix_automatic():
inv.set_ident('1234567') inv.set_ident('1234567')
assert inv.invoice_ident_prefix == '' assert inv.invoice_ident_prefix == ''
def test_invoice_ident_prefix_manual(): def test_invoice_ident_prefix_manual():
inv = form.NationalSalesInvoice() inv = form.NationalSalesInvoice()
inv.set_ident('SETP1234567') inv.set_ident('SETP1234567')
inv.set_ident_prefix('SETA') inv.set_ident_prefix('SETA')
assert inv.invoice_ident_prefix == 'SETA' assert inv.invoice_ident_prefix == 'SETA'
def test_invoice_ident_prefix_automatic_debit(): def test_invoice_ident_prefix_automatic_debit():
inv = form.DebitNote(form.BillingReference('','','')) inv = form.DebitNote(form.BillingReference('', '', ''))
inv.set_ident('ABCDEF1234567') inv.set_ident('ABCDEF1234567')
assert inv.invoice_ident_prefix == 'ABCDEF' assert inv.invoice_ident_prefix == 'ABCDEF'

View File

@@ -6,13 +6,24 @@
"""Tests for `facho` package.""" """Tests for `facho` package."""
import pytest import pytest
from datetime import datetime # from datetime import datetime
import copy import copy
from facho.fe import form from facho.fe import form
from facho.fe import form_xml from facho.fe import form_xml
# from fixtures import *
from fixtures import (
simple_invoice,
simple_invoice_without_lines,
simple_credit_note_without_lines,
simple_debit_note_without_lines)
simple_invoice = simple_invoice
simple_invoice_without_lines = simple_invoice_without_lines
simple_credit_note_without_lines = simple_credit_note_without_lines
simple_debit_note_without_lines = simple_debit_note_without_lines
from fixtures import *
def test_import_DIANInvoiceXML(): def test_import_DIANInvoiceXML():
try: try:
@@ -27,70 +38,82 @@ def test_import_DIANDebitNoteXML():
except AttributeError: except AttributeError:
pytest.fail("unexpected not found") pytest.fail("unexpected not found")
def test_import_DIANCreditNoteXML(): def test_import_DIANCreditNoteXML():
try: try:
form_xml.DIANCreditNoteXML form_xml.DIANCreditNoteXML
except AttributeError: except AttributeError:
pytest.fail("unexpected not found") pytest.fail("unexpected not found")
def test_allowance_charge_in_invoice(simple_invoice_without_lines):
inv = copy.copy(simple_invoice_without_lines)
inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'),
description = 'producto facho',
item = form.StandardItem(9999),
price = form.Price(
amount = form.Amount(100.0),
type_code = '01',
type = 'x'
),
tax = form.TaxTotal(
subtotals = [
form.TaxSubTotal(
percent = 19.0,
)
]
)
))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.calculate()
xml = form_xml.DIANInvoiceXML(inv) # def test_allowance_charge_in_invoice(simple_invoice_without_lines):
assert xml.get_element_text('./cac:AllowanceCharge/cbc:ID') == '1' # inv = copy.copy(simple_invoice_without_lines)
assert xml.get_element_text('./cac:AllowanceCharge/cbc:ChargeIndicator') == 'true' # inv.add_invoice_line(form.InvoiceLine(
assert xml.get_element_text('./cac:AllowanceCharge/cbc:Amount') == '19.0' # quantity=form.Quantity(1, '94'),
assert xml.get_element_text('./cac:AllowanceCharge/cbc:BaseAmount') == '100.0' # description='productofacho',
# item=form.StandardItem(9999),
# price=form.Price(
# amount=form.Amount(100.0),
# type_code='01',
# type='x'
# ),
# tax=form.TaxTotal(
# subtotals=[
# form.TaxSubTotal(
# percent=19.0,
# )]),
# withholding=form.WithholdingTaxTotal(
# subtotals=[])
# ))
def test_allowance_charge_in_invoice_line(simple_invoice_without_lines): # inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv = copy.copy(simple_invoice_without_lines) # inv.calculate()
inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'),
description = 'producto facho',
item = form.StandardItem(9999),
price = form.Price(
amount = form.Amount(100.0),
type_code = '01',
type = 'x'
),
tax = form.TaxTotal(
subtotals = [
form.TaxSubTotal(
percent = 19.0,
)
]
),
allowance_charge = [
form.AllowanceChargeAsDiscount(amount=form.Amount(10.0))
]
))
inv.calculate()
# se aplico descuento # xml = form_xml.DIANInvoiceXML(inv)
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(90.0) # assert xml.get_element_text('./cac:AllowanceCharge/cbc:ID') == '1'
# assert xml.get_element_text(
# './cac:AllowanceCharge/cbc:ChargeIndicator') == 'true'
# assert xml.get_element_text(
# './cac:AllowanceCharge/cbc:Amount') == '19.0'
# assert xml.get_element_text(
# './cac:AllowanceCharge/cbc:BaseAmount') == '100.0'
xml = form_xml.DIANInvoiceXML(inv)
with pytest.raises(AttributeError): # def test_allowance_charge_in_invoice_line(simple_invoice_without_lines):
assert xml.get_element_text('/fe:Invoice/cac:AllowanceCharge/cbc:ID') == '1' # inv = copy.copy(simple_invoice_without_lines)
xml.get_element_text('/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:ID') == '1' # inv.add_invoice_line(form.InvoiceLine(
xml.get_element_text('/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:BaseAmount') == '100.0' # quantity=form.Quantity(1, '94'),
# description='producto facho',
# item=form.StandardItem(9999),
# price=form.Price(
# amount=form.Amount(100.0),
# type_code='01',
# type='x'
# ),
# tax=form.TaxTotal(
# subtotals=[
# form.TaxSubTotal(
# percent=19.0,
# )]),
# withholding=form.WithholdingTaxTotal(
# subtotals=[]),
# allowance_charge=[
# form.AllowanceChargeAsDiscount(amount=form.Amount(10.0))
# ]
# ))
# inv.calculate()
# # se aplico descuento
# assert inv.invoice_legal_monetary_total.line_extension_amount == (
# form.Amount(90.0))
# xml = form_xml.DIANInvoiceXML(inv)
# with pytest.raises(AttributeError):
# assert xml.get_element_text(
# '/fe:Invoice/cac:AllowanceCharge/cbc:ID') == '1'
# xml.get_element_text(
# '/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:ID') == '1'
# xml.get_element_text(
# '/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:BaseAmount'
# ) == '100.0'

601
tests/test_model.py Normal file
View File

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

119
tests/test_model_invoice.py Normal file
View File

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

View File

@@ -4,467 +4,467 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
"""Tests for `facho` package.""" """Tests for `facho` package."""
import re # import re
import pytest # import pytest
from facho import fe # from facho import fe
import helpers # import helpers
def assert_error(errors, msg): # def assert_error(errors, msg):
for error in errors: # for error in errors:
if str(error) == msg: # if str(error) == msg:
return True # return True
raise "wants error: %s" % (msg) # raise "wants error: %s" % (msg)
def test_adicionar_devengado_Basico(): # def test_adicionar_devengado_Basico():
nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 30, # dias_trabajados = 30,
sueldo_trabajado = fe.nomina.Amount(1_000_000) # sueldo_trabajado = fe.nomina.Amount(1_000_000)
)) # ))
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30' # assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30'
assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00' # assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00'
def test_adicionar_devengado_transporte(): # def test_adicionar_devengado_transporte():
nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( # nomina.adicionar_devengado(fe.nomina.DevengadoTransporte(
auxilio_transporte = fe.nomina.Amount(2_000_000) # auxilio_transporte = fe.nomina.Amount(2_000_000)
)) # ))
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()
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 60, # dias_trabajados = 60,
sueldo_trabajado = fe.nomina.Amount(2_000_000) # sueldo_trabajado = fe.nomina.Amount(2_000_000)
)) # ))
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( # nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
porcentaje = fe.nomina.Amount(19), # porcentaje = fe.nomina.Amount(19),
deduccion = fe.nomina.Amount(1_000_000) # deduccion = fe.nomina.Amount(1_000_000)
)) # ))
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()
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 60, # dias_trabajados = 60,
sueldo_trabajado = fe.nomina.Amount(1_000_000) # sueldo_trabajado = fe.nomina.Amount(1_000_000)
)) # ))
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( # nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
porcentaje = fe.nomina.Amount(19), # porcentaje = fe.nomina.Amount(19),
deduccion = fe.nomina.Amount(1_000_000) # deduccion = fe.nomina.Amount(1_000_000)
)) # ))
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()
nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( # nomina.adicionar_devengado(fe.nomina.DevengadoTransporte(
auxilio_transporte = fe.nomina.Amount(2_000_000) # auxilio_transporte = fe.nomina.Amount(2_000_000)
)) # ))
nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( # nomina.adicionar_devengado(fe.nomina.DevengadoTransporte(
auxilio_transporte = fe.nomina.Amount(3_000_000) # auxilio_transporte = fe.nomina.Amount(3_000_000)
)) # ))
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()
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 60, # dias_trabajados = 60,
sueldo_trabajado = fe.nomina.Amount(1000) # sueldo_trabajado = fe.nomina.Amount(1000)
)) # ))
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( # nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
porcentaje = fe.nomina.Amount(19), # porcentaje = fe.nomina.Amount(19),
deduccion = fe.nomina.Amount(1000) # deduccion = fe.nomina.Amount(1000)
)) # ))
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()
errors = nomina.validate() # errors = nomina.validate()
assert_error(errors, 'se requiere Periodo') # assert_error(errors, 'se requiere Periodo')
assert_error(errors, 'se requiere DevengadoBasico') # assert_error(errors, 'se requiere DevengadoBasico')
assert_error(errors, 'se requiere DeduccionSalud') # assert_error(errors, 'se requiere DeduccionSalud')
assert_error(errors, 'se requiere DeduccionFondoPension') # assert_error(errors, 'se requiere DeduccionFondoPension')
def test_nomina_xml(): # def test_nomina_xml():
nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
nomina.asignar_metadata(fe.nomina.Metadata( # nomina.asignar_metadata(fe.nomina.Metadata(
novedad=fe.nomina.Novedad( # novedad=fe.nomina.Novedad(
activa = True, # activa = True,
cune = "N0111" # cune = "N0111"
), # ),
secuencia=fe.nomina.NumeroSecuencia( # secuencia=fe.nomina.NumeroSecuencia(
prefijo = 'N', # prefijo = 'N',
consecutivo='00001' # consecutivo='00001'
), # ),
lugar_generacion=fe.nomina.Lugar( # lugar_generacion=fe.nomina.Lugar(
pais = fe.nomina.Pais( # pais = fe.nomina.Pais(
code = 'CO' # code = 'CO'
), # ),
departamento = fe.nomina.Departamento( # departamento = fe.nomina.Departamento(
code = '05' # code = '05'
), # ),
municipio = fe.nomina.Municipio( # municipio = fe.nomina.Municipio(
code = '05001' # code = '05001'
), # ),
), # ),
proveedor=fe.nomina.Proveedor( # proveedor=fe.nomina.Proveedor(
nit='999999', # nit='999999',
dv=2, # dv=2,
software_id='xx', # software_id='xx',
software_pin='12', # software_pin='12',
razon_social='facho' # razon_social='facho'
) # )
)) # ))
nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( # nomina.asignar_informacion_general(fe.nomina.InformacionGeneral(
fecha_generacion = '2020-01-16', # fecha_generacion = '2020-01-16',
hora_generacion = '1053:10-05:00', # hora_generacion = '1053:10-05:00',
tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, # tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION,
software_pin = '693', # software_pin = '693',
tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_NORMAL, # tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_NORMAL,
periodo_nomina = fe.nomina.PeriodoNomina(code='1'), # periodo_nomina = fe.nomina.PeriodoNomina(code='1'),
tipo_moneda = fe.nomina.TipoMoneda(code='COP') # tipo_moneda = fe.nomina.TipoMoneda(code='COP')
)) # ))
nomina.asignar_empleador(fe.nomina.Empleador( # nomina.asignar_empleador(fe.nomina.Empleador(
razon_social='facho', # razon_social='facho',
nit = '700085371', # nit = '700085371',
dv = '1', # dv = '1',
pais = fe.nomina.Pais( # pais = fe.nomina.Pais(
code = 'CO' # code = 'CO'
), # ),
departamento = fe.nomina.Departamento( # departamento = fe.nomina.Departamento(
code = '05' # code = '05'
), # ),
municipio = fe.nomina.Municipio( # municipio = fe.nomina.Municipio(
code = '05001' # code = '05001'
), # ),
direccion = 'calle etrivial' # direccion = 'calle etrivial'
)) # ))
nomina.asignar_trabajador(fe.nomina.Trabajador( # nomina.asignar_trabajador(fe.nomina.Trabajador(
tipo_contrato = fe.nomina.TipoContrato( # tipo_contrato = fe.nomina.TipoContrato(
code = '1' # code = '1'
), # ),
alto_riesgo = False, # alto_riesgo = False,
tipo_documento = fe.nomina.TipoDocumento( # tipo_documento = fe.nomina.TipoDocumento(
code = '11' # code = '11'
), # ),
primer_apellido = 'gnu', # primer_apellido = 'gnu',
segundo_apellido = 'emacs', # segundo_apellido = 'emacs',
primer_nombre = 'facho', # primer_nombre = 'facho',
lugar_trabajo = fe.nomina.LugarTrabajo( # lugar_trabajo = fe.nomina.LugarTrabajo(
pais = fe.nomina.Pais(code='CO'), # pais = fe.nomina.Pais(code='CO'),
departamento = fe.nomina.Departamento(code='05'), # departamento = fe.nomina.Departamento(code='05'),
municipio = fe.nomina.Municipio(code='05001'), # municipio = fe.nomina.Municipio(code='05001'),
direccion = 'calle facho' # direccion = 'calle facho'
), # ),
numero_documento = '800199436', # numero_documento = '800199436',
tipo = fe.nomina.TipoTrabajador( # tipo = fe.nomina.TipoTrabajador(
code = '01' # code = '01'
), # ),
salario_integral = True, # salario_integral = True,
sueldo = fe.nomina.Amount(1_500_000) # sueldo = fe.nomina.Amount(1_500_000)
)) # ))
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 60, # dias_trabajados = 60,
sueldo_trabajado = fe.nomina.Amount(3_500_000) # sueldo_trabajado = fe.nomina.Amount(3_500_000)
)) # ))
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( # nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
porcentaje = fe.nomina.Amount(19), # porcentaje = fe.nomina.Amount(19),
deduccion = fe.nomina.Amount(1_000_000) # deduccion = fe.nomina.Amount(1_000_000)
)) # ))
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab' # expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab'
assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune # assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune
assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102' # assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') == f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_cune}" # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/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/Empleador/@NIT') == '700085371'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True' # assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True'
assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov') == 'N0111' # 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()
nomina.asignar_pago(fe.nomina.Pago( # nomina.asignar_pago(fe.nomina.Pago(
forma = fe.nomina.FormaPago(code='1'), # forma = fe.nomina.FormaPago(code='1'),
metodo = fe.nomina.MetodoPago(code='1') # metodo = fe.nomina.MetodoPago(code='1')
)) # ))
def test_nomina_xmlsign(monkeypatch): # def test_nomina_xmlsign(monkeypatch):
nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
signer = fe.nomina.DianXMLExtensionSigner('./tests/example.p12') # signer = fe.nomina.DianXMLExtensionSigner('./tests/example.p12')
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()
nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnas( # nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnas(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
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'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Porcentaje') == '2.00'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Pago') == '200.00'
def test_nomina_devengado_horas_recargo_nocturno(): # def test_nomina_devengado_horas_recargo_nocturno():
nomina = fe.nomina.DIANNominaIndividual() # nomina = fe.nomina.DIANNominaIndividual()
nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturno( # nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturno(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
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'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Porcentaje') == '2.00'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Pago') == '200.00'
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.DevengadoHorasExtrasDiariasDominicalesYFestivos(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
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'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Porcentaje') == '2.00'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Pago') == '200.00'
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.DevengadoHorasRecargoDiariasDominicalesYFestivos(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
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'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Porcentaje') == '2.00'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Pago') == '200.00'
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.DevengadoHorasExtrasNocturnasDominicalesYFestivos(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
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'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Porcentaje') == '2.00'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Pago') == '200.00'
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.DevengadoHorasRecargoNocturnoDominicalesYFestivos(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
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'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Porcentaje') == '2.00'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Pago') == '200.00'
def test_fecha_validacion(): # def test_fecha_validacion():
with pytest.raises(ValueError) as e: # with pytest.raises(ValueError) as e:
fe.nomina.Fecha('535-35-3') # fe.nomina.Fecha('535-35-3')

View File

@@ -4,232 +4,233 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
"""Tests for `facho` package.""" """Tests for `facho` package."""
import re # import re
import pytest # import pytest
from facho import fe # from facho import fe
import helpers # import helpers
def atest_nomina_ajuste_reemplazar(): # def atest_nomina_ajuste_reemplazar():
nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
print(xml) # print(xml)
assert False # assert False
def test_nomina_ajuste_reemplazar_asignacion_tipo_xml(): # def test_nomina_ajuste_reemplazar_asignacion_tipo_xml():
nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
nomina.asignar_metadata(fe.nomina.Metadata( # nomina.asignar_metadata(fe.nomina.Metadata(
novedad=fe.nomina.Novedad( # novedad=fe.nomina.Novedad(
activa = True, # activa = True,
cune = "N0111" # cune = "N0111"
), # ),
secuencia=fe.nomina.NumeroSecuencia( # secuencia=fe.nomina.NumeroSecuencia(
prefijo = 'N', # prefijo = 'N',
consecutivo='00001' # consecutivo='00001'
), # ),
lugar_generacion=fe.nomina.Lugar( # lugar_generacion=fe.nomina.Lugar(
pais = fe.nomina.Pais( # pais = fe.nomina.Pais(
code = 'CO' # code = 'CO'
), # ),
departamento = fe.nomina.Departamento( # departamento = fe.nomina.Departamento(
code = '05' # code = '05'
), # ),
municipio = fe.nomina.Municipio( # municipio = fe.nomina.Municipio(
code = '05001' # code = '05001'
), # ),
), # ),
proveedor=fe.nomina.Proveedor( # proveedor=fe.nomina.Proveedor(
nit='999999', # nit='999999',
dv=2, # dv=2,
software_id='xx', # software_id='xx',
software_pin='12', # software_pin='12',
razon_social='facho' # razon_social='facho'
) # )
)) # ))
nomina.asignar_empleador(fe.nomina.Empleador( # nomina.asignar_empleador(fe.nomina.Empleador(
razon_social='facho', # razon_social='facho',
nit = '700085371', # nit = '700085371',
dv = '1', # dv = '1',
pais = fe.nomina.Pais( # pais = fe.nomina.Pais(
code = 'CO' # code = 'CO'
), # ),
departamento = fe.nomina.Departamento( # departamento = fe.nomina.Departamento(
code = '05' # code = '05'
), # ),
municipio = fe.nomina.Municipio( # municipio = fe.nomina.Municipio(
code = '05001' # code = '05001'
), # ),
direccion = 'calle etrivial' # direccion = 'calle etrivial'
)) # ))
nomina.asignar_trabajador(fe.nomina.Trabajador( # nomina.asignar_trabajador(fe.nomina.Trabajador(
tipo_contrato = fe.nomina.TipoContrato( # tipo_contrato = fe.nomina.TipoContrato(
code = '1' # code = '1'
), # ),
alto_riesgo = False, # alto_riesgo = False,
tipo_documento = fe.nomina.TipoDocumento( # tipo_documento = fe.nomina.TipoDocumento(
code = '11' # code = '11'
), # ),
primer_apellido = 'gnu', # primer_apellido = 'gnu',
segundo_apellido = 'emacs', # segundo_apellido = 'emacs',
primer_nombre = 'facho', # primer_nombre = 'facho',
lugar_trabajo = fe.nomina.LugarTrabajo( # lugar_trabajo = fe.nomina.LugarTrabajo(
pais = fe.nomina.Pais(code='CO'), # pais = fe.nomina.Pais(code='CO'),
departamento = fe.nomina.Departamento(code='05'), # departamento = fe.nomina.Departamento(code='05'),
municipio = fe.nomina.Municipio(code='05001'), # municipio = fe.nomina.Municipio(code='05001'),
direccion = 'calle facho' # direccion = 'calle facho'
), # ),
numero_documento = '800199436', # numero_documento = '800199436',
tipo = fe.nomina.TipoTrabajador( # tipo = fe.nomina.TipoTrabajador(
code = '01' # code = '01'
), # ),
salario_integral = True, # salario_integral = True,
sueldo = fe.nomina.Amount(1_500_000) # sueldo = fe.nomina.Amount(1_500_000)
)) # ))
nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( # nomina.asignar_informacion_general(fe.nomina.InformacionGeneral(
fecha_generacion = '2020-01-16', # fecha_generacion = '2020-01-16',
hora_generacion = '1053:10-05:00', # hora_generacion = '1053:10-05:00',
tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, # tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION,
software_pin = '693', # software_pin = '693',
tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_AJUSTES, # tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_AJUSTES,
periodo_nomina = fe.nomina.PeriodoNomina(code='1'), # periodo_nomina = fe.nomina.PeriodoNomina(code='1'),
tipo_moneda = fe.nomina.TipoMoneda(code='COP') # tipo_moneda = fe.nomina.TipoMoneda(code='COP')
)) # ))
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():
nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() # nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar()
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 60, # dias_trabajados = 60,
sueldo_trabajado = fe.nomina.Amount(2_000_000) # sueldo_trabajado = fe.nomina.Amount(2_000_000)
)) # ))
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( # nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
porcentaje = fe.nomina.Amount(19), # porcentaje = fe.nomina.Amount(19),
deduccion = fe.nomina.Amount(1_000_000) # deduccion = fe.nomina.Amount(1_000_000)
)) # ))
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/ComprobanteTotal') == '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'
)) # ))
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 xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@NumeroPred') == '123456'
assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456' # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456'
assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16' # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/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'
)) # ))
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
print(xml.tostring()) # print(xml.tostring())
assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None
assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is None # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') 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.Predecesor(
numero = '123456', # numero = '123456',
cune = 'ABC123456', # cune = 'ABC123456',
fecha_generacion = '2021-11-16' # fecha_generacion = '2021-11-16'
)) # ))
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
print(xml.tostring()) # print(xml.tostring())
assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None
assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is None # assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') 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()
nomina.adicionar_devengado(fe.nomina.DevengadoBasico( # nomina.adicionar_devengado(fe.nomina.DevengadoBasico(
dias_trabajados = 60, # dias_trabajados = 60,
sueldo_trabajado = fe.nomina.Amount(2_000_000) # sueldo_trabajado = fe.nomina.Amount(2_000_000)
)) # ))
nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( # nomina.adicionar_deduccion(fe.nomina.DeduccionSalud(
porcentaje = fe.nomina.Amount(19), # porcentaje = fe.nomina.Amount(19),
deduccion = fe.nomina.Amount(1_000_000) # deduccion = fe.nomina.Amount(1_000_000)
)) # ))
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/ComprobanteTotal') == '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.Predecesor(
numero = '123456', # numero = '123456',
cune = 'ABC123456', # cune = 'ABC123456',
fecha_generacion = '2021-11-16' # fecha_generacion = '2021-11-16'
)) # ))
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 xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@NumeroPred') == '123456'
assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456' # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456'
assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16' # assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/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()
nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiarias( # nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiarias(
horas_extras=[ # horas_extras=[
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T19:09:55', # hora_inicio='2021-11-30T19:09:55',
hora_fin='2021-11-30T20:09:55', # hora_fin='2021-11-30T20:09:55',
cantidad=1, # cantidad=1,
porcentaje=fe.nomina.Amount(1), # porcentaje=fe.nomina.Amount(1),
pago=fe.nomina.Amount(100) # pago=fe.nomina.Amount(100)
), # ),
fe.nomina.DevengadoHoraExtra( # fe.nomina.DevengadoHoraExtra(
hora_inicio='2021-11-30T18:09:55', # hora_inicio='2021-11-30T18:09:55',
hora_fin='2021-11-30T19:09:55', # hora_fin='2021-11-30T19:09:55',
cantidad=2, # cantidad=2,
porcentaje=fe.nomina.Amount(2), # porcentaje=fe.nomina.Amount(2),
pago=fe.nomina.Amount(200) # pago=fe.nomina.Amount(200)
) # )
] # ]
)) # ))
xml = nomina.toFachoXML() # xml = nomina.toFachoXML()
extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDs/HED', multiple=True) # extras = xml.get_element(
assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' # '/nomina:NominaIndividual/Devengados/HEDs/HED', multiple=True)
assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' # assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55'
assert extras[0].get('Cantidad') == '1' # assert extras[0].get('HoraFin') == '2021-11-30T20:09:55'
assert extras[0].get('Porcentaje') == '1.00' # assert extras[0].get('Cantidad') == '1'
assert extras[0].get('Pago') == '100.00' # assert extras[0].get('Porcentaje') == '1.00'
assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' # assert extras[0].get('Pago') == '100.00'
assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' # assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55'
assert extras[1].get('Cantidad') == '2' # assert extras[1].get('HoraFin') == '2021-11-30T19:09:55'
assert extras[1].get('Porcentaje') == '2.00' # assert extras[1].get('Cantidad') == '2'
assert extras[1].get('Pago') == '200.00' # assert extras[1].get('Porcentaje') == '2.00'
# assert extras[1].get('Pago') == '200.00'

View File

@@ -3,16 +3,22 @@
# This file is part of facho. The COPYRIGHT file at the top level of # This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
import pytest # import pytest
import facho.fe.form as form import facho.fe.form as form
from facho import fe from facho import fe
from facho.fe.form_xml import DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML
from fixtures import * from facho.fe.form_xml import DIANInvoiceXML
# from facho.fe.form_xml import (
# DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML)
from fixtures import simple_invoice
from facho.fe.form import query from facho.fe.form import query
simple_invoice = simple_invoice
def test_query_billing_reference(simple_invoice): def test_query_billing_reference(simple_invoice):
xml = DIANInvoiceXML(simple_invoice) xml = DIANInvoiceXML(simple_invoice)
cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice)

10
tox.ini
View File

@@ -1,12 +1,12 @@
[tox] [tox]
envlist = py27, py34, py35, py36, flake8 envlist = py39, py310, py311, py312, flake8
[travis] [travis]
python = python =
3.6: py36 3.9: py39
3.5: py35 3.10: py310
3.4: py34 3.11: py311
2.7: py27 3.12: py312
[testenv:flake8] [testenv:flake8]
basepython = python basepython = python