1 Commits

Author SHA1 Message Date
bit4bit
1f75ac46f4 Create new branch named "quantity-float"
FossilOrigin-Name: 20c749c4c007bedda6d9fe1218eec2bdfb1c2930de90ad7e1cf685936e4fcde8
2020-11-10 23:15:50 +00:00
31 changed files with 170 additions and 2389 deletions

View File

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

View File

@@ -2,6 +2,8 @@
facho
=====
!!INESTABLE NO RECOMENDAMOS USO PARA PRODUCION!!
Libreria para facturacion electronica colombia.
- facho/facho.py: abstracion para manipulacion del XML
@@ -22,7 +24,7 @@ usando pip::
CLI
===
tambien se provee linea de comandos **facho** para generacion, firmado y envio de documentos::
tambien se provee linea de comandos **facho** para firmado y envio de documentos::
facho --help
CONTRIBUIR
@@ -30,13 +32,17 @@ CONTRIBUIR
ver **CONTRIBUTING.rst**
USO
===
ver **USAGE.rst**
DIAN HABILITACION
=================
guia oficial actualizada al 2020-04-20: https://www.dian.gov.co/fizcalizacioncontrol/herramienconsulta/FacturaElectronica/Facturaci%C3%B3n_Gratuita_DIAN/Documents/Guia_usuario_08052019.pdf#search=numeracion
ERROR X509SerialNumber
======================
lxml.etree.DocumentInvalid: Element '{http://www.w3.org/2000/09/xmldsig#}X509SerialNumber': '632837201711293159666920255411738137494572618415' is not a valid value of the atomic type 'xs:integer'
Actualmente el xmlschema usado por xmlsig para el campo X509SerialNumber es tipo
integer ahi que parchar manualmente a tipo string, en el archivo site-packages/xmlsig/data/xmldsig-core-schema.xsd.

View File

