23 Commits

Author SHA1 Message Date
93a3d18e32 Compatibilidad con Python 3.13
- Actualiza dependencias: cryptography>=41.0.0, pyOpenSSL>=23.2.0,
  xmlsec>=1.3.12, xmlsig>=0.1.9, zeep>=4.2.1, lxml>=5.2.2
- Anade monkey-patch para xades.load_pkcs12 con pyOpenSSL >= 24
  (PKCS12 eliminado de OpenSSL.crypto)
- Reemplaza datetime.utcnow() por datetime.now(timezone.utc)
- Reemplaza APIs deprecadas de lxml en facho.py y codelist
- Migra verificacion PKCS12 de pyOpenSSL a cryptography API en fe.py
- Moderniza typing: Union/Optional/List a sintaxis | y list[]
- Actualiza requirements_dev.txt con versiones recientes
- Actualiza tox.ini y Makefile.dev para Python 3.13
2026-07-02 22:45:33 -05:00
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
40 changed files with 1993 additions and 1572 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ def extensions(inv):
'SETP', 990000000, 995000000)#del SET de pruebas
return [security_code, authorization_provider, cufe, software_provider, inv_authorization]
def invoice():
# factura de venta nacional
inv = form.Invoice('01')
@@ -50,9 +51,10 @@ def invoice():
inv.set_supplier(form.Party(
legal_name = 'Nombre registrado de la empresa',
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
responsability_code = form.Responsability(['O-07', 'O-14', 'O-48']),
responsability_code=form.Responsability(['ZZ', 'O-14', 'O-48']),
# ver DIAN:FAJ28
responsability_regime_code='48',
# tipo de organizacion juridica ver DIAN:6.2.3
@@ -90,11 +92,12 @@ def invoice():
payment_id='2'
))
# adicionar una linea al documento
inv.add_invoice_line(form.InvoiceLine(
inv.add_invoice_line(
form.InvoiceLine(
quantity=form.Quantity(int(20.5), '94'),
# item general de codigo 999
description='productO3',
item = form.StandardItem('test', 9999),
sitem=form.StandardItem('test', 9999),
price=form.Price(
# precio base del item (sin iva)
amount=form.Amount(200.00),
@@ -107,11 +110,11 @@ def invoice():
form.TaxSubTotal(
percent=19.00,
scheme=form.TaxScheme('01')
)
]
)]
)
))
return inv
def document_xml():
return form_xml.DIANInvoiceXML

View File

