1 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
35 changed files with 584 additions and 2939 deletions

View File

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

View File

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

View File

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

View File

@@ -140,12 +140,12 @@ class LXMLBuilder:
attrs['pretty_print'] = attrs.pop('pretty_print', False) attrs['pretty_print'] = attrs.pop('pretty_print', False)
attrs['encoding'] = attrs.pop('encoding', 'UTF-8') attrs['encoding'] = attrs.pop('encoding', 'UTF-8')
for el in elem.getiterator(): for el in elem.iter():
keys = filter(lambda key: key.startswith('facho_'), el.keys()) keys = filter(lambda key: key.startswith('facho_'), el.keys())
self.remove_attributes(el, keys, exclude=['facho_optional']) self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True' is_optional = el.get('facho_optional', 'False') == 'True'
if is_optional and el.getchildren() == [] and el.keys() == [ if is_optional and list(el) == [] and el.keys() == [
'facho_optional']: 'facho_optional']:
el.getparent().remove(el) el.getparent().remove(el)
@@ -291,7 +291,7 @@ class FachoXML:
# se fuerza la adicion como un nuevo elemento # se fuerza la adicion como un nuevo elemento
if append: if append:
last_slibing = None last_slibing = None
for child in parent.getchildren(): for child in list(parent):
if child.tag == node_tag: if child.tag == node_tag:
last_slibing = child last_slibing = child
@@ -389,12 +389,94 @@ class FachoXML:
def get_element(self, xpath, multiple=False): def get_element(self, xpath, multiple=False):
xpath = self.fragment_prefix + self._path_xpath_for(xpath) xpath = self.fragment_prefix + self._path_xpath_for(xpath)
elem = self.builder.xpath(self.root, xpath) return self.builder.xpath(self.root, xpath, multiple=multiple)
if elem is None:
raise AttributeError('xpath %s invalid' % (xpath))
def get_element_text(self, xpath, format_=str, multiple=False):
xpath = self.fragment_prefix + self._path_xpath_for(xpath)
# MACHETE(bit4bit) al usar ./ queda ../
xpath = re.sub(r'^\.\.+', '.', xpath)
elem = self.builder.xpath(self.root, xpath, multiple=multiple)
if multiple:
vals = []
for e in elem:
text = self.builder.get_text(e)
if text is not None:
vals.append(format_(text))
return vals
else:
text = self.builder.get_text(elem) text = self.builder.get_text(elem)
return str(text) if text is None:
return None
return format_(text)
def get_element_text_or_attribute(
self, xpath, default=None, multiple=False, raise_on_fail=False):
parts = xpath.split('/')
is_attribute = parts[-1].startswith('@')
if is_attribute:
attribute_name = parts.pop(-1).lstrip('@')
element_path = "/".join(parts)
try:
val = self.get_element_attribute(
element_path, attribute_name, multiple=multiple)
if val is None:
return default
return val
except KeyError as e:
if raise_on_fail:
raise e
return default
except ValueError as e:
if raise_on_fail:
raise e
return default
else:
try:
val = self.get_element_text(xpath, multiple=multiple)
if val is None:
return default
return val
except ValueError as e:
if raise_on_fail:
raise e
return default
def get_elements_text_or_attributes(self, xpaths, raise_on_fail=True):
"""
returna el contenido o attributos de un conjunto de XPATHS
si algun XPATH es una tupla se retorna el primer elemento del mismo.
"""
vals = []
for xpath in xpaths:
if isinstance(xpath, tuple):
val = xpath[0]
else:
val = self.get_element_text_or_attribute(
xpath, raise_on_fail=raise_on_fail)
vals.append(val)
return vals
def exist_element(self, xpath):
elem = self.get_element(xpath)
# no se encontro elemento
if elem is None:
return False
# el placeholder no ha sido populado
if elem.get('facho_placeholder') == 'True':
return False
# el valor opcional no ha sido populado
if elem.get('facho_optional') == 'True':
return False
return True
def _remove_facho_attributes(self, elem):
self.builder.remove_attributes(
elem, ['facho_optional', 'facho_placeholder'])
def tostring(self, **kw): def tostring(self, **kw):
return self.builder.tostring(self.root, **kw) return self.builder.tostring(self.root, **kw)

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import uuid
import xmlsig import xmlsig
import xades import xades
from datetime import datetime from datetime import datetime
import OpenSSL
import zipfile import zipfile
# import warnings # import warnings
import hashlib import hashlib
@@ -18,6 +17,21 @@ from dateutil import tz
from cryptography.hazmat.primitives.serialization import pkcs12 from cryptography.hazmat.primitives.serialization import pkcs12
# Monkey-patch xades/xmlsig para compatibilidad con pyOpenSSL >= 24
# (PKCS12 fue eliminado de OpenSSL.crypto, pero ambas librerías lo referencian)
import OpenSSL
import xades.xades_context
if not hasattr(OpenSSL.crypto, 'PKCS12'):
def _patched_load_pkcs12(self, key):
if isinstance(key, tuple):
self.x509 = key[1]
self.public_key = key[1].public_key()
self.private_key = key[0]
else:
raise NotImplementedError("unsupported key type")
xades.xades_context.XAdESContext.load_pkcs12 = _patched_load_pkcs12
AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
@@ -57,8 +71,6 @@ Bogota = tz.gettz('America/Bogota')
NAMESPACES = { NAMESPACES = {
'apr': 'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2',
'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1', 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1',
'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
@@ -108,21 +120,17 @@ class FeXML(FachoXML):
def tostring(self, **kw): def tostring(self, **kw):
# MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un namespace # MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un namespace
urn_oasis = {
'AttachedDocument': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2',
'Invoice': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
'CreditNote': 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2',
'ApplicationResponse': 'urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2'
}
root_namespace = self.root_namespace() root_namespace = self.root_namespace()
root_localname = self.root_localname() root_localname = self.root_localname()
xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace] xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace]
if root_localname == 'Invoice':
urn_oasis = 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
if root_localname == 'CreditNote':
urn_oasis = 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2'
return super().tostring(**kw)\ return super().tostring(**kw)\
.replace(xmlns_name + ':', '')\ .replace(xmlns_name + ':', '')\
.replace('xmlns:'+xmlns_name, 'xmlns')\ .replace('xmlns:'+xmlns_name, 'xmlns')\
.replace(root_namespace, urn_oasis[root_localname]) .replace(root_namespace, urn_oasis)
class DianXMLExtensionCUDFE(FachoXMLExtension): class DianXMLExtensionCUDFE(FachoXMLExtension):
@@ -450,9 +458,6 @@ class DianXMLExtensionSigner:
self._pkcs12_data, self._pkcs12_data,
self._passphrase)) self._passphrase))
# ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(
# self._pkcs12_data,
# self._passphrase))
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():
ctx.sign(signature) ctx.sign(signature)
@@ -596,8 +601,8 @@ class DianXMLExtensionSignerVerifier:
if isinstance(self._pkcs12_path_or_bytes, str): if isinstance(self._pkcs12_path_or_bytes, str):
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read() pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
ctx = xades.XAdESContext() ctx = xades.XAdESContext()
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data, ctx.load_pkcs12(pkcs12.load_key_and_certificates(
self._passphrase)) pkcs12_data, self._passphrase))
try: try:
if self._localpolicy: if self._localpolicy:
with mock_xades_policy(): with mock_xades_policy():

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
from .. import fe from .. import fe
from ..form import * from ..form import *
from collections import defaultdict from collections import defaultdict
from .attached_document import AttachedDocument
__all__ = ['DIANInvoiceXML'] __all__ = ['DIANInvoiceXML']
@@ -22,147 +21,79 @@ class DIANInvoiceXML(fe.FeXML):
# ZE02 se requiere existencia para firmar # ZE02 se requiere existencia para firmar
ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True) ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True)
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
self.attach_invoice = self.attach_invoice(invoice) self.attach_invoice(invoice)
# self.attach_document = self.attached_document_invoice(attach_invoice)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None"""
fexml.placeholder_for('./ext:UBLExtensions')
fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1')
fexml.set_element(
'./cbc:CustomizationID', invoice.invoice_operation_type)
fexml.placeholder_for('./cbc:ProfileID')
fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
# DIAN 1.7.-2020: FAD10
fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00'))
fexml.set_element(
'./cbc:%sTypeCode' % (fexml.tag_document()),
invoice.invoice_type_code,
listAgencyID='195',
listAgencyName='No matching global declaration available for the validation root',
listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
if fexml.tag_document() == 'Invoice':
fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (
fexml.tag_document()),
invoice.invoice_period_start.strftime(
'%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (
fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_billing_reference(invoice)
fexml.customize(invoice)
fexml.set_supplier(invoice)
fexml.set_customer(invoice)
fexml.set_payment_mean(invoice)
fexml.set_invoice_totals(invoice)
fexml.set_legal_monetary(invoice)
fexml.set_invoice_lines(invoice)
fexml.set_allowance_charge(invoice)
return fexml
def attached_document_invoice(fexml, invoice):
attach_invoice = fexml.attach_invoice(invoice)
attached_document = AttachedDocument(
invoice, '123', attach_invoice)
return attached_document
def set_supplier(fexml, invoice): def set_supplier(fexml, invoice):
fexml.placeholder_for('./cac:AccountingSupplierParty') fexml.placeholder_for('./cac:AccountingSupplierParty')
#DIAN 1.7.-2020: CAJ02 #DIAN 1.7.-2020: CAJ02
#DIAN 1.7.-2020: FAJ02 #DIAN 1.7.-2020: FAJ02
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cbc:AdditionalAccountID',
'./cac:AccountingSupplierParty/cbc:AdditionalAccountID',
invoice.invoice_supplier.organization_code) invoice.invoice_supplier.organization_code)
#DIAN 1.7.-2020: CAJ06 #DIAN 1.7.-2020: CAJ06
#DIAN 1.7.-2020: FAJ06 #DIAN 1.7.-2020: FAJ06
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name',
invoice.invoice_supplier.name) invoice.invoice_supplier.name)
#DIAN 1.7.-2020: CAJ07, CAJ08 #DIAN 1.7.-2020: CAJ07, CAJ08
#DIAN 1.7.-2020: FAJ07 #DIAN 1.7.-2020: FAJ07
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address')
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address')
#DIAN 1.7.-2020: FAJ08 #DIAN 1.7.-2020: FAJ08
#DIAN 1.7.-2020: CAJ09 #DIAN 1.7.-2020: CAJ09
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
invoice.invoice_supplier.address.city.code) invoice.invoice_supplier.address.city.code)
#DIAN 1.7.-2020: FAJ09 #DIAN 1.7.-2020: FAJ09
#DIAN 1.7.-2020: CAJ10 #DIAN 1.7.-2020: CAJ10
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName',
invoice.invoice_supplier.address.city.name) invoice.invoice_supplier.address.city.name)
#DIAN 1.7.-2020: FAJ11 #DIAN 1.7.-2020: FAJ11
#DIAN 1.7.-2020: CAJ11 #DIAN 1.7.-2020: CAJ11
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
invoice.invoice_supplier.address.countrysubentity.name) invoice.invoice_supplier.address.countrysubentity.name)
#DIAN 1.7.-2020: FAJ12 #DIAN 1.7.-2020: FAJ12
#DIAN 1.7.-2020: CAJ12 #DIAN 1.7.-2020: CAJ12
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
invoice.invoice_supplier.address.countrysubentity.code) invoice.invoice_supplier.address.countrysubentity.code)
#DIAN 1.7.-2020: FAJ14 #DIAN 1.7.-2020: FAJ14
#DIAN 1.7.-2020: CAJ13, CAJ14 #DIAN 1.7.-2020: CAJ13, CAJ14
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
invoice.invoice_supplier.address.street) invoice.invoice_supplier.address.street)
#DIAN 1.7.-2020: FAJ16 #DIAN 1.7.-2020: FAJ16
#DIAN 1.7.-2020: CAJ16, CAJ16 #DIAN 1.7.-2020: CAJ16, CAJ16
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
invoice.invoice_supplier.address.country.code) invoice.invoice_supplier.address.country.code)
#DIAN 1.7.-2020: FAJ17 #DIAN 1.7.-2020: FAJ17
#DIAN 1.7.-2020: CAJ17 #DIAN 1.7.-2020: CAJ17
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
'./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
invoice.invoice_supplier.address.country.name, invoice.invoice_supplier.address.country.name,
#DIAN 1.7.-2020: FAJ18 #DIAN 1.7.-2020: FAJ18
languageID = 'es') languageID = 'es')
supplier_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() supplier_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
supplier_company_id_attrs.update({ supplier_company_id_attrs.update({'schemeID': invoice.invoice_supplier.ident.dv,
'schemeID': invoice.invoice_supplier.ident.dv, 'schemeName': invoice.invoice_supplier.ident.type_fiscal})
'schemeName': invoice.invoice_supplier.ident.type_fiscal
})
#DIAN 1.7.-2020: FAJ19 #DIAN 1.7.-2020: FAJ19
#DIAN 1.7.-2020: CAJ19 #DIAN 1.7.-2020: CAJ19
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme')
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme')
#DIAN 1.7.-2020: FAJ20 #DIAN 1.7.-2020: FAJ20
#DIAN 1.7.-2020: CAJ20 #DIAN 1.7.-2020: CAJ20
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
invoice.invoice_supplier.legal_name) invoice.invoice_supplier.legal_name)
#DIAN 1.7.-2020: FAJ21 #DIAN 1.7.-2020: FAJ21
#DIAN 1.7.-2020: CAJ21 #DIAN 1.7.-2020: CAJ21
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
invoice.invoice_supplier.ident, invoice.invoice_supplier.ident,
#DIAN 1.7.-2020: FAJ22,FAJ23,FAJ24,FAJ25 #DIAN 1.7.-2020: FAJ22,FAJ23,FAJ24,FAJ25
**supplier_company_id_attrs) **supplier_company_id_attrs)
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
#DIAN 1.7.-2020: FAJ26 #DIAN 1.7.-2020: FAJ26
#DIAN 1.7.-2020: CAJ26 #DIAN 1.7.-2020: CAJ26
invoice.invoice_supplier.responsability_code, invoice.invoice_supplier.responsability_code,
@@ -171,36 +102,30 @@ class DIANInvoiceXML(fe.FeXML):
listName=invoice.invoice_supplier.responsability_regime_code) listName=invoice.invoice_supplier.responsability_regime_code)
#DIAN 1.7.-2020: FAJ28 #DIAN 1.7.-2020: FAJ28
#DIAN 1.7.-2020: CAJ28 #DIAN 1.7.-2020: CAJ28
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress')
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress')
#DIAN 1.7.-2020: FAJ29 #DIAN 1.7.-2020: FAJ29
#DIAN 1.7.-2020: CAJ29 #DIAN 1.7.-2020: CAJ29
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID',
invoice.invoice_supplier.address.city.code) invoice.invoice_supplier.address.city.code)
#DIAN 1.7.-2020: FAJ30 #DIAN 1.7.-2020: FAJ30
#DIAN 1.7.-2020: CAJ30 #DIAN 1.7.-2020: CAJ30
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName', invoice.invoice_supplier.address.city.name)
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName', invoice.invoice_supplier.address.city.name)
#DIAN 1.7.-2020: FAJ31 #DIAN 1.7.-2020: FAJ31
#DIAN 1.7.-2020: CAJ31 #DIAN 1.7.-2020: CAJ31
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity',
invoice.invoice_supplier.address.countrysubentity.name) invoice.invoice_supplier.address.countrysubentity.name)
#DIAN 1.7.-2020: FAJ32 #DIAN 1.7.-2020: FAJ32
#DIAN 1.7.-2020: CAJ32 #DIAN 1.7.-2020: CAJ32
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode',
invoice.invoice_supplier.address.countrysubentity.code) invoice.invoice_supplier.address.countrysubentity.code)
#DIAN 1.7.-2020: FAJ33,FAJ34 #DIAN 1.7.-2020: FAJ33,FAJ34
#DIAN 1.7.-2020: CAJ33,CAJ34 #DIAN 1.7.-2020: CAJ33,CAJ34
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line',
invoice.invoice_supplier.address.street) invoice.invoice_supplier.address.street)
#DIAN 1.7.-2020: FAJ35,FAJ36 #DIAN 1.7.-2020: FAJ35,FAJ36
@@ -210,129 +135,106 @@ class DIANInvoiceXML(fe.FeXML):
#DIAN 1.7.-2020: FAJ37,FAJ38 #DIAN 1.7.-2020: FAJ37,FAJ38
#DIAN 1.7.-2020: CAJ37,CAJ38 #DIAN 1.7.-2020: CAJ37,CAJ38
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name',
invoice.invoice_supplier.address.country.name, invoice.invoice_supplier.address.country.name,
languageID='es') languageID='es')
#DIAN 1.7.-2020: FAJ39 #DIAN 1.7.-2020: FAJ39
#DIAN 1.7.-2020: CAJ39 #DIAN 1.7.-2020: CAJ39
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
#DIAN 1.7.-2020: CAJ40 #DIAN 1.7.-2020: CAJ40
#DIAN 1.7.-2020: FAJ40 #DIAN 1.7.-2020: FAJ40
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code) invoice.invoice_customer.tax_scheme.code)
#DIAN 1.7.-2020: CAJ41 #DIAN 1.7.-2020: CAJ41
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name) invoice.invoice_customer.tax_scheme.name)
#DIAN 1.7.-2020: FAJ42 #DIAN 1.7.-2020: FAJ42
#DIAN 1.7.-2020: CAJ42 #DIAN 1.7.-2020: CAJ42
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity')
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity')
#DIAN 1.7.-2020: FAJ43 #DIAN 1.7.-2020: FAJ43
#DIAN 1.7.-2020: CAJ43 #DIAN 1.7.-2020: CAJ43
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName',
invoice.invoice_supplier.legal_name) invoice.invoice_supplier.legal_name)
#DIAN 1.7.-2020: FAJ44,FAJ45,FAJ46,FAJ47,FAJ48 #DIAN 1.7.-2020: FAJ44,FAJ45,FAJ46,FAJ47,FAJ48
#DIAN 1.7.-2020: CAJ44,CAJ45,CAJ46,CAJ47,CAJ48 #DIAN 1.7.-2020: CAJ44,CAJ45,CAJ46,CAJ47,CAJ48
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
invoice.invoice_supplier.ident, invoice.invoice_supplier.ident,
**supplier_company_id_attrs) **supplier_company_id_attrs)
#DIAN 1.7.-2020: FAJ49 #DIAN 1.7.-2020: FAJ49
#DIAN 1.7.-2020: CAJ49 #DIAN 1.7.-2020: CAJ49
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme')
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme')
#DIAN 1.7.-2020: FAJ50 #DIAN 1.7.-2020: FAJ50
#DIAN 1.7.-2020: CAJ50 #DIAN 1.7.-2020: CAJ50
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme/cbc:ID',
'./cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cac:CorporateRegistrationScheme/cbc:ID',
invoice.invoice_ident_prefix) invoice.invoice_ident_prefix)
#DIAN 1.7.-2020: CAJ67 #DIAN 1.7.-2020: CAJ67
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingSupplierParty/cac:Party/cac:Contact')
'./cac:AccountingSupplierParty/cac:Party/cac:Contact')
#DIAN 1.7.-2020: FAJ71 #DIAN 1.7.-2020: FAJ71
#DIAN 1.7.-2020: CAJ71 #DIAN 1.7.-2020: CAJ71
fexml.set_element( fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicMail',
'./cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicMail',
invoice.invoice_supplier.email) invoice.invoice_supplier.email)
def set_customer(fexml, invoice): def set_customer(fexml, invoice):
fexml.placeholder_for('./cac:AccountingCustomerParty') fexml.placeholder_for('./cac:AccountingCustomerParty')
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cbc:AdditionalAccountID',
'./cac:AccountingCustomerParty/cbc:AdditionalAccountID',
invoice.invoice_customer.organization_code) invoice.invoice_customer.organization_code)
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID',
invoice.invoice_customer.ident) invoice.invoice_customer.ident)
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name',
invoice.invoice_customer.name) invoice.invoice_customer.name)
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation')
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation')
customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy()
#DIAN 1.7.-2020: FAK25 #DIAN 1.7.-2020: FAK25
#DIAN 1.7.-2020: CAK25 #DIAN 1.7.-2020: CAK25
customer_company_id_attrs.update({ customer_company_id_attrs.update({'schemeID': invoice.invoice_customer.ident.dv,
'schemeID': invoice.invoice_customer.ident.dv,
'schemeName': invoice.invoice_customer.ident.type_fiscal}) 'schemeName': invoice.invoice_customer.ident.type_fiscal})
#DIAN 1.7.-2020: FAK07 #DIAN 1.7.-2020: FAK07
#DIAN 1.7.-2020: CAK07 #DIAN 1.7.-2020: CAK07
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address')
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address')
#DIAN 1.7.-2020: FAK08 #DIAN 1.7.-2020: FAK08
#DIAN 1.7.-2020: CAK08 #DIAN 1.7.-2020: CAK08
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID',
invoice.invoice_customer.address.city.code) invoice.invoice_customer.address.city.code)
#DIAN 1.7.-2020: FAK09 #DIAN 1.7.-2020: FAK09
#DIAN 1.7.-2020: CAK09 #DIAN 1.7.-2020: CAK09
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', invoice.invoice_customer.address.city.name)
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', invoice.invoice_customer.address.city.name)
#DIAN 1.7.-2020: FAK11 #DIAN 1.7.-2020: FAK11
#DIAN 1.7.-2020: CAK11 #DIAN 1.7.-2020: CAK11
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity',
invoice.invoice_customer.address.countrysubentity.name) invoice.invoice_customer.address.countrysubentity.name)
#DIAN 1.7.-2020: FAK12 #DIAN 1.7.-2020: FAK12
#DIAN 1.7.-2020: CAK12 #DIAN 1.7.-2020: CAK12
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode',
invoice.invoice_customer.address.countrysubentity.code) invoice.invoice_customer.address.countrysubentity.code)
#DIAN 1.7.-2020: CAK13, CAK14 #DIAN 1.7.-2020: CAK13, CAK14
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line',
invoice.invoice_customer.address.street) invoice.invoice_customer.address.street)
#DIAN 1.7.-2020: CAK16 #DIAN 1.7.-2020: CAK16
#DIAN 1.7.-2020: FAK16 #DIAN 1.7.-2020: FAK16
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode',
invoice.invoice_customer.address.country.code) invoice.invoice_customer.address.country.code)
#DIAN 1.7.-2020: FAK17 #DIAN 1.7.-2020: FAK17
#DIAN 1.7.-2020: CAK17 #DIAN 1.7.-2020: CAK17
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
'./cac:AccountingCustomerParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name',
invoice.invoice_customer.address.country.name, invoice.invoice_customer.address.country.name,
#DIAN 1.7.-2020: FAK18 #DIAN 1.7.-2020: FAK18
#DIAN 1.7.-2020: CAK18 #DIAN 1.7.-2020: CAK18
@@ -340,26 +242,22 @@ class DIANInvoiceXML(fe.FeXML):
#DIAN 1.7.-2020: FAK17,FAK19 #DIAN 1.7.-2020: FAK17,FAK19
#DIAN 1.7.-2020: CAK19 #DIAN 1.7.-2020: CAK19
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme')
#DIAN 1.7.-2020: FAK17,FAK20 #DIAN 1.7.-2020: FAK17,FAK20
#DIAN 1.7.-2020: CAK20 #DIAN 1.7.-2020: CAK20
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName',
invoice.invoice_customer.legal_name) invoice.invoice_customer.legal_name)
#DIAN 1.7.-2020: CAK21 #DIAN 1.7.-2020: CAK21
#DIAN 1.7.-2020: FAK21 #DIAN 1.7.-2020: FAK21
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
#DIAN 1.7.-2020: CAK22, CAK23, CAK24, CAK25 #DIAN 1.7.-2020: CAK22, CAK23, CAK24, CAK25
**customer_company_id_attrs) **customer_company_id_attrs)
#DIAN 1.7.-2020: CAK26 #DIAN 1.7.-2020: CAK26
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode',
#DIAN 1.7.-2020: FAK26 #DIAN 1.7.-2020: FAK26
invoice.invoice_customer.responsability_code, invoice.invoice_customer.responsability_code,
#DIAN 1.7.-2020: FAK27 #DIAN 1.7.-2020: FAK27
@@ -368,121 +266,98 @@ class DIANInvoiceXML(fe.FeXML):
#DIAN 1.7.-2020: FAK28 #DIAN 1.7.-2020: FAK28
#DIAN 1.7.-2020: CAK28 #DIAN 1.7.-2020: CAK28
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress')
#DIAN 1.7.-2020: FAK29 #DIAN 1.7.-2020: FAK29
#DIAN 1.7.-2020: CAK29 #DIAN 1.7.-2020: CAK29
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:ID',
invoice.invoice_customer.address.city.code) invoice.invoice_customer.address.city.code)
#DIAN 1.7.-2020: FAK30 #DIAN 1.7.-2020: FAK30
#DIAN 1.7.-2020: CAK30 #DIAN 1.7.-2020: CAK30
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CityName',
invoice.invoice_customer.address.city.name) invoice.invoice_customer.address.city.name)
#DIAN 1.7.-2020: FAK31 #DIAN 1.7.-2020: FAK31
#DIAN 1.7.-2020: CAK31 #DIAN 1.7.-2020: CAK31
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentity',
invoice.invoice_customer.address.countrysubentity.name) invoice.invoice_customer.address.countrysubentity.name)
#DIAN 1.7.-2020: FAK32 #DIAN 1.7.-2020: FAK32
#DIAN 1.7.-2020: CAK32 #DIAN 1.7.-2020: CAK32
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cbc:CountrySubentityCode',
invoice.invoice_customer.address.countrysubentity.code) invoice.invoice_customer.address.countrysubentity.code)
#DIAN 1.7.-2020: FAK33 #DIAN 1.7.-2020: FAK33
#DIAN 1.7.-2020: CAK33 #DIAN 1.7.-2020: CAK33
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine')
#DIAN 1.7.-2020: FAK34 #DIAN 1.7.-2020: FAK34
#DIAN 1.7.-2020: CAK34 #DIAN 1.7.-2020: CAK34
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:AddressLine/cbc:Line',
invoice.invoice_customer.address.street) invoice.invoice_customer.address.street)
#DIAN 1.7.-2020: CAK35 #DIAN 1.7.-2020: CAK35
#DIAN 1.7.-2020: FAK35 #DIAN 1.7.-2020: FAK35
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country')
#DIAN 1.7.-2020: CAK36 #DIAN 1.7.-2020: CAK36
#DIAN 1.7.-2020: FAK36 #DIAN 1.7.-2020: FAK36
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
invoice.invoice_customer.address.country.code) invoice.invoice_customer.address.country.code)
#DIAN 1.7.-2020: CAK37 #DIAN 1.7.-2020: CAK37
#DIAN 1.7.-2020: FAK37 #DIAN 1.7.-2020: FAK37
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name', invoice.invoice_customer.address.country.name)
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:Name', invoice.invoice_customer.address.country.name)
#DIAN 1.7.-2020: FAK38 #DIAN 1.7.-2020: FAK38
#DIAN 1.7.-2020: CAK38 #DIAN 1.7.-2020: CAK38
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:RegistrationAddress/cac:Country/cbc:IdentificationCode',
invoice.invoice_customer.address.country.code, invoice.invoice_customer.address.country.code,
languageID='es') languageID='es')
#DIAN 1.7.-2020: CAK39 #DIAN 1.7.-2020: CAK39
#DIAN 1.7.-2020: FAK39 #DIAN 1.7.-2020: FAK39
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme')
#DIAN 1.7.-2020: CAK40 Machete Construir Validación #DIAN 1.7.-2020: CAK40 Machete Construir Validación
#DIAN 1.7.-2020: FAK40 #DIAN 1.7.-2020: FAK40
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID',
invoice.invoice_customer.tax_scheme.code) invoice.invoice_customer.tax_scheme.code)
#DIAN 1.7.-2020: FAK41 #DIAN 1.7.-2020: FAK41
#DIAN 1.7.-2020: CAK41 Machete Construir Validación #DIAN 1.7.-2020: CAK41 Machete Construir Validación
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name',
invoice.invoice_customer.tax_scheme.name) invoice.invoice_customer.tax_scheme.name)
#DIAN 1.7.-2020: FAK42 #DIAN 1.7.-2020: FAK42
#DIAN 1.7.-2020: CAK42 #DIAN 1.7.-2020: CAK42
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
#DIAN 1.7.-2020: FAK43 #DIAN 1.7.-2020: FAK43
#DIAN 1.7.-2020: CAK43 #DIAN 1.7.-2020: CAK43
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName',
invoice.invoice_customer.legal_name) invoice.invoice_customer.legal_name)
#DIAN 1.7.-2020: CAK44 #DIAN 1.7.-2020: CAK44
#DIAN 1.7.-2020: FAK44,FAK45,FAK46,FAK47,FAK48 #DIAN 1.7.-2020: FAK44,FAK45,FAK46,FAK47,FAK48
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID',
invoice.invoice_customer.ident, invoice.invoice_customer.ident,
**customer_company_id_attrs) **customer_company_id_attrs)
fexml.placeholder_for( fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
'./cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity')
#DIAN 1.7.-2020: FAK55 #DIAN 1.7.-2020: FAK55
#DIAN 1.7.-2020: CAK51, CAK55 #DIAN 1.7.-2020: CAK51, CAK55
fexml.set_element( fexml.set_element('./cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicMail',
'./cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicMail',
invoice.invoice_customer.email) invoice.invoice_customer.email)
def set_payment_mean(fexml, invoice): def set_payment_mean(fexml, invoice):
payment_mean = invoice.invoice_payment_mean payment_mean = invoice.invoice_payment_mean
fexml.set_element( fexml.set_element('./cac:PaymentMeans/cbc:ID', payment_mean.id)
'./cac:PaymentMeans/cbc:ID', payment_mean.id) fexml.set_element('./cac:PaymentMeans/cbc:PaymentMeansCode', payment_mean.code)
fexml.set_element( fexml.set_element('./cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d'))
'./cac:PaymentMeans/cbc:PaymentMeansCode', payment_mean.code) fexml.set_element('./cac:PaymentMeans/cbc:PaymentID', payment_mean.payment_id)
fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentDueDate', payment_mean.due_at.strftime('%Y-%m-%d'))
fexml.set_element(
'./cac:PaymentMeans/cbc:PaymentID', payment_mean.payment_id)
def set_element_amount_for(fexml, xml, xpath, amount): def set_element_amount_for(fexml, xml, xpath, amount):
if not isinstance(amount, Amount): if not isinstance(amount, Amount):
@@ -631,43 +506,36 @@ class DIANInvoiceXML(fe.FeXML):
next_append = index > 0 next_append = index > 0
#DIAN 1.7.-2020: FAS01 #DIAN 1.7.-2020: FAS01
line = fexml.fragment( line = fexml.fragment('./cac:WithholdingTaxTotal', append=next_append)
'./cac:WithholdingTaxTotal', append=next_append)
#DIAN 1.7.-2020: FAU06 #DIAN 1.7.-2020: FAU06
tax_amount = amount_of['tax_amount'] tax_amount = amount_of['tax_amount']
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'/cac:WithholdingTaxTotal/cbc:TaxAmount', '/cac:WithholdingTaxTotal/cbc:TaxAmount',
tax_amount) tax_amount)
#DIAN 1.7.-2020: FAS05 #DIAN 1.7.-2020: FAS05
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
amount_of['taxable_amount']) amount_of['taxable_amount'])
#DIAN 1.7.-2020: FAU06 #DIAN 1.7.-2020: FAU06
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
amount_of['tax_amount']) amount_of['tax_amount'])
#DIAN 1.7.-2020: FAS07 #DIAN 1.7.-2020: FAS07
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
line.set_element( line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:Percent',
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
if percent_for[cod_impuesto]: if percent_for[cod_impuesto]:
line.set_element( line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent',
percent_for[cod_impuesto]) percent_for[cod_impuesto])
line.set_element( line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID',
cod_impuesto) cod_impuesto)
line.set_element( line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
'/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name',
'ReteRenta') 'ReteRenta')
# abstract method # abstract method
@@ -679,115 +547,82 @@ class DIANInvoiceXML(fe.FeXML):
return 'Invoiced' return 'Invoiced'
def set_invoice_line_tax(fexml, line, invoice_line): def set_invoice_line_tax(fexml, line, invoice_line):
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'./cac:TaxTotal/cbc:TaxAmount', './cac:TaxTotal/cbc:TaxAmount',
invoice_line.tax_amount) invoice_line.tax_amount)
#DIAN 1.7.-2020: FAX05 #DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', './cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.taxable_amount) invoice_line.taxable_amount)
for subtotal in invoice_line.tax.subtotals: for subtotal in invoice_line.tax.subtotals:
line.set_element( line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP')
'./cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
subtotal.tax_amount,
currencyID='COP')
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element( line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2))
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2))
if subtotal.scheme is not None: if subtotal.scheme is not None:
#DIAN 1.7.-2020: FAX15 #DIAN 1.7.-2020: FAX15
line.set_element( line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code)
'./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code) line.set_element('./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:Name', subtotal.scheme.name)
def set_invoice_line_withholding(fexml, line, invoice_line): def set_invoice_line_withholding(fexml, line, invoice_line):
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'./cac:WithholdingTaxTotal/cbc:TaxAmount', './cac:WithholdingTaxTotal/cbc:TaxAmount',
invoice_line.withholding_amount) invoice_line.withholding_amount)
#DIAN 1.7.-2020: FAX05 #DIAN 1.7.-2020: FAX05
fexml.set_element_amount_for( fexml.set_element_amount_for(line,
line,
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', './cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount',
invoice_line.withholding_taxable_amount) invoice_line.withholding_taxable_amount)
for subtotal in invoice_line.withholding.subtotals: for subtotal in invoice_line.withholding.subtotals:
line.set_element( line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP')
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount',
subtotal.tax_amount,
currencyID='COP')
if subtotal.percent is not None: if subtotal.percent is not None:
line.set_element( line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2))
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2))
if subtotal.scheme is not None: if subtotal.scheme is not None:
#DIAN 1.7.-2020: FAX15 #DIAN 1.7.-2020: FAX15
line.set_element( line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code)
'./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code) line.set_element('./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:Name', subtotal.scheme.name)
def set_invoice_lines(fexml, invoice): def set_invoice_lines(fexml, invoice):
next_append = False next_append = False
for index, invoice_line in enumerate(invoice.invoice_lines): for index, invoice_line in enumerate(invoice.invoice_lines):
line = fexml.fragment( line = fexml.fragment('./cac:%sLine' % (fexml.tag_document()), append=next_append)
'./cac:%sLine' % (fexml.tag_document()),
append=next_append)
next_append = True next_append = True
line.set_element('./cbc:ID', index + 1) line.set_element('./cbc:ID', index + 1)
line.set_element( line.set_element('./cbc:%sQuantity' % (fexml.tag_document_concilied()), invoice_line.quantity, unitCode = 'NAR')
'./cbc:%sQuantity' % ( fexml.set_element_amount_for(line,
fexml.tag_document_concilied()),
invoice_line.quantity,
unitCode='NAR')
fexml.set_element_amount_for(
line,
'./cbc:LineExtensionAmount', './cbc:LineExtensionAmount',
invoice_line.total_amount) invoice_line.total_amount)
if not isinstance( if not isinstance(invoice_line.tax, TaxTotalOmit):
invoice_line.tax, TaxTotalOmit):
fexml.set_invoice_line_tax(line, invoice_line) fexml.set_invoice_line_tax(line, invoice_line)
if not isinstance( if not isinstance(invoice_line.withholding, WithholdingTaxTotalOmit):
invoice_line.withholding, WithholdingTaxTotalOmit):
fexml.set_invoice_line_withholding(line, invoice_line) fexml.set_invoice_line_withholding(line, invoice_line)
line.set_element( line.set_element('./cac:Item/cbc:Description', invoice_line.item.description)
'./cac:Item/cbc:Description', invoice_line.item.description)
line.set_element( line.set_element('./cac:Item/cac:StandardItemIdentification/cbc:ID',
'./cac:Item/cac:StandardItemIdentification/cbc:ID',
invoice_line.item.id, invoice_line.item.id,
schemeID=invoice_line.item.scheme_id, schemeID=invoice_line.item.scheme_id,
schemeName=invoice_line.item.scheme_name, schemeName=invoice_line.item.scheme_name,
schemeAgencyID=invoice_line.item.scheme_agency_id) schemeAgencyID=invoice_line.item.scheme_agency_id)
line.set_element( line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code)
'./cac:Price/cbc:PriceAmount',
invoice_line.price.amount,
currencyID=invoice_line.price.amount.currency.code)
#DIAN 1.7.-2020: FBB04 #DIAN 1.7.-2020: FBB04
line.set_element( line.set_element('./cac:Price/cbc:BaseQuantity',
'./cac:Price/cbc:BaseQuantity',
invoice_line.price.quantity, invoice_line.price.quantity,
unitCode=invoice_line.quantity.code) unitCode=invoice_line.quantity.code)
for idx, charge in enumerate(invoice_line.allowance_charge): for idx, charge in enumerate(invoice_line.allowance_charge):
next_append_charge = idx > 0 next_append_charge = idx > 0
fexml.append_allowance_charge( fexml.append_allowance_charge(line, index + 1, charge, append=next_append_charge)
line, index + 1, charge, append=next_append_charge)
def set_allowance_charge(fexml, invoice): def set_allowance_charge(fexml, invoice):
for idx, charge in enumerate(invoice.invoice_allowance_charge): for idx, charge in enumerate(invoice.invoice_allowance_charge):
@@ -815,6 +650,46 @@ class DIANInvoiceXML(fe.FeXML):
fexml.set_element_amount_for( fexml.set_element_amount_for(
line, './cbc:BaseAmount', charge.base_amount) line, './cbc:BaseAmount', charge.base_amount)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None"""
fexml.placeholder_for('./ext:UBLExtensions')
fexml.set_element('./cbc:UBLVersionID', 'UBL 2.1')
fexml.set_element('./cbc:CustomizationID', invoice.invoice_operation_type)
fexml.placeholder_for('./cbc:ProfileID')
fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
#DIAN 1.7.-2020: FAD10
fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00'))
fexml.set_element('./cbc:%sTypeCode' % (fexml.tag_document()),
invoice.invoice_type_code,
listAgencyID='195',
listAgencyName='No matching global declaration available for the validation root',
listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
if fexml.tag_document() == 'Invoice':
fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (
fexml.tag_document()),
invoice.invoice_period_start.strftime('%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (
fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_billing_reference(invoice)
fexml.customize(invoice)
fexml.set_supplier(invoice)
fexml.set_customer(invoice)
fexml.set_payment_mean(invoice)
fexml.set_invoice_totals(invoice)
fexml.set_legal_monetary(invoice)
fexml.set_invoice_lines(invoice)
fexml.set_allowance_charge(invoice)
return fexml
def customize(fexml, invoice): def customize(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML """adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None""" en caso de fallar validacion retorna None"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,15 @@
attrs==22.1.0 attrs==24.2.0
distlib==0.3.6 distlib==0.3.9
filelock==3.8.0 filelock==3.16.1
iniconfig==1.1.1 iniconfig==2.0.0
packaging==21.3 packaging==24.1
platformdirs==2.5.2 platformdirs==4.3.6
pluggy==1.0.0 pluggy==1.5.0
py==1.11.0 pyparsing==3.2.0
pyparsing==3.0.9 pytest==8.3.4
pytest==7.1.3
semantic-version==2.10.0 semantic-version==2.10.0
setuptools-rust==1.5.2 setuptools-rust==1.10.2
six==1.16.0 six==1.16.0
tomli==2.0.1 tomli==2.2.1
tox==3.26.0 tox==4.23.2
typing_extensions==4.4.0 virtualenv==20.28.0
virtualenv==20.16.5

View File

@@ -14,14 +14,14 @@ with open('HISTORY.rst') as history_file:
history = history_file.read() history = history_file.read()
requirements = ['Click>=8.1.7', requirements = ['Click>=8.1.7',
'zeep==4.2.1', 'zeep>=4.2.1',
'lxml==5.2.2', 'lxml>=5.2.2',
'cryptography==3.3.2', 'cryptography>=41.0.0',
'pyOpenSSL==20.0.1', 'pyOpenSSL>=23.2.0',
'xmlsig==0.1.7', 'xmlsig>=0.1.9',
'xades==1.0.0', 'xades>=1.0.0',
'xmlsec==1.3.14', 'xmlsec>=1.3.12',
'python-dateutil==2.9.0.post0', 'python-dateutil>=2.9.0',
# usamos esta dependencia en runtime # usamos esta dependencia en runtime
# para forzar uso de policy_id de archivo local # para forzar uso de policy_id de archivo local
'mock>=5.1.0', 'mock>=5.1.0',
@@ -57,10 +57,10 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Natural Language :: English', 'Natural Language :: English',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
], ],
description="Facturacion Electronica Colombia", description="Facturacion Electronica Colombia",
entry_points={ entry_points={
@@ -84,10 +84,6 @@ setup(
test_suite='tests', test_suite='tests',
tests_require=test_requirements, tests_require=test_requirements,
url='https://github.com/bit4bit/facho', url='https://github.com/bit4bit/facho',
<<<<<<< HEAD
version='0.2.0', version='0.2.0',
=======
version='0.2.1',
>>>>>>> morfo
zip_safe=False, zip_safe=False,
) )

View File

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

View File

@@ -4,122 +4,13 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
# from datetime import datetime # from datetime import datetime
import pytest # import pytest
from facho.fe import form_xml # from facho.fe import form_xml
from datetime import datetime
import helpers
from fixtures import simple_invoice
simple_invoice = simple_invoice # import helpers
def test_xml_with_required_elements(simple_invoice):
xml_header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' # def test_xml_with_required_elements():
DIANInvoiceXML = form_xml.DIANInvoiceXML( # doc = form_xml.AttachedDocument(id='123')
simple_invoice) # xml = doc.toFachoXML()
# assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123'
doc = form_xml.AttachedDocument(
simple_invoice,
DIANInvoiceXML,
id='123')
xml = doc.toFachoXML()
DIANInvoiceXML = form_xml.DIANInvoiceXML(
simple_invoice, 'Invoice').attach_invoice
ApplicationResponse = xml_header + form_xml.ApplicationResponse(simple_invoice).toFachoXML().tostring()
attached_document = xml_header + DIANInvoiceXML.tostring()
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:UBLVersionID') == 'UBL 2.1'
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:CustomizationID') == 'Documentos adjuntos'
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:ProfileID') == 'Factura Electrónica de Venta'
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:ProfileExecutionID') == '1'
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:ID') == '123'
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:IssueDate') == str(datetime.today().date())
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:IssueTime') == datetime.today(
).time().strftime(
'%H:%M:%S-05:00')
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:DocumentType'
) == 'Contenedor de Factura Electrónica'
assert xml.get_element_text(
'/atd:AttachedDocument/cbc:ParentDocumentID'
) == 'ABC123'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cbc:RegistrationName'
) == 'facho-supplier'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cbc:CompanyID'
) == '123'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cbc:TaxLevelCode'
) == 'ZZ'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID'
) == '01'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:SenderParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name'
) == 'IVA'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cbc:RegistrationName'
) == 'facho-customer'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cbc:CompanyID'
) == '321'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cbc:TaxLevelCode'
) == 'ZZ'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID'
) == '01'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ReceiverParty/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name'
) == 'IVA'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:Attachment/cac:ExternalReference/cbc:MimeCode'
) == "text/xml"
assert xml.get_element_text(
'/atd:AttachedDocument/cac:Attachment/cac:ExternalReference/cbc:EncodingCode'
) == "UTF-8"
assert xml.get_element_text(
'/atd:AttachedDocument/cac:Attachment/cac:ExternalReference/cbc:Description'
) == "<![CDATA[{}]]>".format(attached_document)
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cbc:LineID'
) == '1'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:ID'
) == '1234'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:UUID'
) == '1234'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:IssueDate'
) == '2024-11-28'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cbc:DocumentType'
) == 'ApplicationResponse'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:MimeCode'
) == 'text/xml'
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:EncodingCode'
) == "UTF-8"
assert xml.get_element_text(
'/atd:AttachedDocument/cac:ParentDocumentLineReference/cac:DocumentReference/cac:Attachment/cac:ExternalReference/cbc:Description'
) == "<![CDATA[{}]]>".format(ApplicationResponse)

View File

@@ -130,3 +130,6 @@ def test_xml_signature_timestamp(monkeypatch):
xmlstring = xml.tostring() xmlstring = xml.tostring()
signer = fe.DianXMLExtensionSigner('./tests/example.p12') signer = fe.DianXMLExtensionSigner('./tests/example.p12')
xmlsigned = signer.sign_xml_string(xmlstring) xmlsigned = signer.sign_xml_string(xmlstring)
with open('invoice.xml', 'w') as file_:
file_.write(xmlsigned)

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
[tox] [tox]
envlist = py39, py310, py311, py312, flake8 envlist = py39, py310, py311, py312, py313, flake8
[travis] [travis]
python = python =
@@ -7,6 +7,7 @@ python =
3.10: py310 3.10: py310
3.11: py311 3.11: py311
3.12: py312 3.12: py312
3.13: py313
[testenv:flake8] [testenv:flake8]
basepython = python basepython = python