@@ -1,24 +0,0 @@
uso de la libreria
==================
**facho** es tanto una libreria para modelar y generar los documentos xml requeridos para la facturacion,
asi como una herramienta de **consola** para facilitar algunas actividades como: generaciones de xml
apartir de una especificacion en python, comprimir y enviar archivos según el SOAP vigente.
**facho** es diseñado para ser usado en conjunto con el documento **docs/DIAN/Anexo_Tecnico_Factura_Electronica_Vr1_7_2020.pdf**, ya que en gran medida sigue la terminologia presente en este.
Para ejemplos ver **examples/** .
En terminos generales seria modelar la factura usando **facho/fe/form.py**, instanciar las extensiones requeridas ver **facho/fe/fe.py** y
una vez generado el objeto invoice y las extensiones requeridas se procede a crear el XML, ejemplo:
~~~python
....
xml = form_xml.DIANInvoiceXML(invoice)
extensions = module.extensions(invoice)
for extension in extensions:
xml.add_extension(extension)
form_xml.DIANWriteSigned(xml, "factura.xml", "llave privada", "frase")
~~~

View File

@@ -1,117 +0,0 @@
# este archivo es un ejemplo para le generacion
# una factura de venta nacional usando el comando **facho**.
#
# ejemplo: facho generate-invoice generate-invoice-from-cli.py
#
# importar libreria de modelos
from facho.fe import form
from facho.fe import form_xml
# importar libreria extensiones xml para cumplir decreto
from facho.fe import fe
# importar otras necesarias
from datetime import datetime, date
# Datos del fomulario del SET de pruebas
INVOICE_AUTHORIZATION = '181360000001' #Número suministrado por la Dian en el momento de la creación del SET de Pruebas
ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas
PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas
CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por la Dian en el momento de la creación del SET de Pruebas
# callback que retonar las extensiones XML necesarias
# para que el documento final XML cumpla el decreto.
#
# muchos de los valores usados son obtenidos
# del servicio web de la DIAN.
def extensions(inv):
security_code = fe.DianXMLExtensionSoftwareSecurityCode(ID_SOFTWARE, PIN, inv.invoice_ident)
authorization_provider = fe.DianXMLExtensionAuthorizationProvider()
cufe = fe.DianXMLExtensionCUFE(inv, CLAVE_TECNICA, fe.AMBIENTE_PRUEBAS)
software_provider = fe.DianXMLExtensionSoftwareProvider('nit_empresa', 'dígito_verificación', ID_SOFTWARE)
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(INVOICE_AUTHORIZATION,
datetime(2019, 1, 19),#Datos toamdos de
datetime(2030, 1, 19),#la configuración
'SETP', 990000000, 995000000)#del SET de pruebas
return [security_code, authorization_provider, cufe, software_provider, inv_authorization]
def invoice():
# factura de venta nacional
inv = form.Invoice('01')
# asignar periodo de facturacion
inv.set_period(datetime.now(), datetime.now())
# asignar fecha de emision de la factura
inv.set_issue(datetime.now())
# asignar prefijo y numero del documento
inv.set_ident('SETP990000008')
# asignar tipo de operacion ver DIAN:6.1.5
inv.set_operation_type('10')
inv.set_supplier(form.Party(
legal_name = 'Nombre registrado de la empresa',
name = 'Nombre comercial o él mismo nombre registrado',
ident = form.PartyIdentification('nit_empresa', 'digito_verificación', '31'),
# obligaciones del contribuyente ver DIAN:FAK26
responsability_code = form.Responsability(['O-07', 'O-14', 'O-48']),
# ver DIAN:FAJ28
responsability_regime_code = '48',
# tipo de organizacion juridica ver DIAN:6.2.3
organization_code = '1',
email = "correoempresa@correoempresa.correo",
address = form.Address(
'', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')),
))
#Tercero a quien se le factura
inv.set_customer(form.Party(
legal_name = 'consumidor final',
name = 'consumidor final',
ident = form.PartyIdentification('222222222222', '', '13'),
responsability_code = form.Responsability(['R-99-PN']),
responsability_regime_code = '49',
organization_code = '2',
email = "consumidor_final0final.final",
address = form.Address(
'', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia')),
#tax_scheme = form.TaxScheme('01', 'IVA')
))
# asignar metodo de pago
inv.set_payment_mean(form.PaymentMean(
# metodo de pago ver DIAN:3.4.1
id = '1',
# codigo correspondiente al medio de pago ver DIAN:3.4.2
code = '20',
# fecha de vencimiento de la factura
due_at = datetime.now(),
# identificador numerico
payment_id = '2'
))
# adicionar una linea al documento
inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(int(20.5), '94'),
# item general de codigo 999
description = 'productO3',
item = form.StandardItem('test', 9999),
price = form.Price(
# precio base del item (sin iva)
amount = form.Amount(200.00),
# ver DIAN:6.3.5.1
type_code = '01',
type = 'x'
),
tax = form.TaxTotal(
subtotals = [
form.TaxSubTotal(
percent = 19.00,
scheme=form.TaxScheme('01')
)
]
)
))
return inv
def document_xml():
return form_xml.DIANInvoiceXML

View File

@@ -0,0 +1,74 @@
import facho.fe.form as form
from facho.fe import fe
from datetime import datetime
def extensions(inv):
nit = form.PartyIdentification('nit', '5', '31')
security_code = fe.DianXMLExtensionSoftwareSecurityCode('id software', 'pin', inv.invoice_ident)
authorization_provider = fe.DianXMLExtensionAuthorizationProvider()
cufe = fe.DianXMLExtensionCUFE(inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS,
'clave tecnica')
software_provider = fe.DianXMLExtensionSoftwareProvider(nit, nit.dv, 'id software')
inv_authorization = fe.DianXMLExtensionInvoiceAuthorization('invoice autorization',
datetime(2019, 1, 19),
datetime(2030, 1, 19),
'SETP', 990000001, 995000000)
return [security_code, authorization_provider, cufe, software_provider, inv_authorization]
def invoice():
inv = form.Invoice()
inv.set_period(datetime.now(), datetime.now())
inv.set_issue(datetime.now())
inv.set_ident('SETP990003033')
inv.set_operation_type('10')
inv.set_supplier(form.Party(
legal_name = 'FACHO SOS',
name = 'FACHO SOS',
ident = form.PartyIdentification('900579212', '5', '31'),
responsability_code = form.Responsability(['O-07', 'O-09', 'O-14', 'O-48']),
responsability_regime_code = '48',
organization_code = '1',
email = "sdds@sd.com",
address = form.Address(
'', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia'))
))
inv.set_customer(form.Party(
legal_name = 'facho-customer',
name = 'facho-customer',
ident = form.PartyIdentification('999999999', '', '13'),
responsability_code = form.Responsability(['R-99-PN']),
responsability_regime_code = '49',
organization_code = '2',
email = "sdds@sd.com",
address = form.Address(
'', '', form.City('05001', 'Medellín'),
form.Country('CO', 'Colombia'),
form.CountrySubentity('05', 'Antioquia'))
))
inv.set_payment_mean(form.PaymentMean(
id = '1',
code = '10',
due_at = datetime.now(),
payment_id = '1'
))
inv.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1, '94'),
description = 'producto facho',
item = form.StandardItem('test', 9999),
price = form.Price(
amount = form.Amount(100.00),
type_code = '01',
type = 'x'
),
tax = form.TaxTotal(
subtotals = [
form.TaxSubTotal(
percent = 19.00,
)
]
)
))
return inv

View File

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

View File

@@ -129,11 +129,6 @@ class FachoXML:
self.xpath_for = {}
self.extensions = []
@classmethod
def from_string(cls, document: str, namespaces: dict() = []) -> 'FachoXML':
xml = LXMLBuilder.from_string(document)
return FachoXML(xml, nsmap=namespaces)
def append_element(self, elem, new_elem):
#elem = self.find_or_create_element(xpath, append=append)
#self.builder.append(elem, new_elem)
@@ -257,9 +252,6 @@ class FachoXML:
def get_element_text(self, xpath, format_=str):
xpath = self.fragment_prefix + self._path_xpath_for(xpath)
elem = self.builder.xpath(self.root, xpath)
if elem is None:
raise AttributeError('xpath %s invalid' % (xpath))
text = self.builder.get_text(elem)
return format_(text)

View File

@@ -138,16 +138,10 @@ class GetStatusResponse:
@classmethod
def fromdict(cls, data):
if data['ErrorMessage']:
error_message = data['ErrorMessage']['string']
else:
error_message = None
return cls(data['IsValid'],
data['StatusDescription'],
data['StatusCode'],
error_message)
data['ErrorMessage']['string'])
@dataclass
class GetStatus(SOAPService):

View File

@@ -14,7 +14,6 @@ from contextlib import contextmanager
from .data.dian import codelist
from . import form
from collections import defaultdict
from pathlib import Path
AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code']
AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code']
@@ -26,9 +25,8 @@ SCHEME_AGENCY_ATTRS = {
}
pwd = Path(__file__).parent
# RESOLUCION 0001: pagina 516
POLICY_ID = 'file://'+str(pwd)+'/data/dian/politicadefirmav2.pdf'
POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf'
POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.'
@@ -51,7 +49,8 @@ NAMESPACES = {
}
def fe_from_string(document: str) -> FachoXML:
return FeXML.from_string(document)
xml = LXMLBuilder.from_string(document)
return FachoXML(xml, nsmap=NAMESPACES)
from contextlib import contextmanager
@contextmanager
@@ -70,7 +69,6 @@ def mock_xades_policy():
mock.return_value = UrllibPolicyMock()
yield
class FeXML(FachoXML):
def __init__(self, root, namespace):
@@ -81,10 +79,6 @@ class FeXML(FachoXML):
self._cn = root.rstrip('/')
#self.find_or_create_element(self._cn)
@classmethod
def from_string(cls, document: str) -> 'FeXML':
return super().from_string(document, namespaces=NAMESPACES)
def tostring(self, **kw):
return super().tostring(**kw)\
.replace("fe:", "")\
@@ -191,14 +185,14 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE):
'%s' % build_vars['NumFac'],
'%s' % build_vars['FecFac'],
'%s' % build_vars['HoraFac'],
form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
form.Amount(build_vars['ValorBruto']).format('%.02f'),
CodImpuesto1,
build_vars['ValorImpuestoPara'].get(CodImpuesto1, form.Amount(0.0)).truncate_as_string(2),
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).format('%.02f'),
CodImpuesto2,
build_vars['ValorImpuestoPara'].get(CodImpuesto2, form.Amount(0.0)).truncate_as_string(2),
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).format('%.02f'),
CodImpuesto3,
build_vars['ValorImpuestoPara'].get(CodImpuesto3, form.Amount(0.0)).truncate_as_string(2),
build_vars['ValorTotalPagar'].truncate_as_string(2),
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).format('%.02f'),
form.Amount(build_vars['ValorTotalPagar']).format('%.02f'),
'%s' % build_vars['NitOFE'],
'%s' % build_vars['NumAdq'],
'%s' % build_vars['ClTec'],
@@ -228,14 +222,14 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE):
'%s' % build_vars['NumFac'],
'%s' % build_vars['FecFac'],
'%s' % build_vars['HoraFac'],
form.Amount(build_vars['ValorBruto']).truncate_as_string(2),
form.Amount(build_vars['ValorBruto']).format('%.02f'),
CodImpuesto1,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2),
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).format('%.02f'),
CodImpuesto2,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).truncate_as_string(2),
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).format('%.02f'),
CodImpuesto3,
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).truncate_as_string(2),
form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2),
form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).format('%.02f'),
form.Amount(build_vars['ValorTotalPagar']).format('%.02f'),
'%s' % build_vars['NitOFE'],
'%s' % build_vars['NumAdq'],
'%s' % build_vars['Software-PIN'],
@@ -283,24 +277,16 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension):
class DianXMLExtensionSigner:
def __init__(self, pkcs12_path, passphrase=None, mockpolicy=False):
self._pkcs12_data = open(pkcs12_path, 'rb').read()
self._pkcs12_path = pkcs12_path
self._passphrase = None
self._mockpolicy = mockpolicy
if passphrase:
self._passphrase = passphrase.encode('utf-8')
@classmethod
def from_bytes(cls, data, passphrase=None, mockpolicy=False):
self = cls.__new__(cls)
self._pkcs12_data = data
self._passphrase = None
self._mockpolicy = mockpolicy
if passphrase:
self._passphrase = passphrase.encode('utf-8')
return self
def from_pkcs12(self, filepath, password=None):
p12 = OpenSSL.crypto.load_pkcs12(open(filepath, 'rb').read(), password)
def sign_xml_string(self, document):
xml = LXMLBuilder.from_string(document)
signature = self.sign_xml_element(xml)
@@ -355,7 +341,7 @@ class DianXMLExtensionSigner:
POLICY_NAME,
xmlsig.constants.TransformSha256)
ctx = xades.XAdESContext(policy)
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(self._pkcs12_data,
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(open(self._pkcs12_path, 'rb').read(),
self._passphrase))
if self._mockpolicy:
@@ -374,7 +360,6 @@ class DianXMLExtensionSigner:
extcontent = fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent')
fachoxml.append_element(extcontent, signature)
class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):
# RESOLUCION 0004: pagina 176
@@ -444,7 +429,7 @@ class DianZIP:
MAX_FILES = 50
def __init__(self, file_like):
self.zipfile = zipfile.ZipFile(file_like, mode='w', compression=zipfile.ZIP_DEFLATED)
self.zipfile = zipfile.ZipFile(file_like, mode='w')
self.num_files = 0
def add_invoice_xml(self, name, xml_data):
@@ -467,8 +452,8 @@ class DianZIP:
class DianXMLExtensionSignerVerifier:
def __init__(self, pkcs12_path_or_bytes, passphrase=None, mockpolicy=False):
self._pkcs12_path_or_bytes = pkcs12_path_or_bytes
def __init__(self, pkcs12_path, passphrase=None, mockpolicy=False):
self._pkcs12_path = pkcs12_path
self._passphrase = None
self._mockpolicy = mockpolicy
if passphrase:
@@ -485,12 +470,7 @@ class DianXMLExtensionSignerVerifier:
fachoxml.root.append(signature)
ctx = xades.XAdESContext()
pkcs12_data = self._pkcs12_path_or_bytes
if isinstance(self._pkcs12_path_or_bytes, str):
pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read()
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data,
ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(open(self._pkcs12_path, 'rb').read(),
self._passphrase))
try:

View File

@@ -4,7 +4,6 @@
import hashlib
from functools import reduce
import copy
import dataclasses
from dataclasses import dataclass
from datetime import datetime, date
from collections import defaultdict
@@ -54,8 +53,8 @@ class AmountCollection(Collection):
return total
class Amount:
def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP'), precision = DECIMAL_PRECISION):
self.precision = precision
def __init__(self, amount: int or float or Amount, currency: Currency = Currency('COP')):
#DIAN 1.7.-2020: 1.2.3.1
if isinstance(amount, Amount):
if amount < Amount(0.0):
@@ -64,60 +63,42 @@ class Amount:
self.amount = amount.amount
self.currency = amount.currency
else:
if float(amount) < 0:
if amount < 0:
raise ValueError('amount must be positive >= 0')
self.amount = Decimal(amount, decimal.Context(prec=self.precision,
self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION,
#DIAN 1.7.-2020: 1.2.1.1
rounding=decimal.ROUND_HALF_EVEN ))
self.currency = currency
def fromNumber(self, val):
return Amount(val, currency=self.currency)
def round(self, prec):
return Amount(round(self.amount, prec), currency=self.currency)
def __round__(self, prec):
return round(self.amount, prec)
def __str__(self):
return str(self.float())
return '%.06f' % self.amount
def __lt__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return round(self.amount, self.precision) < round(other, 2)
return round(self.amount, DECIMAL_PRECISION) < round(other, 2)
def __eq__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return round(self.amount, self.precision) == round(other.amount, self.precision)
return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION)
def _cast(self, val):
if type(val) in [int, float]:
return self.fromNumber(val)
if isinstance(val, Amount):
return val
if isinstance(val, Decimal):
return self.fromNumber(float(val))
raise TypeError("cant cast %s to amount" % (type(val)))
def __add__(self, rother):
other = self._cast(rother)
def __add__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return Amount(self.amount + other.amount, self.currency)
def __sub__(self, rother):
other = self._cast(rother)
def __sub__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return Amount(self.amount - other.amount, self.currency)
def __mul__(self, rother):
other = self._cast(rother)
def __mul__(self, other):
if not self.is_same_currency(other):
raise AmountCurrencyError()
return Amount(self.amount * other.amount, self.currency)
@@ -125,9 +106,8 @@ class Amount:
def is_same_currency(self, other):
return self.currency == other.currency
def truncate_as_string(self, prec):
parts = str(self.float()).split('.', 1)
return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0'))
def format(self, formatter):
return formatter % self.float()
def float(self):
return float(round(self.amount, DECIMAL_PRECISION))
@@ -136,25 +116,27 @@ class Amount:
class Quantity:
def __init__(self, val, code):
if type(val) not in [float, int]:
raise ValueError('val expected int or float')
if not isinstance(val, int):
raise ValueError('val expected int')
if code not in codelist.UnidadesMedida:
raise ValueError("code [%s] not found" % (code))
self.value = Amount(val)
self.value = val
self.code = code
def __mul__(self, other):
if isinstance(other, Amount):
return Amount(self.value) * other
return self.value * other
def __lt__(self, other):
if isinstance(other, Amount):
return Amount(self.value) < other
return self.value < other
def __str__(self):
return str(self.value)
def __repr__(self):
return str(self)
@dataclass
class Item:
@@ -341,15 +323,11 @@ class Price:
amount: Amount
type_code: str
type: str
quantity: int = 1
def __post_init__(self):
if self.type_code not in codelist.CodigoPrecioReferencia:
raise ValueError("type_code [%s] not found" % (self.type_code))
if not isinstance(self.quantity, int):
raise ValueError("quantity must be int")
self.amount *= self.quantity
@dataclass
class PaymentMean:
@@ -400,52 +378,6 @@ class InvoiceDocumentReference(BillingReference):
date: fecha de emision de la nota credito relacionada
"""
@dataclass
class AllowanceChargeReason:
code: str
reason: str
def __post_init__(self):
if self.code not in codelist.CodigoDescuento:
raise ValueError("code [%s] not found" % (self.code))
@dataclass
class AllowanceCharge:
#DIAN 1.7.-2020: FAQ03
charge_indicator: bool = True
amount: Amount = Amount(0.0)
reason: AllowanceChargeReason = None
#Valor Base para calcular el descuento o el cargo
base_amount: typing.Optional[Amount] = Amount(0.0)
# Porcentaje: Porcentaje que aplicar.
multiplier_factor_numeric: Amount = Amount(1.0)
def isCharge(self):
return self.charge_indicator == True
def isDiscount(self):
return self.charge_indicator == False
def asCharge(self):
self.charge_indicator = True
def asDiscount(self):
self.charge_indicator = False
def hasReason(self):
return self.reason is not None
def set_base_amount(self, amount):
self.base_amount = amount
class AllowanceChargeAsDiscount(AllowanceCharge):
def __init__(self, amount: Amount = Amount(0.0)):
self.charge_indicator = False
self.amount = amount
@dataclass
class InvoiceLine:
# RESOLUCION 0004: pagina 155
@@ -459,31 +391,9 @@ class InvoiceLine:
# de subtotal
tax: typing.Optional[TaxTotal]
allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list)
def add_allowance_charge(self, charge):
if not isinstance(charge, AllowanceCharge):
raise TypeError('charge invalid type expected AllowanceCharge')
charge.set_base_amount(self.total_amount_without_charge)
self.allowance_charge.add(charge)
@property
def total_amount_without_charge(self):
return (self.quantity * self.price.amount)
@property
def total_amount(self):
charge = AmountCollection(self.allowance_charge)\
.filter(lambda charge: charge.isCharge())\
.map(lambda charge: charge.amount)\
.sum()
discount = AmountCollection(self.allowance_charge)\
.filter(lambda charge: charge.isDiscount())\
.map(lambda charge: charge.amount)\
.sum()
return self.total_amount_without_charge + charge - discount
return self.quantity * self.price.amount
@property
def total_tax_inclusive_amount(self):
@@ -531,6 +441,37 @@ class LegalMonetaryTotal:
- self.prepaid_amount
@dataclass
class AllowanceChargeReason:
code: str
reason: str
def __post_init__(self):
if self.code not in codelist.CodigoDescuento:
raise ValueError("code [%s] not found" % (self.code))
@dataclass
class AllowanceCharge:
#DIAN 1.7.-2020: FAQ03
charge_indicator: bool = True
amount: Amount = Amount(0.0)
reason: AllowanceChargeReason = None
def isCharge(self):
return self.charge_indicator == True
def isDiscount(self):
return self.charge_indicator == False
def asCharge(self):
self.charge_indicator = True
def asDiscount(self):
self.charge_indicator = False
def hasReason(self):
return self.reason is not None
class NationalSalesInvoiceDocumentType(str):
def __str__(self):
@@ -629,7 +570,7 @@ class Invoice:
self.invoice_operation_type = operation
def add_allowance_charge(self, charge: AllowanceCharge):
def add_allownace_charge(self, charge: AllowanceCharge):
self.invoice_allowance_charge.append(charge)
def add_invoice_line(self, line: InvoiceLine):
@@ -677,22 +618,11 @@ class Invoice:
#DIAN 1.7.-2020: FAU14
self.invoice_legal_monetary_total.calculate()
def _refresh_charges_base_amount(self):
if self.invoice_allowance_charge:
for invline in self.invoice_lines:
if invline.allowance_charge:
# TODO actualmente solo uno de los cargos es permitido
raise ValueError('allowance charge in invoice exclude invoice line')
# cargos a nivel de factura
for charge in self.invoice_allowance_charge:
charge.set_base_amount(self.invoice_legal_monetary_total.line_extension_amount)
def calculate(self):
for invline in self.invoice_lines:
invline.calculate()
self._calculate_legal_monetary_total()
self._refresh_charges_base_amount()
class NationalSalesInvoice(Invoice):
def __init__(self):

View File

@@ -540,31 +540,10 @@ class DIANInvoiceXML(fe.FeXML):
line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code)
#DIAN 1.7.-2020: FBB04
line.set_element('./cac:Price/cbc:BaseQuantity',
invoice_line.price.quantity,
invoice_line.quantity,
unitCode=invoice_line.quantity.code)
for idx, charge in enumerate(invoice_line.allowance_charge):
next_append_charge = idx > 0
fexml.append_allowance_charge(line, index + 1, charge, append=next_append_charge)
def set_allowance_charge(fexml, invoice):
for idx, charge in enumerate(invoice.invoice_allowance_charge):
next_append = idx > 0
fexml.append_allowance_charge(fexml, idx + 1, charge, append=next_append)
def append_allowance_charge(fexml, parent, idx, charge, append=False):
line = parent.fragment('./cac:AllowanceCharge', append=append)
#DIAN 1.7.-2020: FAQ02
line.set_element('./cbc:ID', idx)
#DIAN 1.7.-2020: FAQ03
line.set_element('./cbc:ChargeIndicator', str(charge.charge_indicator).lower())
if charge.reason:
line.set_element('./cbc:AllowanceChargeReasonCode', charge.reason.code)
line.set_element('./cbc:allowanceChargeReason', charge.reason.reason)
line.set_element('./cbc:MultiplierFactorNumeric', str(round(charge.multiplier_factor_numeric, 2)))
fexml.set_element_amount_for(line, './cbc:Amount', charge.amount)
fexml.set_element_amount_for(line, './cbc:BaseAmount', charge.base_amount)
def attach_invoice(fexml, invoice):
"""adiciona etiquetas a FEXML y retorna FEXML
en caso de fallar validacion retorna None"""
@@ -600,7 +579,6 @@ class DIANInvoiceXML(fe.FeXML):
fexml.set_invoice_totals(invoice)
fexml.set_invoice_lines(invoice)
fexml.set_payment_mean(invoice)
fexml.set_allowance_charge(invoice)
fexml.set_billing_reference(invoice)
return fexml

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

@@ -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

@@ -61,6 +61,6 @@ setup(
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/bit4bit/facho',
version='0.2.1',
version='0.1.2',
zip_safe=False,
)

View File

@@ -20,34 +20,3 @@ def test_amount_equals():
assert price1 == price2
assert price1 == form.Amount(100) + form.Amount(10)
assert price1 == form.Amount(10) * form.Amount(10) + form.Amount(10)
assert form.Amount(110) == (form.Amount(1.10) * form.Amount(100))
def test_round_half_even():
# https://www.w3.org/TR/xpath-functions-31/#func-round-half-to-even
assert form.Amount(0.5).round(0).float() == 0.0
assert form.Amount(1.5).round(0).float() == 2.0
assert form.Amount(2.5).round(0).float() == 2.0
assert form.Amount(3.567812e+3).round(2).float() == 3567.81e0
assert form.Amount(4.7564e-3).round(2).float() == 0.0e0
def test_round():
# Entre 0 y 5 Mantener el dígito menos significativo
assert form.Amount(1.133).round(2) == form.Amount(1.13)
# Entre 6 y 9 Incrementar el dígito menos significativo
assert form.Amount(1.166).round(2) == form.Amount(1.17)
# 5, y el segundo dígito siguiente al dígito menos significativo es cero o par Mantener el dígito menos significativo
assert str(form.Amount(1.1542).round(2)) == str(form.Amount(1.15))
# 5, y el segundo dígito siguiente al dígito menos significativo es impar Incrementar el dígito menos significativo
assert str(form.Amount(1.1563).round(2)) == str(form.Amount(1.16))
def test_amount_truncate():
assert form.Amount(1.1569).truncate_as_string(2) == '1.15'
assert form.Amount(587.0700).truncate_as_string(2) == '587.07'
assert form.Amount(14705.8800).truncate_as_string(2) == '14705.88'
assert form.Amount(9423.7000).truncate_as_string(2) == '9423.70'
assert form.Amount(10084.03).truncate_as_string(2) == '10084.03'
assert form.Amount(10000.02245).truncate_as_string(2) == '10000.02'
assert form.Amount(10000.02357).truncate_as_string(2) == '10000.02'
def test_amount_format():
assert str(round(form.Amount(1.1569),2)) == '1.16'

View File

@@ -110,19 +110,3 @@ def test_xml_sign_dian(monkeypatch):
helpers.mock_urlopen(m)
xmlsigned = signer.sign_xml_string(xmlstring)
assert "Signature" in xmlsigned
def test_xml_sign_dian_using_bytes(monkeypatch):
xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent')
ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True)
extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent')
xmlstring = xml.tostring()
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

View File

@@ -55,13 +55,6 @@ def test_invoicesimple_zip(simple_invoice):
with fe.DianZIP(zipdata) as dianzip:
name_invoice = dianzip.add_invoice_xml(simple_invoice.invoice_ident, str(xml_invoice))
# el zip ademas de archivar debe comprimir los archivos
# de lo contrario la DIAN lo rechaza
with zipfile.ZipFile(zipdata) as dianzip:
dianzip.testzip()
for zipinfo in dianzip.infolist():
assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, "se espera el zip comprimido"
with zipfile.ZipFile(zipdata) as dianzip:
xml_data = dianzip.open(name_invoice).read().decode('utf-8')
assert xml_data == str(xml_invoice)
@@ -119,7 +112,7 @@ def test_invoice_cufe(simple_invoice_without_lines):
simple_invoice.invoice_supplier.ident = form.PartyIdentification('700085371', '5', '31')
simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31')
simple_invoice.add_invoice_line(form.InvoiceLine(
quantity = form.Quantity(1.00, '94'),
quantity = form.Quantity(1, '94'),
description = 'producto',
item = form.StandardItem(111),
price = form.Price(form.Amount(1_500_000), '01', ''),
@@ -177,7 +170,6 @@ def test_invoice_cufe(simple_invoice_without_lines):
assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4'
def test_credit_note_cude(simple_credit_note_without_lines):
simple_invoice = simple_credit_note_without_lines
simple_invoice.invoice_ident = '8110007871'

View File

@@ -38,10 +38,7 @@ def test_invoice_legalmonetary():
assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0)
assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(0.0)
def test_allowancecharge_as_discount():
discount = form.AllowanceChargeAsDiscount(amount=form.Amount(1000.0))
assert discount.isDiscount() == True
def test_FAU10():
inv = form.NationalSalesInvoice()
inv.add_invoice_line(form.InvoiceLine(
@@ -61,7 +58,7 @@ def test_FAU10():
]
)
))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.add_allownace_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.calculate()
assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0)
@@ -89,7 +86,7 @@ def test_FAU14():
]
)
))
inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.add_allownace_charge(form.AllowanceCharge(amount=form.Amount(19.0)))
inv.add_prepaid_payment(form.PrePaidPayment(paid_amount = form.Amount(50.0)))
inv.calculate()

View File

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

View File

@@ -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'