@@ -1,51 +1,70 @@
# importar libreria de modelos
from facho import fe, form_xml
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'
# consultar las extensiones necesarias
def extensions(inv):
security_code = fe.DianXMLExtensionSoftwareSecurityCode('id software', 'pin', inv.invoice_ident)
security_code = fe.DianXMLExtensionSoftwareSecurityCode(
'id software', 'pin', inv.invoice_ident)
authorization_provider = fe.DianXMLExtensionAuthorizationProvider()
cufe = fe.DianXMLExtensionCUFE(inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS,
cufe = fe.DianXMLExtensionCUFE(
inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS,
'clave tecnica')
nit = form.PartyIdentification('nit', '5', '31')
software_provider = fe.DianXMLExtensionSoftwareProvider(nit, nit.dv, 'id software')
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization('invoice autorization',
software_provider = fe.DianXMLExtensionSoftwareProvider(
nit, nit.dv, 'id software')
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(
'invoice autorization',
datetime(2019, 1, 19),
datetime(2030, 1, 19),
'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
def generate_document(invoice, filepath):
xml = form_xml.DIANInvoiceXML(invoice)
for extension in extensions(invoice):
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
# ...
# factura de venta nacional
inv = form.NationalSalesInvoice()
# asignar periodo de facturacion
inv.set_period(datetime.now(), datetime.now())
# asignar fecha de emision de la factura
inv.set_issue(datetime.now())
# asignar prefijo y numero del documento
inv.set_ident('SETP990003033')
# asignar tipo de operacion ver DIAN:6.1.5
inv.set_operation_type('10')
# asignar proveedor
inv.set_supplier(form.Party(
legal_name='FACHOSOS',
name='FACHOSOS',
ident=form.PartyIdentification('900579212', '5', '31'),
# 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
responsability_regime_code='48',
# tipo de organizacion juridica ver DIAN:6.2.3
@@ -58,6 +77,7 @@ inv.set_supplier(form.Party(
country=form.Country('CO', 'Colombia'),
countrysubentity=form.CountrySubentity('05', 'Antioquia'))
))
inv.set_customer(form.Party(
legal_name='facho-customer',
name='facho-customer',
@@ -81,7 +101,6 @@ inv.set_payment_mean(form.PaymentMean(
code='10',
# fecha de vencimiento de la factura
due_at=datetime.now(),
# identificador numerico
payment_id='1'
))
@@ -102,8 +121,7 @@ inv.add_invoice_line(form.InvoiceLine(
subtotals=[
form.TaxSubTotal(
percent=19.00,
)
]
)]
)
))

View File

@@ -76,7 +76,7 @@
<cac:PartyTaxScheme>
<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: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:PartyTaxScheme>
<cac:Contact>

View File

@@ -140,12 +140,12 @@ class LXMLBuilder:
attrs['pretty_print'] = attrs.pop('pretty_print', False)
attrs['encoding'] = attrs.pop('encoding', 'UTF-8')
for el in elem.getiterator():
for el in elem.iter():
keys = filter(lambda key: key.startswith('facho_'), el.keys())
self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True'
if is_optional and el.getchildren() == [] and el.keys() == [
if is_optional and list(el) == [] and el.keys() == [
'facho_optional']:
el.getparent().remove(el)
@@ -291,7 +291,7 @@ class FachoXML:
# se fuerza la adicion como un nuevo elemento
if append:
last_slibing = None
for child in parent.getchildren():
for child in list(parent):
if child.tag == node_tag:
last_slibing = child

View File

@@ -8,7 +8,7 @@ import xmlsec
import urllib.request
from datetime import datetime
from dataclasses import dataclass, asdict, field
from typing import List
import http.client
import hashlib
import secrets
@@ -47,7 +47,7 @@ class GetNumberingRangeResponse:
ValidateDateTo: str
TechnicalKey: str
NumberRangeResponse: List[NumberRangeResponse]
NumberRangeResponse: list[NumberRangeResponse]
@classmethod
@@ -91,7 +91,7 @@ class SendBillAsync(SOAPService):
@dataclass
class SendTestSetAsyncResponse:
ZipKey: str
ErrorMessageList: List[str]
ErrorMessageList: list[str]
@classmethod
def fromdict(cls, data):
@@ -134,7 +134,7 @@ class GetStatusResponse:
IsValid: bool
StatusDescription: str
StatusCode: int
ErrorMessage: List[str]
ErrorMessage: list[str]
@classmethod
def fromdict(cls, data):

View File

@@ -9,7 +9,7 @@ module.
"""
import pytz
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from lxml import etree
from lxml.etree import QName
@@ -222,7 +222,7 @@ def sign_envelope(
return _sign_envelope_with_key(envelope, key, signature_method, digest_method)
def get_timestamp(timestamp = None, delta=None):
timestamp = timestamp or datetime.utcnow()
timestamp = timestamp or datetime.now(timezone.utc)
if delta:
timestamp += delta
@@ -234,10 +234,9 @@ def _append_timestamp(security, expires_dt=None):
if expires_dt is None:
expires_dt = timedelta(seconds=6000)
timestamp = datetime.now()
etimestamp = utils.WSU.Timestamp({'{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Id': utils.get_unique_id()})
etimestamp.append(utils.WSU.Created(get_timestamp(timestamp=timestamp)))
etimestamp.append(utils.WSU.Expires(get_timestamp(timestamp=timestamp, delta=expires_dt)))
etimestamp.append(utils.WSU.Created(get_timestamp()))
etimestamp.append(utils.WSU.Expires(get_timestamp(delta=expires_dt)))
security.insert(0, etimestamp)
if etree.LXML_VERSION[:2] >= (3, 5):
etree.cleanup_namespaces(security,

View File

@@ -96,27 +96,5 @@
<SimpleValue>Exclusivo en referencias a documentos (elementos DocumentReference)</SimpleValue>
</Value>
</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>
</gc:CodeList>

View File

@@ -64,26 +64,10 @@
</Row>
<Row>
<Value ColumnRef="code">
<SimpleValue>O-48</SimpleValue>
<SimpleValue>ZZ</SimpleValue>
</Value>
<Value ColumnRef="name">
<SimpleValue>Impuesto sobre las ventas IVA</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>
<SimpleValue>No aplica</SimpleValue>
</Value>
</Row>
</SimpleCodeList>

View File

@@ -35,8 +35,8 @@ class CodeList:
row = {}
#construir registro...
for value in xmlrow.getchildren():
row[value.attrib['ColumnRef']] = value.getchildren()[0].text
for value in list(xmlrow):
row[value.attrib['ColumnRef']] = list(value)[0].text
return row

View File

@@ -1,20 +1,36 @@
# This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from ..facho import FachoXML, FachoXMLExtension, LXMLBuilder
import uuid
import xmlsig
import xades
from datetime import datetime
import OpenSSL
import zipfile
import warnings
# import warnings
import hashlib
from contextlib import contextmanager
from .data.dian import codelist
from . import form
from collections import defaultdict
from pathlib import Path
# from pathlib import Path
from dateutil import tz
from cryptography.hazmat.primitives.serialization import pkcs12
# Monkey-patch xades/xmlsig para compatibilidad con pyOpenSSL >= 24
# (PKCS12 fue eliminado de OpenSSL.crypto, pero ambas librerías lo referencian)
import OpenSSL
import xades.xades_context
if not hasattr(OpenSSL.crypto, 'PKCS12'):
def _patched_load_pkcs12(self, key):
if isinstance(key, tuple):
self.x509 = key[1]
self.public_key = key[1].public_key()
self.private_key = key[0]
else:
raise NotImplementedError("unsupported key type")
xades.xades_context.XAdESContext.load_pkcs12 = _patched_load_pkcs12
AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
@@ -30,7 +46,7 @@ SCHEME_AGENCY_ATTRS = {
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.'
Bogota = tz.gettz('America/Bogota')
# NAMESPACES = {
# 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
# 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual',
@@ -68,11 +84,11 @@ NAMESPACES = {
}
def fe_from_string(document: str) -> FachoXML:
return FeXML.from_string(document)
from contextlib import contextmanager
# from contextlib import contextmanager
@contextmanager
def mock_xades_policy():
from mock import patch
@@ -118,7 +134,6 @@ class FeXML(FachoXML):
class DianXMLExtensionCUDFE(FachoXMLExtension):
def __init__(self, invoice, tipo_ambiente=AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente
self.invoice = invoice
@@ -157,9 +172,11 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
# #DIAN 1.8.-2021: FAD03
# 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
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))
def issue_time(self, datetime_):
@@ -176,7 +193,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
build_vars['HoraFac'] = self.issue_time(invoice.invoice_issue)
# PAG 601
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))
build_vars['CodImpuesto1'] = '01'
build_vars['CodImpuesto2'] = '04'
@@ -206,7 +224,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension):
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.clave_tecnica = clave_tecnica
self.invoice = invoice
@@ -242,6 +261,7 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
'%d' % build_vars['TipoAmb'],
]
class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente
@@ -279,6 +299,7 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
'%d' % build_vars['TipoAmb'],
]
class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS):
self.tipo_ambiente = tipo_ambiente
@@ -312,6 +333,7 @@ class DianXMLExtensionCUDS(DianXMLExtensionCUDFE):
'%d' % build_vars['TipoAmb'],
]
class DianXMLExtensionSoftwareProvider(FachoXMLExtension):
# RESOLUCION 0004: pagina 108
@@ -321,7 +343,8 @@ class DianXMLExtensionSoftwareProvider(FachoXMLExtension):
self.id_software = id_software
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.update({'schemeID': self.dv})
#DIAN 1.7.-2020: FAB23
@@ -351,7 +374,6 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
class DianXMLExtensionSigner:
def __init__(self, pkcs12_path, passphrase=None, localpolicy=True):
self._pkcs12_data = open(pkcs12_path, 'rb').read()
self._passphrase = None
@@ -362,7 +384,6 @@ class DianXMLExtensionSigner:
@classmethod
def from_bytes(cls, data, passphrase=None, localpolicy=True):
self = cls.__new__(cls)
self._pkcs12_data = data
self._passphrase = None
self._localpolicy = localpolicy
@@ -372,7 +393,9 @@ class DianXMLExtensionSigner:
return self
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):
xml = LXMLBuilder.from_string(document)
@@ -394,7 +417,6 @@ class DianXMLExtensionSigner:
)
xml.append(signature)
ref = xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="", name="xmldsig-%s-ref0" % (id_uuid)
)
@@ -402,14 +424,16 @@ class DianXMLExtensionSigner:
id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid)
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)
data = xmlsig.template.add_x509_data(ki)
xmlsig.template.x509_data_add_certificate(data)
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)
id_props = "xmldsig-%s-signedprops" % (id_uuid)
@@ -417,10 +441,12 @@ class DianXMLExtensionSigner:
signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_props),
uri_type="http://uri.etsi.org/01903#SignedProperties"
)
xmlsig.template.add_transform(props_ref, xmlsig.constants.TransformInclC14N)
xmlsig.template.add_transform(
props_ref, xmlsig.constants.TransformInclC14N)
# 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")
policy = xades.policy.GenericPolicyId(
@@ -428,7 +454,8 @@ class DianXMLExtensionSigner:
POLICY_NAME,
xmlsig.constants.TransformSha256)
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))
if self._localpolicy:
@@ -454,19 +481,18 @@ class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
def build(self, fexml):
attrs = {'schemeID': '4', 'schemeName': '31'}
attrs.update(SCHEME_AGENCY_ATTRS)
authorization_provider = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider')
authorization_provider.set_element('./sts:AuthorizationProviderID',
'800197268',
**attrs)
class DianXMLExtensionInvoiceSource(FachoXMLExtension):
# CAB13
def build(self, fexml):
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",
listAgencyName="United Nations Economic Commission for Europe",
listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1")
@@ -499,16 +525,15 @@ class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension):
invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To',
self.to)
fexml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
fexml.set_element(
'./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode',
'CO',
# DIAN 1.7.-2020: FAB15
listAgencyID="6",
# DIAN 1.7.-2020: FAB16
listAgencyName="United Nations Economic Commission for Europe",
# DIAN 1.7.-2020: FAB17
listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1"
)
listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1")
class DianZIP:
@@ -517,7 +542,8 @@ class DianZIP:
MAX_FILES = 50
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
def add_xml(self, name, xml_data):
@@ -538,7 +564,6 @@ class DianZIP:
def __enter__(self):
"""
Facilita el uso de esta manera:
f = open('xxx', 'rb')
with DianZIP(f) as zip:
zip.add_invoice_xml('name', 'data xml')
@@ -576,8 +601,8 @@ class DianXMLExtensionSignerVerifier:
if isinstance(self._pkcs12_path_or_bytes, str):
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
ctx = xades.XAdESContext()
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data,
self._passphrase))
ctx.load_pkcs12(pkcs12.load_key_and_certificates(
pkcs12_data, self._passphrase))
try:
if self._localpolicy:
with mock_xades_policy():

View File

@@ -1,22 +1,25 @@
# 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 hashlib
from functools import reduce
import copy
# import hashlib
# from functools import reduce
# import copy
import dataclasses
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime, date
from collections import defaultdict
# from collections import defaultdict
import decimal
from decimal import Decimal
import typing
from ..data.dian import codelist
DECIMAL_PRECISION = 6
class AmountCurrencyError(TypeError):
pass
@dataclass
class Currency:
code: str
@@ -27,6 +30,7 @@ class Currency:
def __str__(self):
return self.code
class Collection:
def __init__(self, array):
@@ -43,6 +47,7 @@ class Collection:
def sum(self):
return sum(self.array)
class AmountCollection(Collection):
def sum(self):
@@ -51,8 +56,11 @@ class AmountCollection(Collection):
total += v
return total
class Amount:
def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')):
def __init__(
self, amount: int | float | str | "Amount",
currency: Currency = Currency('COP')):
# DIAN 1.7.-2020: 1.2.3.1
if isinstance(amount, Amount):
@@ -65,7 +73,9 @@ class Amount:
if float(amount) < 0:
raise ValueError('amount must be positive >= 0')
self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION,
self.amount = Decimal(
amount, decimal.Context(
prec=DECIMAL_PRECISION,
# DIAN 1.7.-2020: 1.2.1.1
rounding=decimal.ROUND_HALF_EVEN))
self.currency = currency
@@ -90,7 +100,8 @@ class Amount:
def __eq__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION)
return round(self.amount, DECIMAL_PRECISION) == round(
other.amount, DECIMAL_PRECISION)
def _cast(self, val):
if type(val) in [int, float]:
@@ -151,6 +162,7 @@ class Quantity:
def __repr__(self):
return str(self)
@dataclass
class Item:
scheme_name: str
@@ -188,6 +200,7 @@ class Country:
raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.Paises[self.code]['name']
@dataclass
class CountrySubentity:
code: str
@@ -198,6 +211,7 @@ class CountrySubentity:
raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.Departamento[self.code]['name']
@dataclass
class City:
code: str
@@ -208,18 +222,22 @@ class City:
raise ValueError("code [%s] not found" % (self.code))
self.name = codelist.Municipio[self.code]['name']
@dataclass
class PostalZone:
code: str = ''
@dataclass
class Address:
name: str
street: str = ''
city: City = City('05001')
country: Country = Country('CO')
countrysubentity: CountrySubentity = CountrySubentity('05')
postalzone: PostalZone = PostalZone('')
city: City = field(default_factory=lambda: City('05001'))
country: Country = field(default_factory=lambda: Country('CO'))
countrysubentity: CountrySubentity = field(
default_factory=lambda: CountrySubentity('05'))
postalzone: PostalZone = field(default_factory=lambda: PostalZone(''))
@dataclass
class PartyIdentification:
@@ -240,6 +258,7 @@ class PartyIdentification:
if self.type_fiscal not in codelist.TipoIdFiscal:
raise ValueError("type_fiscal [%s] not found" % (self.type_fiscal))
@dataclass
class Responsability:
codes: list
@@ -269,17 +288,18 @@ class TaxScheme:
raise ValueError("code not found")
self.name = codelist.TipoImpuesto[self.code]['name']
@dataclass
class Party:
name: str
ident: str
responsability_code: typing.List[Responsability]
responsability_code: list[Responsability]
responsability_regime_code: str
organization_code: str
tax_scheme: TaxScheme = TaxScheme('01')
tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01'))
phone: str = ''
address: Address = Address('')
address: Address = field(default_factory=lambda: Address(''))
email: str = ''
legal_name: str = ''
legal_company_ident: str = ''
@@ -306,8 +326,8 @@ class TaxScheme:
@dataclass
class TaxSubTotal:
percent: float
scheme: typing.Optional[TaxScheme] = None
tax_amount: Amount = Amount(0.0)
scheme: TaxScheme | None = None
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline):
if self.percent is not None:
@@ -317,8 +337,8 @@ class TaxSubTotal:
@dataclass
class TaxTotal:
subtotals: list
tax_amount: Amount = Amount(0.0)
taxable_amount: Amount = Amount(0.0)
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
taxable_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline):
self.taxable_amount = invline.total_amount
@@ -334,21 +354,23 @@ class TaxTotalOmit(TaxTotal):
def calculate(self, invline):
pass
@dataclass
class WithholdingTaxSubTotal:
percent: float
scheme: typing.Optional[TaxScheme] = None
tax_amount: Amount = Amount(0.0)
scheme: TaxScheme | None = None
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline):
if self.percent is not None:
self.tax_amount = invline.total_amount * Amount(self.percent / 100)
@dataclass
class WithholdingTaxTotal:
subtotals: list
tax_amount: Amount = Amount(0.0)
taxable_amount: Amount = Amount(0.0)
tax_amount: Amount = field(default_factory=lambda: Amount(0.0))
taxable_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self, invline):
self.taxable_amount = invline.total_amount
@@ -357,6 +379,7 @@ class WithholdingTaxTotal:
subtax.calculate(invline)
self.tax_amount += subtax.tax_amount
class WithholdingTaxTotalOmit(WithholdingTaxTotal):
def __init__(self):
super().__init__([])
@@ -364,6 +387,7 @@ class WithholdingTaxTotalOmit(WithholdingTaxTotal):
def calculate(self, invline):
pass
@dataclass
class Price:
amount: Amount
@@ -379,6 +403,7 @@ class Price:
self.amount *= self.quantity
@dataclass
class PaymentMean:
DEBIT = '01'
@@ -397,7 +422,8 @@ class PaymentMean:
@dataclass
class PrePaidPayment:
# DIAN 1.7.-2020: FBD03
paid_amount: Amount = Amount(0.0)
paid_amount: Amount = field(default_factory=lambda: Amount(0.0))
@dataclass
class BillingResponse:
@@ -405,6 +431,7 @@ class BillingResponse:
code: str
description: str
class SupportDocumentCreditNoteResponse(BillingResponse):
"""
ReferenceID: Identifica la sección del Documento
@@ -414,13 +441,13 @@ class SupportDocumentCreditNoteResponse(BillingResponse):
"""
@dataclass
class BillingReference:
ident: str
uuid: str
date: date
class CreditNoteDocumentReference(BillingReference):
"""
ident: Prefijo + Numero de la factura relacionada
@@ -428,6 +455,7 @@ class CreditNoteDocumentReference(BillingReference):
date: fecha de emision de la factura relacionada
"""
class DebitNoteDocumentReference(BillingReference):
"""
ident: Prefijo + Numero de la factura relacionada
@@ -435,6 +463,7 @@ class DebitNoteDocumentReference(BillingReference):
date: fecha de emision de la factura relacionada
"""
class InvoiceDocumentReference(BillingReference):
"""
ident: Prefijo + Numero de la nota credito relacionada
@@ -442,6 +471,7 @@ class InvoiceDocumentReference(BillingReference):
date: fecha de emision de la nota credito relacionada
"""
@dataclass
class AllowanceChargeReason:
code: str
@@ -456,20 +486,24 @@ class AllowanceChargeReason:
class AllowanceCharge:
# DIAN 1.7.-2020: FAQ03
charge_indicator: bool = True
amount: Amount = Amount(0.0)
amount: Amount = field(default_factory=lambda: Amount(0.0))
reason: AllowanceChargeReason = None
# Valor Base para calcular el descuento o el cargo
base_amount: typing.Optional[Amount] = Amount(0.0)
base_amount: Amount | None = field(
default_factory=lambda: Amount(0.0))
# 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):
return self.charge_indicator == True
charge_indicator = self.charge_indicator is True
return charge_indicator
def isDiscount(self):
return self.charge_indicator == False
charge_indicator = self.charge_indicator is False
return charge_indicator
def asCharge(self):
self.charge_indicator = True
@@ -483,11 +517,13 @@ class AllowanceCharge:
def set_base_amount(self, amount):
self.base_amount = amount
class AllowanceChargeAsDiscount(AllowanceCharge):
def __init__(self, amount: Amount = Amount(0.0)):
self.charge_indicator = False
self.amount = amount
@dataclass
class InvoiceLine:
# RESOLUCION 0004: pagina 155
@@ -499,9 +535,10 @@ class InvoiceLine:
# ya que al reportar los totales es sobre
# la factura y el percent es unico por type_code
# de subtotal
tax: typing.Optional[TaxTotal]
withholding: typing.Optional[WithholdingTaxTotal]
allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list)
tax: TaxTotal | None
withholding: WithholdingTaxTotal | None
allowance_charge: list[AllowanceCharge] = dataclasses.field(
default_factory=list)
def add_allowance_charge(self, charge):
if not isinstance(charge, AllowanceCharge):
@@ -566,15 +603,16 @@ class InvoiceLine:
if self.withholding is None:
self.withholding = WithholdingTaxTotalOmit()
@dataclass
class LegalMonetaryTotal:
line_extension_amount: Amount = Amount(0.0)
tax_exclusive_amount: Amount = Amount(0.0)
tax_inclusive_amount: Amount = Amount(0.0)
charge_total_amount: Amount = Amount(0.0)
allowance_total_amount: Amount = Amount(0.0)
payable_amount: Amount = Amount(0.0)
prepaid_amount: Amount = Amount(0.0)
line_extension_amount: Amount = field(default_factory=lambda: Amount(0.0))
tax_exclusive_amount: Amount = field(default_factory=lambda: Amount(0.0))
tax_inclusive_amount: Amount = field(default_factory=lambda: Amount(0.0))
charge_total_amount: Amount = field(default_factory=lambda: Amount(0.0))
allowance_total_amount: Amount = field(default_factory=lambda: Amount(0.0))
payable_amount: Amount = field(default_factory=lambda: Amount(0.0))
prepaid_amount: Amount = field(default_factory=lambda: Amount(0.0))
def calculate(self):
# DIAN 1.7.-2020: FAU14
@@ -585,26 +623,29 @@ class LegalMonetaryTotal:
- self.prepaid_amount
class NationalSalesInvoiceDocumentType(str):
def __str__(self):
# 6.1.3
return '01'
class CreditNoteDocumentType(str):
def __str__(self):
# 6.1.3
return '91'
class DebitNoteDocumentType(str):
def __str__(self):
# 6.1.3
return '92'
class CreditNoteSupportDocumentType(str):
def __str__(self):
return '95'
class Invoice:
def __init__(self, type_code: str):
if str(type_code) not in codelist.TipoDocumento:
@@ -650,7 +691,8 @@ class Invoice:
if len(prefix) <= 4:
self.invoice_ident_prefix = prefix
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):
"""
@@ -703,7 +745,6 @@ class Invoice:
def set_discrepancy_response(self, billing_response: BillingResponse):
self.invoice_discrepancy_response = billing_response
def accept(self, visitor):
visitor.visit_payment_mean(self.invoice_payment_mean)
visitor.visit_customer(self.invoice_customer)
@@ -715,27 +756,32 @@ class Invoice:
def _calculate_legal_monetary_total(self):
for invline in self.invoice_lines:
self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount
self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount
self.invoice_legal_monetary_total.line_extension_amount +=\
invline.total_amount
self.invoice_legal_monetary_total.tax_exclusive_amount +=\
invline.total_tax_exclusive_amount
# DIAN 1.7.-2020: FAU6
self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount
self.invoice_legal_monetary_total.tax_inclusive_amount +=\
invline.total_tax_inclusive_amount
# 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())\
.map(lambda charge: charge.amount)\
.sum()
# 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())\
.map(lambda charge: charge.amount)\
.sum()
# DIAN 1.7.-2020: FAU12
self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(self.invoice_prepaid_payment)\
.map(lambda paid: paid.paid_amount)\
.sum()
self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(
self.invoice_prepaid_payment).map(
lambda paid: paid.paid_amount).sum()
# DIAN 1.7.-2020: FAU14
self.invoice_legal_monetary_total.calculate()
@@ -745,11 +791,13 @@ class Invoice:
for invline in self.invoice_lines:
if invline.allowance_charge:
# 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
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):
for invline in self.invoice_lines:
@@ -757,6 +805,7 @@ class Invoice:
self._calculate_legal_monetary_total()
self._refresh_charges_base_amount()
class NationalSalesInvoice(Invoice):
def __init__(self):
super().__init__(NationalSalesInvoiceDocumentType())
@@ -801,11 +850,14 @@ class DebitNote(Invoice):
if not self.invoice_ident_prefix:
self.invoice_ident_prefix = self.invoice_ident[0:6]
class SupportDocument(Invoice):
pass
class SupportDocumentCreditNote(SupportDocument):
def __init__(self, invoice_document_reference: BillingReference,
def __init__(
self, invoice_document_reference: BillingReference,
invoice_discrepancy_response: BillingResponse):
super().__init__(CreditNoteSupportDocumentType())

View File

@@ -2,13 +2,14 @@ from .. import fe
__all__ = ['AttachedDocument']
class AttachedDocument():
def __init__(self, id):
schema = 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2'
schema =\
'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2'
self.fexml = fe.FeXML('AttachedDocument', schema)
self.fexml.set_element('./cbc:ID', id)
def toFachoXML(self):
return self.fexml

View File

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

View File

@@ -1,9 +1,10 @@
from .. import fe
from ..form import *
# from .. import fe
# from ..form import *
from .invoice import DIANInvoiceXML
__all__ = ['DIANDebitNoteXML']
class DIANDebitNoteXML(DIANInvoiceXML):
"""
DianInvoiceXML mapea objeto form.Invoice a XML segun
@@ -21,17 +22,22 @@ class DIANDebitNoteXML(DIANInvoiceXML):
# DIAN 1.7.-2020: DAU03
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)
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:TaxExclusiveAmount',
fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:TaxExclusiveAmount',
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)
fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:ChargeTotalAmount',
fexml.set_element_amount(
'./cac:RequestedMonetaryTotal/cbc:ChargeTotalAmount',
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)

View File

@@ -1,5 +1,6 @@
from .. import fe
from ..form import *
from collections import defaultdict
__all__ = ['DIANInvoiceXML']
@@ -626,20 +627,28 @@ class DIANInvoiceXML(fe.FeXML):
def set_allowance_charge(fexml, invoice):
for idx, charge in enumerate(invoice.invoice_allowance_charge):
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):
line = parent.fragment('./cac:AllowanceCharge', append=append)
# DIAN 1.7.-2020: FAQ02
line.set_element('./cbc:ID', idx)
# 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:
line.set_element('./cbc:AllowanceChargeReasonCode', 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:BaseAmount', charge.base_amount)
line.set_element(
'./cbc:AllowanceChargeReasonCode', 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:BaseAmount', charge.base_amount)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML

View File

@@ -1,10 +1,16 @@
from .. import fe
from ..form import *
from datetime import datetime, date
from .attached_document import *
from ..form import (
Amount, DebitNoteDocumentReference, CreditNoteDocumentReference,
InvoiceDocumentReference, TaxTotalOmit, WithholdingTaxTotalOmit
)
from collections import defaultdict
from datetime import datetime
# from .attached_document import *
__all__ = ['DIANSupportDocumentXML']
class DIANSupportDocumentXML(fe.FeXML):
"""
DianSupportDocumentXML mapea objeto form.Invoice a XML segun
@@ -16,31 +22,38 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAB03
# 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: 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: 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: 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: 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
# DIAN 1.1.-2021: DSAA02 DSAB01
# DIAN 1.1.-2021: NSAA02 NSAB01
ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True)
ublextension = self.fragment(
'./ext:UBLExtensions/ext:UBLExtension', append=True)
# DIAN 1.1.-2021: DSAB02
# DIAN 1.1.-2021: NSAB02
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
extcontent = ublextension.find_or_create_element(
'/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice(invoice)
def set_supplier(fexml, invoice):
@@ -50,44 +63,53 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAJ02
# DIAN 1.1.-2021: NSAJ02
fexml.set_element('./cac:AccountingSupplierParty/cbc:AdditionalAccountID',
fexml.set_element(
'./cac:AccountingSupplierParty/cbc:AdditionalAccountID',
invoice.invoice_supplier.organization_code)
# DIAN 1.1.-2021: DSAJ07 DSAJ08
# 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: 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)
# DIAN 1.1.-2021: DSAJ10
# 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)
# DIAN 1.1.-2021: DSAJ73
# 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)
# DIAN 1.1.-2021: DSAJ11
# 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)
# DIAN 1.1.-2021: DSAJ12
# 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)
# 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)
# DIAN 1.1.-2021: DSAJ15 DSAJ16
# DIAN 1.1.-2021: NSAJ15 NSAJ16
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
invoice.invoice_supplier.address.country.code)
# DIAN 1.1.-2021: DSAJ17
@@ -95,26 +117,30 @@ class DIANSupportDocumentXML(fe.FeXML):
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
invoice.invoice_supplier.address.country.name,
# DIAN 1.1.-2021: DSAJ18
#DIAN 1.1.-2021: NSAJ18
# # DIAN 1.1.-2021: NSAJ18
languageID='es')
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})
# DIAN 1.1.-2021: DSAJ19
# 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: 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)
# DIAN 1.1.-2021: DSAJ21
# 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,
# DIAN 1.1.-2021: DSAJ22 DSAJ23 DSAJ24 DSAJ25
# DIAN 1.1.-2021: NSAJ22 NSAJ23 NSAJ24 NSAJ25
@@ -122,25 +148,28 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAJ26
# DIAN 1.1.-2021: NSAJ26
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
invoice.invoice_supplier.responsability_code,
listName=invoice.invoice_supplier.responsability_regime_code)
# DIAN 1.1.-2021: DSAJ39
# DIAN 1.1.-2021: NSAJ39
fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
fexml.placeholder_for(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
# DIAN 1.1.-2021: DSAJ40
# DIAN 1.1.-2021: NSAJ40
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code)
# DIAN 1.1.-2021: DSAJ41
# DIAN 1.1.-2021: NSAJ41
fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
fexml.set_element(
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name)
def set_customer(fexml, invoice):
# DIAN 1.1.-2021: DSAK01
# DIAN 1.1.-2021: NSAK01
@@ -148,7 +177,8 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAK02
# DIAN 1.1.-2021: NSAK02
fexml.set_element('./cac:AccountingCustomerParty/cbc:AdditionalAccountID',
fexml.set_element(
'./cac:AccountingCustomerParty/cbc:AdditionalAccountID',
invoice.invoice_customer.organization_code)
# DIAN 1.1.-2021: DSAK03
@@ -157,20 +187,25 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAK19
# 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: 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)
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})
# DIAN 1.1.-2021: DSAK21
# 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,
# DIAN 1.1.-2021: DSAK22 DSAK23 DSAK24 DSAK25
# DIAN 1.1.-2021: NSAK22 NSAK23 NSAK24 NSAK25
@@ -178,24 +213,27 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAK26
# DIAN 1.1.-2021: NSAK26
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
invoice.invoice_customer.responsability_code)
# DIAN 1.1.-2021: DSAK39
# DIAN 1.1.-2021: NSAK39
fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
fexml.placeholder_for(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
# DIAN 1.1.-2021: DSAK40
# DIAN 1.1.-2021: NSAK40
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code)
# DIAN 1.1.-2021: DSAK41
# DIAN 1.1.-2021: NSAK41
fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
fexml.set_element(
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name)
def set_payment_mean(fexml, invoice):
payment_mean = invoice.invoice_payment_mean
@@ -205,15 +243,21 @@ class DIANSupportDocumentXML(fe.FeXML):
# DIAN 1.1.-2021: DSAN03
# 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: 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: 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):
if not isinstance(amount, Amount):
@@ -230,38 +274,45 @@ class DIANSupportDocumentXML(fe.FeXML):
def set_legal_monetary(fexml, invoice):
# DIAN 1.1.-2021: DSAU01 DSAU02 DSAU03
# 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)
# DIAN 1.1.-2021: DSAU04 DSAU05
# 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)
# DIAN 1.1.-2021: DSAU06 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)
# DIAN 1.1.-2021: DSAU10 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)
# DIAN 1.1.-2021: DSAU14 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)
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):
fexml._do_set_billing_reference(reference, 'cac:CreditNoteDocumentReference')
fexml._do_set_billing_reference(
reference, 'cac:CreditNoteDocumentReference')
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):
@@ -272,10 +323,12 @@ class DIANSupportDocumentXML(fe.FeXML):
fexml.set_element('./cac:BillingReference/%s/cbc:ID' % (tag_document),
reference.ident)
fexml.set_element('./cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID',
fexml.set_element(
'./cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID',
reference.uuid,
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"))
def set_billing_reference(fexml, invoice):
@@ -319,21 +372,22 @@ class DIANSupportDocumentXML(fe.FeXML):
for invoice_line in invoice.invoice_lines:
for subtotal in invoice_line.tax.subtotals:
if subtotal.scheme is not None:
tax_amount_for[subtotal.scheme.code]['tax_amount'] += subtotal.tax_amount
tax_amount_for[subtotal.scheme.code]['taxable_amount'] += invoice_line.taxable_amount
tax_amount_for[
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
percent_for[subtotal.scheme.code] = subtotal.percent
total_tax_amount += subtotal.tax_amount
if total_tax_amount != Amount(0.0):
fexml.placeholder_for('./cac:TaxTotal')
fexml.set_element_amount('./cac:TaxTotal/cbc:TaxAmount',
total_tax_amount)
for index, item in enumerate(tax_amount_for.items()):
cod_impuesto, amount_of = item
next_append = index > 0
@@ -347,12 +401,14 @@ class DIANSupportDocumentXML(fe.FeXML):
tax_amount)
# DIAN 1.7.-2020: FAS05
fexml.set_element_amount_for(line,
fexml.set_element_amount_for(
line,
'/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
amount_of['taxable_amount'])
# DIAN 1.7.-2020: FAU06
fexml.set_element_amount_for(line,
fexml.set_element_amount_for(
line,
'/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
amount_of['tax_amount'])
@@ -361,18 +417,19 @@ class DIANSupportDocumentXML(fe.FeXML):
line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cbc:Percent',
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])
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)
line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
'IVA')
line.set_element(
'/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', 'IVA')
# abstract method
def tag_document(fexml):
return 'Invoice'
@@ -385,20 +442,33 @@ class DIANSupportDocumentXML(fe.FeXML):
'./cac:WithholdingTaxTotal/cbc:TaxAmount',
invoice_line.withholding_amount)
# DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(line,
fexml.set_element_amount_for(
line,
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.withholding_taxable_amount)
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:
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:
# DIAN 1.7.-2020: FAX15
line.set_element('./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)
line.set_element(
'./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):
fexml.set_element_amount_for(line,
@@ -406,28 +476,48 @@ class DIANSupportDocumentXML(fe.FeXML):
invoice_line.tax_amount)
# DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for(line,
fexml.set_element_amount_for(
line,
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.taxable_amount)
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:
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:
# DIAN 1.7.-2020: FAX15
line.set_element('./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)
line.set_element(
'./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):
next_append = False
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
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,
'./cbc:LineExtensionAmount',
invoice_line.total_amount)
@@ -435,27 +525,34 @@ class DIANSupportDocumentXML(fe.FeXML):
period = line.fragment('./cac:InvoicePeriod')
period.set_element('./cbc:StartDate',
datetime.now().strftime('%Y-%m-%d'))
period.set_element('./cbc:DescriptionCode',
'1')
period.set_element(
'./cbc:DescriptionCode', '1')
period.set_element('./cbc:Description',
'Por operación')
if not isinstance(invoice_line.tax, TaxTotalOmit):
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)
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,
schemeID=invoice_line.item.scheme_id,
schemeName=invoice_line.item.scheme_name,
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(
'./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',
invoice_line.quantity,
@@ -463,25 +560,34 @@ class DIANSupportDocumentXML(fe.FeXML):
for idx, charge in enumerate(invoice_line.allowance_charge):
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):
for idx, charge in enumerate(invoice.invoice_allowance_charge):
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):
line = parent.fragment('./cac:AllowanceCharge', append=append)
# DIAN 1.7.-2020: FAQ02
line.set_element('./cbc:ID', idx)
# 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:
line.set_element('./cbc:AllowanceChargeReasonCode', charge.reason.code)
line.set_element('./cbc:allowanceChargeReason', charge.reason.reason)
line.set_element('./cbc:MultiplierFactorNumeric', str(round(charge.multiplier_factor_numeric, 2)))
line.set_element(
'./cbc:AllowanceChargeReasonCode',
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:BaseAmount', charge.base_amount)
fexml.set_element_amount_for(
line, './cbc:BaseAmount', charge.base_amount)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
@@ -489,25 +595,37 @@ class DIANSupportDocumentXML(fe.FeXML):
fexml.placeholder_for('./ext:UBLExtensions')
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:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
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()),
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: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'))
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'))
fexml.customize(invoice)

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
from dataclasses import dataclass
from datetime import datetime
import hashlib
import typing
from .. import fe
from .. import form
@@ -86,13 +86,13 @@ class NumeroSecuencia:
@dataclass
class Periodo:
fecha_ingreso: typing.Union[str, Fecha]
fecha_liquidacion_inicio: typing.Union[str, Fecha]
fecha_liquidacion_fin: typing.Union[str, Fecha]
fecha_generacion: typing.Union[str, Fecha]
fecha_ingreso: str | Fecha
fecha_liquidacion_inicio: str | Fecha
fecha_liquidacion_fin: str | Fecha
fecha_generacion: str | Fecha
tiempo_laborado: int = 1
fecha_retiro: typing.Union[str, Fecha] = None
fecha_retiro: str | Fecha | None = None
def __post_init__(self):
self.fecha_ingreso = Fecha.cast(self.fecha_ingreso)
@@ -259,7 +259,7 @@ class InformacionGeneral:
def __str__(self):
self.valor
fecha_generacion: typing.Union[str, Fecha]
fecha_generacion: str | Fecha
hora_generacion: str
periodo_nomina: PeriodoNomina
tipo_moneda: TipoMoneda

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List
from ..amount import Amount
from .devengado import Devengado
@@ -30,7 +30,7 @@ class DevengadoHoraExtra:
@dataclass
class DevengadoHorasExtrasDiarias(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HEDs')
@@ -39,7 +39,7 @@ class DevengadoHorasExtrasDiarias(Devengado):
@dataclass
class DevengadoHorasExtrasNocturnas(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HENs')
@@ -49,7 +49,7 @@ class DevengadoHorasExtrasNocturnas(Devengado):
@dataclass
class DevengadoHorasRecargoNocturno(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRNs')
@@ -58,7 +58,7 @@ class DevengadoHorasRecargoNocturno(Devengado):
@dataclass
class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HEDDFs')
@@ -67,7 +67,7 @@ class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado):
@dataclass
class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRDDFs')
@@ -76,7 +76,7 @@ class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado):
@dataclass
class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HENDFs')
@@ -85,7 +85,7 @@ class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado):
@dataclass
class DevengadoHorasRecargoNocturnoDominicalesYFestivos(Devengado):
horas_extras: List[DevengadoHoraExtra]
horas_extras: list[DevengadoHoraExtra]
def apply(self, fragment):
hora_extra_xml = fragment.fragment('./HRNDFs')

View File

@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from ..amount import Amount
@@ -29,7 +29,7 @@ class Trabajador:
codigo_trabajador: 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):
fragment.set_attributes('./Trabajador',

15
requirements_dev.txt Normal file
View File

@@ -0,0 +1,15 @@
attrs==24.2.0
distlib==0.3.9
filelock==3.16.1
iniconfig==2.0.0
packaging==24.1
platformdirs==4.3.6
pluggy==1.5.0
pyparsing==3.2.0
pytest==8.3.4
semantic-version==2.10.0
setuptools-rust==1.10.2
six==1.16.0
tomli==2.2.1
tox==4.23.2
virtualenv==20.28.0

View File

@@ -22,4 +22,4 @@ exclude = docs
test = 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:
history = history_file.read()
requirements = ['Click>=8.1.7',
'zeep>=4.2.1',
'lxml>=5.2.2',
'cryptography>=41.0.0',
'pyOpenSSL>=23.2.0',
'xmlsig>=0.1.9',
'xades>=1.0.0',
'xmlsec>=1.3.12',
'python-dateutil>=2.9.0',
# usamos esta dependencia en runtime
# para forzar uso de policy_id de archivo local
'mock>=5.1.0',
'xmlschema>=3.0.0']
"""
Listado de Versiones Anteriores
requirements = ['Click>=6.0',
'zeep==4.0.0',
'lxml==4.6.3',
@@ -26,6 +42,8 @@ requirements = ['Click>=6.0',
'mock>=2.0.0',
'xmlschema>=1.8']
"""
setup_requirements = ['pytest-runner', ]
test_requirements = ['pytest', ]
@@ -39,10 +57,10 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Natural Language :: English',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
],
description="Facturacion Electronica Colombia",
entry_points={

1
tests/cude.txt Normal file
View File

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

0
tests/cufe.txt Normal file
View File

View File

@@ -4,16 +4,18 @@ from datetime import datetime
@pytest.fixture
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_issue(datetime.now())
inv.set_ident('ABC123')
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(
name='facho-supplier',
ident=form.PartyIdentification('123', '', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code='48',
organization_code='1',
address=form.Address(
@@ -24,7 +26,7 @@ def simple_debit_note_without_lines():
inv.set_customer(form.Party(
name='facho-customer',
ident=form.PartyIdentification('321', '', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code=form.Responsability(['ZZ']),
responsability_regime_code='48',
organization_code='1',
address=form.Address(
@@ -45,7 +47,7 @@ def simple_credit_note_without_lines():
inv.set_supplier(form.Party(
name = 'facho-supplier',
ident = form.PartyIdentification('123','', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48',
organization_code = '1',
address = form.Address(
@@ -56,7 +58,7 @@ def simple_credit_note_without_lines():
inv.set_customer(form.Party(
name = 'facho-customer',
ident = form.PartyIdentification('321', '', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48',
organization_code = '1',
address = form.Address(
@@ -77,7 +79,7 @@ def simple_invoice_without_lines():
inv.set_supplier(form.Party(
name = 'facho-supplier',
ident = form.PartyIdentification('123','', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48',
organization_code = '1',
address = form.Address(
@@ -88,7 +90,7 @@ def simple_invoice_without_lines():
inv.set_customer(form.Party(
name = 'facho-customer',
ident = form.PartyIdentification('321', '', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48',
organization_code = '1',
address = form.Address(
@@ -98,6 +100,7 @@ def simple_invoice_without_lines():
))
return inv
@pytest.fixture
def simple_invoice():
inv = form.NationalSalesInvoice()
@@ -109,7 +112,7 @@ def simple_invoice():
inv.set_supplier(form.Party(
name = 'facho-supplier',
ident = form.PartyIdentification('123','', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48',
organization_code = '1',
address = form.Address(
@@ -120,7 +123,7 @@ def simple_invoice():
inv.set_customer(form.Party(
name = 'facho-customer',
ident = form.PartyIdentification('321','', '31'),
responsability_code = form.Responsability(['O-07']),
responsability_code = form.Responsability(['ZZ']),
responsability_regime_code = '48',
organization_code = '1',
address = form.Address(
@@ -128,6 +131,7 @@ def simple_invoice():
form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia'))
))
inv.add_invoice_line(form.InvoiceLine(
quantity=form.Quantity(1, '94'),
description='productofacho',
@@ -139,8 +143,8 @@ def simple_invoice():
subtotals=[
form.TaxSubTotal(
percent=19.0,
)
]
)
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
return inv

View File

@@ -2,16 +2,15 @@
# -*- 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.
from datetime import datetime
# from datetime import datetime
import pytest
from facho.fe import form_xml
# import pytest
# from facho.fe import form_xml
import helpers
# import helpers
def test_xml_with_required_elements():
doc = form_xml.AttachedDocument(id='123')
xml = doc.toFachoXML()
assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123'
# def test_xml_with_required_elements():
# doc = form_xml.AttachedDocument(id='123')
# xml = doc.toFachoXML()
# assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123'

View File

@@ -4,21 +4,26 @@
# this repository contains the full copyright notices and license terms.
"""Tests for `facho` package."""
import pytest
from facho.fe.data.dian import codelist
def test_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():
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():
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():
assert codelist.Departamento['05']['name'] == 'Antioquia'

View File

@@ -5,8 +5,8 @@
from datetime import datetime
import pytest
from facho import fe
from facho import fe
import helpers
@@ -116,3 +116,20 @@ def test_xml_sign_dian_using_bytes(monkeypatch):
xmlsigned = signer.sign_xml_string(xmlstring)
assert "Signature" in xmlsigned
def test_xml_signature_timestamp(monkeypatch):
xml = fe.FeXML(
'Invoice',
'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)
with open('invoice.xml', 'w') as file_:
file_.write(xmlsigned)

View File

@@ -5,24 +5,47 @@
"""Tests for `facho` package."""
import pytest
from datetime import datetime
import io
import zipfile
import facho.fe.form as form
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):
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
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
@@ -42,9 +65,11 @@ def test_invoicesimple_xml_signed(monkeypatch, simple_invoice):
print(xml.tostring())
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
def test_invoicesimple_zip(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):
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
def test_invoice_line_count_numeric(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)
def test_invoice_profileexecutionid(simple_invoice):
xml_invoice = DIANInvoiceXML(simple_invoice)
cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice)
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
def test_invoice_invoice_type_code(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
def test_invoice_totals(simple_invoice_without_lines):
simple_invoice = simple_invoice_without_lines
simple_invoice.invoice_ident = '323200000129'
@@ -102,21 +135,30 @@ def test_invoice_totals(simple_invoice_without_lines):
form.TaxSubTotal(
scheme=form.TaxScheme('01'),
percent=19.0
)])
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
simple_invoice.calculate()
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_785_000) == simple_invoice.invoice_legal_monetary_total.payable_amount
assert form.Amount(1_500_000) == (
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):
simple_invoice = simple_invoice_without_lines
simple_invoice.invoice_ident = '323200000129'
simple_invoice.invoice_issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_supplier.ident = form.PartyIdentification('700085371', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31')
simple_invoice.invoice_issue = datetime.strptime(
'2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
simple_invoice.invoice_supplier.ident = form.PartyIdentification(
'700085371', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification(
'800199436', '5', '31')
simple_invoice.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1.00, '94'),
quantity=form.Quantity(
1.00, '94'),
description='producto',
item=form.StandardItem(111),
price=form.Price(form.Amount(1_500_000), '01', ''),
@@ -125,7 +167,9 @@ def test_invoice_cufe(simple_invoice_without_lines):
form.TaxSubTotal(
scheme=form.TaxScheme('01'),
percent=19.0
)])
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
simple_invoice.calculate()
@@ -137,61 +181,82 @@ def test_invoice_cufe(simple_invoice_without_lines):
clave_tecnica='693ff6f2a553c3646a063436fd4dd9ded0311471'
)
formatVars = cufe_extension.formatVars()
# NumFac
assert formatVars[0] == '323200000129', "NumFac"
# FecFac
assert formatVars[1] == '2019-01-16', "FecFac"
# HoraFac
assert formatVars[2] == '10:53:10-05:00', "HoraFac"
# ValorBruto
assert formatVars[3] == '1500000.00', "ValorBruto"
# CodImpuesto1
assert formatVars[4] == '01', "CodImpuesto1"
# ValorImpuesto1
assert formatVars[5] == '285000.00', "ValorImpuesto1"
# CodImpuesto2
assert formatVars[6] == '04', "CodImpuesto2"
# ValorImpuesto2
assert formatVars[7] == '0.00', "ValorImpuesto2"
# CodImpuesto3
assert formatVars[8] == '03', "CodImpuesto3"
# ValorImpuesto3
assert formatVars[9] == '0.00', "ValorImpuesto3"
# ValTotFac
assert formatVars[10] == '1785000.00', "ValTotFac"
# NitOFE
assert formatVars[11] == '700085371', "NitOFE"
# NumAdq
assert formatVars[12] == '800199436', "NumAdq"
# ClTec
assert formatVars[13] == '693ff6f2a553c3646a063436fd4dd9ded0311471', "ClTec"
# TipoAmbiente
assert formatVars[14] == '1', "TipoAmbiente"
xml_invoice.add_extension(cufe_extension)
cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID')
# RESOLUCION 004: pagina 689
assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4'
assert cufe == CUFE_
def test_credit_note_cude(simple_credit_note_without_lines):
simple_invoice = simple_credit_note_without_lines
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_supplier.ident = form.PartyIdentification('900373076', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification('8355990', '5', '31')
simple_invoice.invoice_issue = datetime.strptime(
'2019-01-12 07:00:00-05:00', '%Y-%m-%d %H:%M:%S%z')
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(
quantity = form.Quantity(1, '94'),
quantity=form.Quantity(
1, '94'),
description='producto',
item=form.StandardItem(111),
price = form.Price(form.Amount(5_000), '01', ''),
price=form.Price(
form.Amount(5_000), '01', ''),
tax=form.TaxTotal(
subtotals=[
form.TaxSubTotal(
scheme=form.TaxScheme('01'),
percent=19.0
)])
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
simple_invoice.calculate()
@@ -206,16 +271,20 @@ def test_credit_note_cude(simple_credit_note_without_lines):
xml_invoice.add_extension(cude_extension)
cude = xml_invoice.get_element_text('/fe:CreditNote/cbc:UUID')
# pag 612
assert cude == '907e4444decc9e59c160a2fb3b6659b33dc5b632a5008922b9a62f83f757b1c448e47f5867f2b50dbdb96f48c7681168'
assert cude == CUDE_
# pag 614
def test_debit_note_cude(simple_debit_note_without_lines):
simple_invoice = simple_debit_note_without_lines
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_supplier.ident = form.PartyIdentification('900197264', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification('10254102', '5', '31')
simple_invoice.invoice_issue = datetime.strptime(
'2019-01-18 10:58:00-05:00', '%Y-%m-%d %H:%M:%S%z')
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(
quantity=form.Quantity(1, '94'),
description='producto',
@@ -226,7 +295,9 @@ def test_debit_note_cude(simple_debit_note_without_lines):
form.TaxSubTotal(
scheme=form.TaxScheme('04'),
percent=8.0
)])
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
simple_invoice.calculate()
@@ -251,7 +322,6 @@ def test_debit_note_cude(simple_debit_note_without_lines):
assert build_vars['Software-PIN'] == '10201'
assert build_vars['TipoAmb'] == 2
cude_composicion = "".join(cude_extension.formatVars())
assert cude_composicion == 'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.0090019726410254102102012'

View File

@@ -6,12 +6,24 @@
"""Tests for `facho` package."""
import pytest
from datetime import datetime
import io
import zipfile
# from datetime import datetime
# import io
# import zipfile
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():
inv = form.NationalSalesInvoice()
@@ -28,19 +40,27 @@ def test_invoice_legalmonetary():
subtotals=[
form.TaxSubTotal(
percent=19.0,
)
]
)
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0)
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == 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)
assert inv.invoice_legal_monetary_total.line_extension_amount == (
form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == (
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():
discount = form.AllowanceChargeAsDiscount(amount=form.Amount(1000.0))
assert discount.isDiscount() == True
assert discount.isDiscount()
def test_FAU10():
inv = form.NationalSalesInvoice()
@@ -56,18 +76,22 @@ def test_FAU10():
tax=form.TaxTotal(
subtotals=[
form.TaxSubTotal(
percent = 19.0,
)
]
)
percent=19.0)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0)
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == 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)
assert inv.invoice_legal_monetary_total.line_extension_amount == (
form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_exclusive_amount == (
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():
@@ -85,12 +109,14 @@ def test_FAU14():
subtotals=[
form.TaxSubTotal(
percent=19.0,
)
]
)
)]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.add_prepaid_payment(form.PrePaidPayment(paid_amount = form.Amount(50.0)))
inv.add_allowance_charge(form.AllowanceCharge(
amount=form.Amount(19.0)))
inv.add_prepaid_payment(form.PrePaidPayment(
paid_amount=form.Amount(50.0)))
inv.calculate()
wants = form.Amount(119.0 + 19.0 - 50.0)
@@ -108,6 +134,7 @@ def test_invalid_tipo_operacion_nota_debito():
with pytest.raises(ValueError):
inv.set_operation_type(22)
def test_valid_tipo_operacion_nota_debito():
reference = form.InvoiceDocumentReference(
ident='11111',
@@ -117,16 +144,19 @@ def test_valid_tipo_operacion_nota_debito():
inv = form.DebitNote(reference)
inv.set_operation_type('30')
def test_invalid_tipo_operacion_nota_credito():
reference = form.InvoiceDocumentReference(
ident='11111',
uuid='21312312',
date='2020-05-05'
)
inv = form.DebitNote(reference)
with pytest.raises(ValueError):
inv.set_operation_type('990')
def test_valid_tipo_operacion_nota_credito():
reference = form.InvoiceDocumentReference(
ident='11111',
@@ -141,6 +171,7 @@ def test_quantity():
quantity1 = form.Quantity(10, '94')
assert quantity1 * form.Amount(3) == form.Amount(30)
def test_invoice_line_quantity_without_taxes():
line = form.InvoiceLine(
quantity=form.Quantity(10, '94'),
@@ -149,13 +180,16 @@ def test_invoice_line_quantity_without_taxes():
price=form.Price(
amount=form.Amount(30.00),
type_code='01',
type = 'x'
),
tax = form.TaxTotal(subtotals=[]))
type='x'),
tax=form.TaxTotal(subtotals=[]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
)
line.calculate()
assert line.total_amount == form.Amount(300)
assert line.tax_amount == form.Amount(0)
def test_invoice_legalmonetary_with_taxes():
inv = form.NationalSalesInvoice()
inv.add_invoice_line(form.InvoiceLine(
@@ -167,15 +201,22 @@ def test_invoice_legalmonetary_with_taxes():
type_code='01',
type='x'
),
tax = form.TaxTotal(subtotals=[])
tax=form.TaxTotal(subtotals=[]),
withholding=form.WithholdingTaxTotal(
subtotals=[])
))
inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0)
assert inv.invoice_legal_monetary_total.tax_exclusive_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)
assert inv.invoice_legal_monetary_total.line_extension_amount == (
form.Amount(100.0))
assert inv.invoice_legal_monetary_total.tax_exclusive_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():
@@ -183,6 +224,7 @@ def test_invoice_ident_prefix_automatic_invalid():
with pytest.raises(ValueError):
inv.set_ident('SETPQJQJ1234567')
def test_invoice_ident_prefix_automatic():
inv = form.NationalSalesInvoice()
inv.set_ident('SETP1234567')
@@ -200,12 +242,14 @@ def test_invoice_ident_prefix_automatic():
inv.set_ident('1234567')
assert inv.invoice_ident_prefix == ''
def test_invoice_ident_prefix_manual():
inv = form.NationalSalesInvoice()
inv.set_ident('SETP1234567')
inv.set_ident_prefix('SETA')
assert inv.invoice_ident_prefix == 'SETA'
def test_invoice_ident_prefix_automatic_debit():
inv = form.DebitNote(form.BillingReference('', '', ''))
inv.set_ident('ABCDEF1234567')

View File

@@ -6,13 +6,24 @@
"""Tests for `facho` package."""
import pytest
from datetime import datetime
# from datetime import datetime
import copy
from facho.fe import form
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():
try:
@@ -27,70 +38,82 @@ def test_import_DIANDebitNoteXML():
except AttributeError:
pytest.fail("unexpected not found")
def test_import_DIANCreditNoteXML():
try:
form_xml.DIANCreditNoteXML
except AttributeError:
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)
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'
# 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='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 = 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,
)
]
),
allowance_charge = [
form.AllowanceChargeAsDiscount(amount=form.Amount(10.0))
]
))
inv.calculate()
# inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
# inv.calculate()
# se aplico descuento
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(90.0)
# xml = form_xml.DIANInvoiceXML(inv)
# 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):
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'
# def test_allowance_charge_in_invoice_line(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,
# )]),
# 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'

View File

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

View File

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

11
tox.ini
View File

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