diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a38caf8..4200623 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -15,7 +15,7 @@ Types of Contributions Report Bugs ~~~~~~~~~~~ -Report bugs at https://lib.facho.cyou +Report bugs at https://git.disroot.org/Etrivial/facho/issues. If you are reporting a bug, please include: @@ -45,7 +45,7 @@ articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ -The best way to send feedback is to file an issue at https://lib.facho.cyou/ticket. +The best way to send feedback is to file an issue at https://git.disroot.org/Etrivial/facho/issues. If you are proposing a feature: @@ -57,28 +57,18 @@ If you are proposing a feature: Get Started! ------------ -Using docker ------------- - -1. make -f Makefile.dev dev-setup - 1. make -f Makefile.dev dev-shell -2. make -f Makefile.dev test -3. make -f Makefile.dev tox - -From Source Code ------------ - Ready to contribute? Here's how to set up `facho` for local development. 1. Fork the `facho` repo . 2. Clone your fork locally:: - $ fossil clone https://lib.facho.cyou + $ git clone https://git.disroot.org/Etrivial/facho.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ python3 -mvenv facho-venv && source facho-venv/bin/activate $ cd facho/ + $ pre-commit install $ python setup.py develop 4. Create a branch for local development:: @@ -104,6 +94,13 @@ Ready to contribute? Here's how to set up `facho` for local development. 7. Submit a pull request through the GitHub website. +Using docker +------------ + +1. make -f Makefile.dev build +2. make -f Makefile.dev dev-shell +3. make -f Makefile.dev python3.8 setup.py develop +4. make -f Makefile.dev python3.8 setup.py test Pull Request Guidelines ----------------------- diff --git a/Dockerfile b/Dockerfile index e98622f..7894872 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ # DERIVADO DE https://alextereshenkov.github.io/run-python-tests-with-tox-in-docker.html -FROM ubuntu:20.04 - -LABEL org.opencontainers.image.authors="bit4bit@riseup.net" +FROM ubuntu:24.04 RUN apt-get -qq update @@ -9,35 +7,34 @@ RUN apt install software-properties-common -y \ && add-apt-repository ppa:deadsnakes/ppa RUN apt-get install -y --no-install-recommends \ - python3.7 python3.7-distutils python3.7-dev \ - python3.8 python3.8-distutils python3.8-dev \ python3.9 python3.9-distutils python3.9-dev \ python3.10 python3.10-distutils python3.10-dev \ + python3.11 python3.11-distutils python3.11-dev \ + python3.12 python3-setuptools python3.12-dev \ wget \ ca-certificates RUN wget https://bootstrap.pypa.io/get-pip.py \ - && python3.7 get-pip.py pip==22.2.2 \ - && python3.8 get-pip.py pip==22.2.2 \ - && python3.9 get-pip.py pip==22.2.2 \ - && python3.10 get-pip.py pip==22.2.2 \ + && python3.9 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.12 get-pip.py pip==23.2.1 --break-system-packages \ && rm get-pip.py RUN apt-get install -y --no-install-recommends \ - libxml2-dev \ - libxmlsec1-dev \ - build-essential \ - zip \ - pkg-config - -RUN python3.7 --version -RUN python3.8 --version + libxml2-dev \ + libxmlsec1-dev \ + build-essential \ + zip + RUN python3.9 --version RUN python3.10 --version +RUN python3.11 --version +RUN python3.12 --version -RUN pip3.7 install setuptools setuptools-rust -RUN pip3.8 install setuptools setuptools-rust RUN pip3.9 install setuptools setuptools-rust RUN pip3.10 install setuptools setuptools-rust +RUN pip3.11 install setuptools setuptools-rust --break-system-packages +RUN pip3.12 install setuptools setuptools-rust --break-system-packages -RUN pip3 install tox pytest +RUN pip3 install tox pytest --break-system-packages diff --git a/Makefile.dev b/Makefile.dev index 9aa37b2..ae90ab9 100644 --- a/Makefile.dev +++ b/Makefile.dev @@ -15,7 +15,7 @@ dev-shell: docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash test: - docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.7 setup.py test' + docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.12 setup.py test' tox: docker run -it -v $(PWD)/:/app -w /app facho tox diff --git a/README.rst b/README.rst index 3598e5e..93b4b93 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ INSTALACION usando pip:: - pip install git+https://github.com/bit4bit/facho + pip install git+https://git.disroot.org/Etrivial/facho CLI === diff --git a/docs/DIAN/Anexo_tecnico_vr18_09022021.pdf b/docs/DIAN/Anexo_tecnico_vr18_09022021.pdf deleted file mode 100644 index 244dc2d..0000000 Binary files a/docs/DIAN/Anexo_tecnico_vr18_09022021.pdf and /dev/null differ diff --git a/docs/DIAN/Caja_de_herramientas_Factura_Electronica_Validacion_Previa-09-02-2021.zip b/docs/DIAN/Caja_de_herramientas_Factura_Electronica_Validacion_Previa-09-02-2021.zip deleted file mode 100644 index 961dc9d..0000000 Binary files a/docs/DIAN/Caja_de_herramientas_Factura_Electronica_Validacion_Previa-09-02-2021.zip and /dev/null differ diff --git a/examples/generate-invoice-from-cli.py b/examples/generate-invoice-from-cli.py index edb6cdd..a2d6351 100644 --- a/examples/generate-invoice-from-cli.py +++ b/examples/generate-invoice-from-cli.py @@ -14,7 +14,7 @@ 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 +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 @@ -36,6 +36,7 @@ def extensions(inv): 'SETP', 990000000, 995000000)#del SET de pruebas return [security_code, authorization_provider, cufe, software_provider, inv_authorization] + def invoice(): # factura de venta nacional inv = form.Invoice('01') @@ -49,16 +50,17 @@ def invoice(): 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'), + 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']), + responsability_code=form.Responsability(['ZZ', 'O-14', 'O-48']), # ver DIAN:FAJ28 - responsability_regime_code = '48', + responsability_regime_code='48', # tipo de organizacion juridica ver DIAN:6.2.3 - organization_code = '1', - email = "correoempresa@correoempresa.correo", - address = form.Address( + organization_code='1', + email="correoempresa@correoempresa.correo", + address=form.Address( '', '', form.City('05001', 'Medellín'), form.Country('CO', 'Colombia'), form.CountrySubentity('05', 'Antioquia')), @@ -76,42 +78,43 @@ def invoice(): '', '', form.City('05001', 'Medellín'), form.Country('CO', 'Colombia'), form.CountrySubentity('05', 'Antioquia')), - #tax_scheme = form.TaxScheme('01', 'IVA') + # 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' + id='1', + # codigocorrespondientealmediodepagoverDIAN:3.4.2 + code='20', + # fechadevencimientodelafactura + due_at=datetime.now(), + # identificadornumerico + 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') - ) - ] - ) - )) + inv.add_invoice_line( + form.InvoiceLine( + quantity=form.Quantity(int(20.5), '94'), + # item general de codigo 999 + description='productO3', + sitem=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 diff --git a/examples/use-as-lib.py b/examples/use-as-lib.py index 96dbbaa..0215f62 100644 --- a/examples/use-as-lib.py +++ b/examples/use-as-lib.py @@ -1,109 +1,127 @@ # importar libreria de modelos +from facho import fe, form_xml import facho.fe.form as form -import facho.fe.form_xml +import datetime + +PRIVATE_KEY_PATH = 'ruta a mi llave privada' +PRIVATE_PASSPHRASE = 'clave de la llave privada' -PRIVATE_KEY_PATH='ruta a mi llave privada' -PRIVATE_PASSPHRASE='clave de la llave privada' # consultar las extensiones necesarias def extensions(inv): - security_code = fe.DianXMLExtensionSoftwareSecurityCode('id software', 'pin', inv.invoice_ident) + security_code = fe.DianXMLExtensionSoftwareSecurityCode( + 'id software', 'pin', inv.invoice_ident) authorization_provider = fe.DianXMLExtensionAuthorizationProvider() - cufe = fe.DianXMLExtensionCUFE(inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS, - 'clave tecnica') + 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] + 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) + 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'), + legal_name='FACHOSOS', + name='FACHOSOS', + ident=form.PartyIdentification('900579212', '5', '31'), # obligaciones del contribuyente ver DIAN:FAK26 - responsability_code = form.Responsability(['O-07', 'O-09', 'O-14', 'O-48']), + responsability_code=form.Responsability(['ZZ', 'O-09', 'O-14', 'O-48']), # ver DIAN:FAJ28 - responsability_regime_code = '48', + 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')) + 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')) + 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', + id='1', # codigo correspondiente al medio de pago ver DIAN:3.4.2 - code = '10', + code='10', # fecha de vencimiento de la factura - due_at = datetime.now(), - + due_at=datetime.now(), # identificador numerico - payment_id = '1' + payment_id='1' )) # adicionar una linea al documento inv.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1, '94'), - description = 'producto facho', + quantity=form.Quantity(1, '94'), + description='producto facho', # item general de codigo 999 - item = form.StandardItem('test', 9999), - price = form.Price( + item=form.StandardItem('test', 9999), + price=form.Price( # precio base del tiem - amount = form.Amount(100.00), + amount=form.Amount(100.00), # ver DIAN:6.3.5.1 - type_code = '01', - type = 'x' + type_code='01', + type='x' ), - tax = form.TaxTotal( - subtotals = [ + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - percent = 19.00, - ) - ] + percent=19.00, + )] ) )) diff --git a/experimental/facho-signer/AUTHORS b/experimental/facho-signer/AUTHORS index cc5e0ee..e69de29 100644 --- a/experimental/facho-signer/AUTHORS +++ b/experimental/facho-signer/AUTHORS @@ -1 +0,0 @@ -Jovany Leandro G.C \ No newline at end of file diff --git a/experimental/facho-signer/README b/experimental/facho-signer/README index 851b2f1..e69de29 100644 --- a/experimental/facho-signer/README +++ b/experimental/facho-signer/README @@ -1 +0,0 @@ -Librería en C para firmar electrónica de documentos contable Colombia. \ No newline at end of file diff --git a/experimental/facho-signer/configure.ac b/experimental/facho-signer/configure.ac index 037ce25..da9e2b6 100644 --- a/experimental/facho-signer/configure.ac +++ b/experimental/facho-signer/configure.ac @@ -1,7 +1,7 @@ # -*- Autoconf -*- # Process this file with autoconf to produce a configure script. -AC_PREREQ([2.69]) +AC_PREREQ([2.71]) AC_INIT([facho-signer], [0.0.1], [bit4bit@riseup.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([src/facho_signer.c]) diff --git a/experimental/facho-signer/src/xmlusigned.xml b/experimental/facho-signer/src/xmlusigned.xml index 23ead39..35e11a5 100644 --- a/experimental/facho-signer/src/xmlusigned.xml +++ b/experimental/facho-signer/src/xmlusigned.xml @@ -76,7 +76,7 @@ NEUROTEC TECNOLOGIA S.A.S 900579212 - O-07;O-09;O-14;O-48 + ZZ;O-09;O-14;O-48 diff --git a/facho/cli.py b/facho/cli.py index 8db2710..9ef1fec 100644 --- a/facho/cli.py +++ b/facho/cli.py @@ -259,14 +259,14 @@ def extensions(form.Invoice): -> List[facho.FachoXMLExtension] spec.loader.exec_module(module) import facho.fe.form as form - from facho.fe.form_xml import DIANInvoiceXML, DIANWriteSigned,DIANWrite + from facho.fe.form_xml import DIANInvoiceXML, DIANWriteSigned, DIANWrite, DIANSupportDocumentXML from facho import fe try: invoice_xml = module.document_xml() except AttributeError: - invoice_xml = DIANInvoiceXML - + #invoice_xml = DIANInvoiceXML + invoice_xml = DIANSupportDocumentXML print("Using document xml:", invoice_xml) invoice = module.invoice() invoice.calculate() diff --git a/facho/facho.py b/facho/facho.py index 27d31e3..0970c2c 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -1,12 +1,11 @@ # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. - from lxml import etree -from lxml.etree import Element, SubElement, tostring +from lxml.etree import Element, tostring import re from collections import defaultdict from copy import deepcopy -from pprint import pprint + class FachoValueInvalid(Exception): def __init__(self, xpath): @@ -32,7 +31,10 @@ class LXMLBuilder: def __init__(self, nsmap): self.nsmap = nsmap - self._re_node_expr = re.compile(r'^(?P((?P\w+):)?(?P[a-zA-Z0-9_-]+))(?P\[.+\])?') + self._re_node_expr = \ + re.compile( + r'^(?P((?P\w+):)?(?P[a-zA-Z0-9_-]+))' + r'(?P\[.+\])?') self._re_attrs = re.compile(r'(\w+)\s*=\s*\"?(\w+)\"?') def match_expression(self, node_expr): @@ -121,7 +123,7 @@ def set_attribute(self, elem, key, value): elem.attrib[key] = value @classmethod - def remove_attributes(cls, elem, keys, exclude = []): + def remove_attributes(cls, elem, keys, exclude=[]): for key in keys: if key in exclude: continue @@ -143,7 +145,8 @@ def tostring(self, oelem, **attrs): self.remove_attributes(el, keys, exclude=['facho_optional']) is_optional = el.get('facho_optional', 'False') == 'True' - if is_optional and el.getchildren() == [] and el.keys() == ['facho_optional']: + if is_optional and el.getchildren() == [] and el.keys() == [ + 'facho_optional']: el.getparent().remove(el) return tostring(elem, **attrs).decode('utf-8') @@ -153,14 +156,15 @@ class FachoXML: """ Decora XML con funciones de consulta XPATH de un solo elemento """ - def __init__(self, root, builder=None, nsmap=None, fragment_prefix='',fragment_root_element=None): + def __init__(self, root, builder=None, nsmap=None, fragment_prefix='', + fragment_root_element=None): if builder is None: self.builder = LXMLBuilder(nsmap) else: self.builder = builder self.nsmap = nsmap - + if isinstance(root, str): self.root = self.builder.build_element_from_string(root, nsmap) else: @@ -180,14 +184,19 @@ def from_string(cls, document: str, namespaces: dict() = []) -> 'FachoXML': def root_namespace(self): return etree.QName(self.root).namespace + def root_localname(self): + return etree.QName(self.root).localname + def append_element(self, elem, new_elem): + # elem = self.find_or_create_element(xpath, append=append) + # self.builder.append(elem, new_elem) self.builder.append(elem, new_elem) def add_extension(self, extension): extension.build(self) - - def fragment(self, xpath, append=False, append_not_exists=False): + def fragment( + self, xpath, append=False, append_not_exists=False): nodes = xpath.split('/') nodes.pop() root_prefix = '/'.join(nodes) @@ -197,7 +206,9 @@ def fragment(self, xpath, append=False, append_not_exists=False): if parent is None: parent = self.find_or_create_element(xpath, append=append) - return FachoXML(parent, nsmap=self.nsmap, fragment_prefix=root_prefix, fragment_root_element=self.root) + return FachoXML( + parent, nsmap=self.nsmap, fragment_prefix=root_prefix, + fragment_root_element=self.root) def register_alias_xpath(self, alias, xpath): self.xpath_for[alias] = xpath @@ -233,7 +244,8 @@ def find_or_create_element(self, xpath, append=False): """ xpath = self._path_xpath_for(xpath) node_paths = xpath.split('/') - node_paths.pop(0) #remove empty / + # remove empty / + node_paths.pop(0) root_tag = node_paths.pop(0) root_node = self.builder.build_from_expression(root_tag) @@ -241,10 +253,10 @@ def find_or_create_element(self, xpath, append=False): # restaurar ya que no es la raiz y asignar actual como raiz node_paths.insert(0, root_tag) root_node = self.root - + if not self.builder.same_tag(root_node.tag, self.root.tag): - - raise ValueError('xpath %s must be absolute to /%s' % (xpath, self.root.tag)) + raise ValueError('xpath %s must be absolute to /%s' % ( + xpath, self.root.tag)) # crea jerarquia segun xpath indicado parent = None @@ -254,8 +266,8 @@ def find_or_create_element(self, xpath, append=False): for node_path in node_paths: node_expr = self.builder.match_expression(node_path) node = self.builder.build_from_expression(node_path) - - child = self.builder.find_relative(current_elem, node_expr['path'], self.nsmap) + child = self.builder.find_relative( + current_elem, node_expr['path'], self.nsmap) parent = current_elem if child is not None: @@ -266,11 +278,12 @@ def find_or_create_element(self, xpath, append=False): node_expr = self.builder.match_expression(node_tag) node = self.builder.build_from_expression(node_tag) - child = self.builder.find_relative(current_elem, node_expr['path'], self.nsmap) + child = self.builder.find_relative( + current_elem, node_expr['path'], self.nsmap) parent = current_elem if child is not None: current_elem = child - + if parent == current_elem: self.builder.append(parent, node) return node @@ -287,9 +300,10 @@ def find_or_create_element(self, xpath, append=False): self.builder.append(parent, node) return node - if self.builder.is_attribute(last_slibing, 'facho_placeholder', 'True'): + if self.builder.is_attribute( + last_slibing, 'facho_placeholder', 'True'): self._remove_facho_attributes(last_slibing) - return last_slibing + return last_slibing self.builder.append_next(last_slibing, node) return node @@ -300,7 +314,8 @@ def find_or_create_element(self, xpath, append=False): self._remove_facho_attributes(current_elem) return current_elem - def set_element_validator(self, xpath, validator = False): + def set_element_validator( + self, xpath, validator=False): """ validador al asignar contenido a xpath indicado @@ -313,8 +328,9 @@ def set_element_validator(self, xpath, validator = False): self._validators[key] = lambda v, attrs: True else: self._validators[key] = validator - - def set_element(self, xpath, content, **attrs): + + def set_element( + self, xpath, content, **attrs): """ asigna contenido ubicado por ruta tipo XPATH. @param xpath ruta tipo XPATH @@ -356,7 +372,8 @@ def set_attributes(self, xpath, **attrs): self.builder.set_attribute(elem, k, str(v)) return self - def get_element_attribute(self, xpath, attribute, multiple=False): + def get_element_attribute( + self, xpath, attribute, multiple=False): elem = self.get_element(xpath, multiple=multiple) if elem is None: @@ -393,14 +410,16 @@ def get_element_text(self, xpath, format_=str, multiple=False): return None return format_(text) - def get_element_text_or_attribute(self, xpath, default=None, multiple=False, raise_on_fail=False): + def get_element_text_or_attribute( + self, xpath, default=None, multiple=False, raise_on_fail=False): parts = xpath.split('/') - is_attribute = parts[-1].startswith('@') + 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) + val = self.get_element_attribute( + element_path, attribute_name, multiple=multiple) if val is None: return default return val @@ -433,7 +452,8 @@ def get_elements_text_or_attributes(self, xpaths, raise_on_fail=True): if isinstance(xpath, tuple): val = xpath[0] else: - val = self.get_element_text_or_attribute(xpath, raise_on_fail=raise_on_fail) + val = self.get_element_text_or_attribute( + xpath, raise_on_fail=raise_on_fail) vals.append(val) return vals @@ -455,7 +475,8 @@ def exist_element(self, xpath): return True def _remove_facho_attributes(self, elem): - self.builder.remove_attributes(elem, ['facho_optional', 'facho_placeholder']) + self.builder.remove_attributes( + elem, ['facho_optional', 'facho_placeholder']) def tostring(self, **kw): return self.builder.tostring(self.root, **kw) @@ -467,15 +488,17 @@ def xpath_from_root(self, xpath): root = self.root if self.fragment_root_element is not None: root = self.fragment_root_element - + if isinstance(self.nsmap, dict): nsmap = dict(map(reversed, self.nsmap.items())) ns = nsmap[etree.QName(root).namespace] + ':' if self.fragment_root_element is not None: - new_xpath = '/' + ns + etree.QName(root).localname + '/' + etree.QName(self.root).localname + '/' + xpath.lstrip('/') + new_xpath = '/' + ns + etree.QName(root).localname + '/' + \ + etree.QName(self.root).localname + '/' + xpath.lstrip('/') else: - new_xpath = '/' + ns + etree.QName(root).localname + '/' + xpath.lstrip('/') + new_xpath = '/' + ns + etree.QName(root).localname + '/' + \ + xpath.lstrip('/') return new_xpath def __str__(self): diff --git a/facho/fe/__init__.py b/facho/fe/__init__.py index 11adce3..fdfaedc 100644 --- a/facho/fe/__init__.py +++ b/facho/fe/__init__.py @@ -5,6 +5,7 @@ from .fe import DianXMLExtensionSoftwareSecurityCode from .fe import DianXMLExtensionCUFE from .fe import DianXMLExtensionCUDE +from .fe import DianXMLExtensionCUDS from .fe import DianXMLExtensionInvoiceAuthorization from .fe import DianXMLExtensionSoftwareProvider from .fe import DianXMLExtensionAuthorizationProvider diff --git a/facho/fe/client/dian.py b/facho/fe/client/dian.py index 732108e..3da2cbf 100644 --- a/facho/fe/client/dian.py +++ b/facho/fe/client/dian.py @@ -21,10 +21,10 @@ class SOAPService: - def wsdl(self): + def get_wsdl(self): raise NotImplementedError() - def service(self): + def get_service(self): raise NotImplementedError() def build_response(self, as_dict): @@ -63,10 +63,10 @@ class GetNumberingRange(SOAPService): accountCodeT: str softwareCode: str - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'GetNumberingRange' def build_response(self, as_dict): @@ -78,10 +78,10 @@ class SendBillAsync(SOAPService): fileName: str contentFile: str - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'SendBillAsync' def build_response(self, as_dict): @@ -106,10 +106,10 @@ class SendTestSetAsync(SOAPService): contentFile: str testSetId: str = '' - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'SendTestSetAsync' def build_response(self, as_dict): @@ -120,10 +120,10 @@ class SendBillSync(SOAPService): fileName: str contentFile: bytes - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'SendBillSync' def build_response(self, as_dict): @@ -153,10 +153,10 @@ def fromdict(cls, data): class GetStatus(SOAPService): trackId: bytes - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'GetStatus' def build_response(self, as_dict): @@ -166,10 +166,10 @@ def build_response(self, as_dict): class GetStatusZip(SOAPService): trackId: bytes - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'GetStatusZip' def build_response(self, as_dict): @@ -179,10 +179,10 @@ def build_response(self, as_dict): class SendNominaSync(SOAPService): contentFile: bytes - def wsdl(self): + def get_wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def service(self): + def get_service(self): return 'SendNominaSync' def build_response(self, as_dict): @@ -193,31 +193,31 @@ class Habilitacion: WSDL = 'https://vpfe-hab.dian.gov.co/WcfDianCustomerServices.svc?wsdl' class GetNumberingRange(GetNumberingRange): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class SendBillAsync(SendBillAsync): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class SendBillSync(SendBillSync): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class SendTestSetAsync(SendTestSetAsync): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class GetStatus(GetStatus): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class GetStatusZip(GetStatusZip): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class SendNominaSync(SendNominaSync): - def wsdl(self): + def get_wsdl(self): return Habilitacion.WSDL class DianGateway: @@ -226,7 +226,7 @@ def _open(self, service): raise NotImplementedError() def _remote_service(self, conn, service): - return conn.service[service.service()] + return conn.service[service.get_service()] def _close(self, conn): return @@ -250,7 +250,7 @@ def __init__(self, user, password): self._password = password def _open(self, service): - return zeep.Client(service.wsdl(), wsse=UsernameToken(self._username, self._password)) + return zeep.Client(service.get_wsdl(), wsse=UsernameToken(self._username, self._password)) class DianSignatureClient(DianGateway): @@ -264,7 +264,7 @@ def _open(self, service): # RESOLUCCION 0004: pagina 756 from zeep.wsse import utils - client = zeep.Client(service.wsdl(), wsse= + client = zeep.Client(service.get_wsdl(), wsse= BinarySignature( self.private_key_path, self.public_key_path, self.password, signature_method=xmlsec.Transform.RSA_SHA256, diff --git a/facho/fe/client/wsse/signature.py b/facho/fe/client/wsse/signature.py index cc42a3d..7295742 100644 --- a/facho/fe/client/wsse/signature.py +++ b/facho/fe/client/wsse/signature.py @@ -234,9 +234,10 @@ def _append_timestamp(security, expires_dt=None): if expires_dt is None: expires_dt = timedelta(seconds=6000) + timestamp = datetime.now() etimestamp = utils.WSU.Timestamp({'{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Id': utils.get_unique_id()}) - etimestamp.append(utils.WSU.Created(get_timestamp())) - etimestamp.append(utils.WSU.Expires(get_timestamp(delta=expires_dt))) + etimestamp.append(utils.WSU.Created(get_timestamp(timestamp=timestamp))) + etimestamp.append(utils.WSU.Expires(get_timestamp(timestamp=timestamp, delta=expires_dt))) security.insert(0, etimestamp) if etree.LXML_VERSION[:2] >= (3, 5): etree.cleanup_namespaces(security, diff --git a/facho/fe/data/dian/codelist/TarifaImpuestoReteIVA-2.1.gc b/facho/fe/data/dian/codelist/TarifaImpuestoReteIVA-2.1.gc index 2720b0e..cd62b2d 100644 --- a/facho/fe/data/dian/codelist/TarifaImpuestoReteIVA-2.1.gc +++ b/facho/fe/data/dian/codelist/TarifaImpuestoReteIVA-2.1.gc @@ -1,50 +1,61 @@ - - - - - TarifaImpuestos - Tarifas por Impuesto - 1 - urn:dian:names:especificacion:ubl:listacodigos:gc:TarifaImpuestos - urn:dian:names:especificacion:ubl:listacodigos:gc:TarifaImpuestos-2.1 - http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TarifaImpuestos-2.1.gc - - DIAN (Dirección de Impuestos y Aduanas Nacionales) - 195 - - - - - Code - Codigo Comun - - - - Name - Nombre - - - - Description - Descripcion - - - - CodeKey - - - - - - - 15.00 - - - ReteIVA - - - ReteIVA - - - - + + + + + TarifaImpuestos + Tarifas por Impuesto + 1 + urn:dian:names:especificacion:ubl:listacodigos:gc:TarifaImpuestos + urn:dian:names:especificacion:ubl:listacodigos:gc:TarifaImpuestos-2.1 + http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TarifaImpuestos-2.1.gc + + DIAN (Dirección de Impuestos y Aduanas Nacionales) + 195 + + + + + Code + Codigo Comun + + + + Name + Nombre + + + + Description + Descripcion + + + + CodeKey + + + + + + + 15.00 + + + ReteIVA + + + ReteIVA + + + + + 100.00 + + + ReteIVA + + + ReteIVA + + + + diff --git a/facho/fe/data/dian/codelist/TarifaImpuestoReteFuente-2.1.gc b/facho/fe/data/dian/codelist/TarifaImpuestoReteRenta-2.1.gc similarity index 100% rename from facho/fe/data/dian/codelist/TarifaImpuestoReteFuente-2.1.gc rename to facho/fe/data/dian/codelist/TarifaImpuestoReteRenta-2.1.gc diff --git a/facho/fe/data/dian/codelist/TipoDocumento-2.1.gc b/facho/fe/data/dian/codelist/TipoDocumento-2.1.gc index 80c7b96..ee0b9ef 100644 --- a/facho/fe/data/dian/codelist/TipoDocumento-2.1.gc +++ b/facho/fe/data/dian/codelist/TipoDocumento-2.1.gc @@ -1,74 +1,100 @@ - - - - - TipoDocumento - Tipo de Documento - 1 - urn:dian:names:especificacion:ubl:listacodigos:gc:TipoDocumento - urn:dian:names:especificacion:ubl:listacodigos:gc:TipoDocumento-2.1 - http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TipoDocumento-2.1.gc - - DIAN (Dirección de Impuestos y Aduanas Nacionales) - 195 - - - - - Code - Codigo Comun - - - - Name - Nombre - - - - CodeKey - - - - - - - 01 - - - Factura de Venta Nacional - - - - - 02 - - - Factura de Exportación - - - - - 03 - - - Factura de Contingencia - - - - - 91 - - - Nota Crédito - - - - - 92 - - - Nota Débito - - - + + + + + TipoDocumento + Tipo de Documento + 1 + urn:dian:names:especificacion:ubl:listacodigos:gc:TipoDocumento + urn:dian:names:especificacion:ubl:listacodigos:gc:TipoDocumento-2.1 + http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TipoDocumento-2.1.gc + + DIAN (Dirección de Impuestos y Aduanas Nacionales) + 195 + + + + + Code + Codigo Comun + + + + Name + Nombre + + + + CodeKey + + + + + + + 01 + + + Factura electrónica de Venta + + + Tipos de factura + + + + + 02 + + + Factura electrónica de venta con propósito de exportación + + + Tipos de factura + + + + + 03 + + + Factura de talonario o papel con numeración de contingencia. + + + Tipos de factura + + + + + 04 + + + Factura electrónica de Venta por Contingencia DIAN + + + Tipos de factura + + + + + 91 + + + Nota Crédito + + + Exclusivo en referencias a documentos (elementos DocumentReference) + + + + + 92 + + + Nota Débito + + + Exclusivo en referencias a documentos (elementos DocumentReference) + + + diff --git a/facho/fe/data/dian/codelist/TipoImpuesto-2.1.gc b/facho/fe/data/dian/codelist/TipoImpuesto-2.1.gc index 4d1bf8b..c70c793 100644 --- a/facho/fe/data/dian/codelist/TipoImpuesto-2.1.gc +++ b/facho/fe/data/dian/codelist/TipoImpuesto-2.1.gc @@ -1,162 +1,171 @@ - - - - - TipoImpuesto - Tipo de Tributos - 1 - urn:dian:names:especificacion:ubl:listacodigos:gc:TipoImpuesto - urn:dian:names:especificacion:ubl:listacodigos:gc:TipoImpuesto-2.1 - http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TipoImpuesto-2.1.gc - - DIAN (Dirección de Impuestos y Aduanas Nacionales) - 195 - - - - - Code - Codigo Comun - - - - Name - Nombre - - - - CodeKey - - - - - - - 01 - - - IVA - - - - - 02 - - - IC - - - - - 03 - - - ICA - - - - - 04 - - - INC - - - - - 05 - - - ReteIVA - - - - - 06 - - - ReteFuente - - - - - 07 - - - ReteICA - - - - - 08 - - - ReteCREE - - - - - 20 - - - FtoHorticultura - - - - - 21 - - - Timbre - - - - - 22 - - - Bolsas - - - - - 23 - - - INCarbono - - - - - 24 - - - INCombustibles - - - - - 25 - - - Sobretasa Combustibles - - - - - 26 - - - Sordicom - - - - - ZZ - - - Nombre de la figura tributaria - - - + + + + + TipoImpuesto + Tipo de Tributos + 1 + urn:dian:names:especificacion:ubl:listacodigos:gc:TipoImpuesto + urn:dian:names:especificacion:ubl:listacodigos:gc:TipoImpuesto-2.1 + http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TipoImpuesto-2.1.gc + + DIAN (Dirección de Impuestos y Aduanas Nacionales) + 195 + + + + + Code + Codigo Comun + + + + Name + Nombre + + + + CodeKey + + + + + + + 01 + + + IVA + + + + + 02 + + + IC + + + + + 03 + + + ICA + + + + + 04 + + + INC + + + + + 05 + + + ReteIVA + + + + + 06 + + + ReteRenta + + + + + 07 + + + ReteICA + + + + + 08 + + + ReteCREE + + + + + 20 + + + FtoHorticultura + + + + + 21 + + + Timbre + + + + + 22 + + + Bolsas + + + + + 23 + + + INCarbono + + + + + 24 + + + INCombustibles + + + + + 25 + + + Sobretasa Combustibles + + + + + 26 + + + Sordicom + + + + + 30 + + + Impuesto al Consumo de Datos + + + + + + ZZ + + + Nombre de la figura tributaria + + + diff --git a/facho/fe/data/dian/codelist/TipoOperacionNCDS-2.1.gc b/facho/fe/data/dian/codelist/TipoOperacionNCDS-2.1.gc new file mode 100644 index 0000000..33002a4 --- /dev/null +++ b/facho/fe/data/dian/codelist/TipoOperacionNCDS-2.1.gc @@ -0,0 +1,47 @@ + + + + TipoOperacion + Tipo de operacion + 1 + urn:dian:names:especificacion:ubl:listacodigos:gc:TipoOperacion + urn:dian:names:especificacion:ubl:listacodigos:gc:TipoOperacion-2.1 + http://dian.gov.co/ubl/os-ubl-2.0/cl/gc/default/TipoOperacion-2.1.gc + + DIAN (Dirección de Impuestos y Aduanas Nacionales) + 195 + + + + + Code + + + + Nombre + + + + CodeKey + + + + + + + 10 + + + Residente + + + + + 11 + + + No Residente + + + + diff --git a/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.custom.gc b/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.custom.gc index 74ee5d8..9c1ddd6 100644 --- a/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.custom.gc +++ b/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.custom.gc @@ -30,46 +30,6 @@ - - - O-99 - - - Otro tipo de obligado - - - - - O-06 - - - Ingresos y patrimonio - - - - - O-07 - - - Retención en la fuente a título de renta - - - - - O-08 - - - Retención timbre nacional - - - - - O-09 - - - Retención en la fuente en el impuesto sobre las ventas - - O-13 @@ -78,14 +38,6 @@ Gran contribuyente - - - O-14 - - - Informante de exógena - - O-15 @@ -94,38 +46,6 @@ Autorretenedor - - - O-16 - - - Obligación de facturar por ingresos de bienes y/o servicios excluidos - - - - - O-17 - - - Profesionales de compra y venta de divisas - - - - - O-19 - - - Productor y/o exportador de bienes exentos - - - - - O-22 - - - Obligado a cumplir deberes formales a nombre de terceros - - O-23 @@ -134,62 +54,6 @@ Agente de retención en el impuesto sobre las ventas - - - O-32 - - - Impuesto Nacional a la Gasolina y al ACPM - - - - - O-33 - - - Impuesto Nacional al consumo - - - - - O-34 - - - Régimen simplificado impuesto nacional consumo rest y bares - - - - - O-36 - - - Establecimiento Permanente - - - - - O-37 - - - Obligado a Facturar Electrónicamente Modelo 2242 - - - - - O-38 - - - Facturación Electrónica Voluntaria Modelo 2242 - - - - - O-39 - - - Proveedor de Servicios Tecnológicos PST Modelo 2242 - - O-47 @@ -214,782 +78,6 @@ No responsable de IVA - - - O-52 - - - Facturador electrónico - - - - - O-99 - - - Otro tipo de obligado - - - - - R-00-PN - - - Clientes del Exterior - - - - - R-12-PN - - - Factor PN - - - - - R-16-PN - - - Mandatario - - - - - R-25-PN - - - Agente Interventor - - - - - R-99-PN - - - No responsable - - - - - R-06-PJ - - - Apoderado especial - - - - - R-07-PJ - - - Apoderado general - - - - - R-12-PJ - - - Factor - - - - - R-16-PJ - - - Mandatario - - - - - R-99-PJ - - - Otro tipo de responsable - - - - - A-01 - - - Agente de carga internacional - - - - - A-02 - - - Agente marítimo - - - - - A-03 - - - Almacén general de depósito - - - - - A-04 - - - Comercializadora internacional (C.I.) - - - - - A-05 - - - Comerciante de la zona aduanera especial de Inírida, Puerto Carreño, Cumaribo y Primavera - - - - - A-06 - - - Comerciantes de la zona de régimen aduanero especial de Leticia - - - - - A-07 - - - Comerciantes de la zona de régimen aduanero especial de Maicao, Uribia y Manaure - - - - - A-08 - - - Comerciantes de la zona de régimen aduanero especial de Urabá, Tumaco y Guapí - - - - - A-09 - - - Comerciantes del puerto libre de San Andrés, Providencia y Santa Catalina - - - - - A-10 - - - Depósito público de apoyo logístico internacional - - - - - A-11 - - - Depósito privado para procesamiento industrial - - - - - A-12 - - - Depósito privado de transformación o ensamble - - - - - A-13 - - - Depósito franco - - - - - A-14 - - - Depósito privado aeronáutico - - - - - A-15 - - - Depósito privado para distribución internacional - - - - - A-16 - - - Depósito privado de provisiones de a bordo para consumo y para llevar - - - - - A-17 - - - Depósito privado para envíos urgentes - - - - - A-18 - - - Depósito privado - - - - - A-19 - - - Depósito público - - - - - A-20 - - - Depósito público para distribución internacional - - - - - A-21 - - - Exportador de café - - - - - A-22 - - - Exportador - - - - - A-23 - - - Importador - - - - - A-24 - - - Intermediario de tráfico postal y envíos urgentes - - - - - A-25 - - - Operador de transporte multimodal - - - - - A-26 - - - Sociedad de intermediación aduanera - - - - - A-27 - - - Titular de puertos y muelles de servicio público o privado - - - - - A-28 - - - Transportador 263nfor régimen de importación y/o exportación - - - - - A-29 - - - Transportista nacional para operaciones del régimen de tránsito aduanero - - - - - A-30 - - - Usuario comercial zona franca - - - - - A-32 - - - Usuario industrial de bienes zona franca - - - - - A-34 - - - Usuario industrial de servicios zona franca - - - - - A-36 - - - Usuario operador de zona franca - - - - - A-37 - - - Usuario aduanero permanente - - - - - A-38 - - - Usuario altamente exportador - - - - - A-39 - - - Usuario de zonas económicas especiales de exportación - - - - - A-40 - - - Deposito privado de instalaciones industriales - - - - - A-41 - - - Beneficiarios de programas especiales de exportación PEX - - - - - A-42 - - - Depósitos privados para mercancías en tránsito San Andrés - - - - - A-43 - - - Observadores de las operaciones de importación - - - - - A-44 - - - Usuarios sistemas especiales Importación exportación - - - - - A-46 - - - Transportador 263nformac régimen de importación y/o exportación - - - - - A-47 - - - Transportador terrestre régimen de importación y/o exportación - - - - - A-48 - - - Aeropuerto de servicio publico o privado - - - - - A-49 - - - Transportador fluvial régimen de importación - - - - - A-50 - - - Usuario industrial zona franca especial - - - - - A-53 - - - Agencias de aduanas 1 - - - - - A-54 - - - Usuario Operador Zona Franca Especial - - - - - A-55 - - - Agencias de aduanas 2 - - - - - A-56 - - - Agencias de aduanas 3 - - - - - A-57 - - - Agencias de aduanas 4 - - - - - A-58 - - - Transportador aéreo nacional - - - - - A-60 - - - Transportador aéreo, marítimo o fluvial modalidad Cabotaje - - - - - A-61 - - - Importador de alimentos de consumo humano y animal - - - - - A-62 - - - Importador Ocasional - - - - - A-63 - - - Importador de maquinaría y sus partes Decreto 2261 de 2012 - - - - - A-64 - - - Beneficiario Programa de Fomento Industria Automotriz-PROFIA - - - - - A-99 - - - Otro tipo de agente aduanero - - - - - E-01 - - - Agencia - - - - - E-02 - - - Establecimiento de comercio - - - - - E-03 - - - Centro de explotación agrícola - - - - - E-04 - - - Centro de explotación animal - - - - - E-05 - - - Centro de explotación minera - - - - - E-06 - - - Centro de explotación de transformación - - - - - E-07 - - - Centro de explotación de servicios - - - - - E-08 - - - Oficina - - - - - E-09 - - - Sede - - - - - E-10 - - - Sucursal - - - - - E-11 - - - Consultorio - - - - - E-12 - - - Administraciones - - - - - E-13 - - - Seccionales - - - - - E-14 - - - Regionales - - - - - E-15 - - - Intendencias - - - - - E-16 - - - Local o negocio - - - - - E-17 - - - Punto de venta - - - - - E-18 - - - Fábrica - - - - - E-19 - - - Taller - - - - - E-20 - - - Cantera - - - - - E-21 - - - Pozo de Petróleo y Gas - - - - - E-22 - - - Otro lug de tipo de extrac explotación de recursos naturales - - - - - E-99 - - - Otro tipo de establecimiento - - - - - O-13 - - - Gran contribuyente - - - - - O-15 - - - Autorretenedor - - - - - O-23 - - - Agente de retención IVA - - - - - O-47 - - - Régimen simple de tributación - - R-99-PN diff --git a/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.gc b/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.gc index 9ba4739..36bd0d3 100644 --- a/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.gc +++ b/facho/fe/data/dian/codelist/TipoResponsabilidad-2.1.gc @@ -62,5 +62,13 @@ Régimen simple de tributación + + + ZZ + + + No aplica + + diff --git a/facho/fe/data/dian/codelist/__init__.py b/facho/fe/data/dian/codelist/__init__.py index feeaa45..837009d 100644 --- a/facho/fe/data/dian/codelist/__init__.py +++ b/facho/fe/data/dian/codelist/__init__.py @@ -82,11 +82,16 @@ def path_for_codelist(name): TipoDocumento = CodeList(path_for_codelist('TipoDocumento-2.1.gc'), 'code', 'name') TipoImpuesto = CodeList(path_for_codelist('TipoImpuesto-2.1.gc'), 'code', 'name')\ .update(CodeList(path_for_codelist('TipoImpuesto-2.1.custom.gc'), 'code', 'name')) +TarifaImpuesto = CodeList(path_for_codelist('TarifaImpuestoINC-2.1.gc'), 'code', 'name')\ + .update(CodeList(path_for_codelist('TarifaImpuestoIVA-2.1.gc'), 'code', 'name'))\ + .update(CodeList(path_for_codelist('TarifaImpuestoReteIVA-2.1.gc'), 'code', 'name'))\ + .update(CodeList(path_for_codelist('TarifaImpuestoReteRenta-2.1.gc'), 'code', 'name')) CodigoPrecioReferencia = CodeList(path_for_codelist('CodigoPrecioReferencia-2.1.gc'), 'code', 'name') MediosPago = CodeList(path_for_codelist('MediosPago-2.1.gc'), 'code', 'name') FormasPago = CodeList(path_for_codelist('FormasPago-2.1.gc'), 'code', 'name') RegimenFiscal = CodeList(path_for_codelist('RegimenFiscal-2.1.custom.gc'), 'code', 'name') TipoOperacionNC = CodeList(path_for_codelist('TipoOperacionNC-2.1.gc'), 'code', 'name') +TipoOperacionNCDS = CodeList(path_for_codelist('TipoOperacionNCDS-2.1.gc'), 'code', 'name') TipoOperacionND = CodeList(path_for_codelist('TipoOperacionND-2.1 - copia.gc'), 'code', 'name') TipoOperacionF = CodeList(path_for_codelist('TipoOperacionF-2.1.gc'), 'code', 'name')\ .update(CodeList(path_for_codelist('TipoOperacionF-2.1.custom.gc'), 'code', 'name')) diff --git a/facho/fe/fe.py b/facho/fe/fe.py index 23f5448..d57ca04 100644 --- a/facho/fe/fe.py +++ b/facho/fe/fe.py @@ -1,6 +1,5 @@ # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. - from ..facho import FachoXML, FachoXMLExtension, LXMLBuilder import uuid import xmlsig @@ -8,13 +7,16 @@ from datetime import datetime import OpenSSL import zipfile -import warnings +# import warnings import hashlib from contextlib import contextmanager from .data.dian import codelist from . import form from collections import defaultdict -from pathlib import Path +# from pathlib import Path +from dateutil import tz + +from cryptography.hazmat.primitives.serialization import pkcs12 AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] @@ -30,33 +32,50 @@ POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf' POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.' +Bogota = tz.gettz('America/Bogota') +# NAMESPACES = { +# 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2', +# 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual', +# 'nominaajuste': 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste', +# 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1', +# 'xs': 'http://www.w3.org/2001/XMLSchema-instance', +# 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', +# 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', +# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', +# 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001', +# 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003', +# 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', +# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', +# 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', +# 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', +# 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', +# 'xades': 'http://uri.etsi.org/01903/v1.3.2#', +# 'xades141': 'http://uri.etsi.org/01903/v1.4.1#', +# 'ds': 'http://www.w3.org/2000/09/xmldsig#', +# 'sig': 'http://www.w3.org/2000/09/xmldsig#', +# } + NAMESPACES = { 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2', - 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual', - 'nominaajuste': 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste', 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1', - 'xs': 'http://www.w3.org/2001/XMLSchema-instance', 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', - 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', - 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001', - 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003', 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', - 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', + 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xades': 'http://uri.etsi.org/01903/v1.3.2#', - 'xades141': 'http://uri.etsi.org/01903/v1.4.1#', 'ds': 'http://www.w3.org/2000/09/xmldsig#', - 'sig': 'http://www.w3.org/2000/09/xmldsig#', + 'xades': 'http://uri.etsi.org/01903/v1.3.2#', } + def fe_from_string(document: str) -> FachoXML: return FeXML.from_string(document) -from contextlib import contextmanager + +# from contextlib import contextmanager @contextmanager def mock_xades_policy(): from mock import patch @@ -74,30 +93,38 @@ def read(self): mock.return_value = UrllibPolicyMock() yield - + class FeXML(FachoXML): def __init__(self, root, namespace): - + # raise Exception(namespace) super().__init__("{%s}%s" % (namespace, root), nsmap=NAMESPACES) @classmethod def from_string(cls, document: str) -> 'FeXML': return super().from_string(document, namespaces=NAMESPACES) - + def tostring(self, **kw): # 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', + } + root_namespace = self.root_namespace() + root_localname = self.root_localname() xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace] + return super().tostring(**kw)\ - .replace(xmlns_name + ':', '')\ - .replace('xmlns:'+xmlns_name, 'xmlns')\ - .replace('schemaLocation', 'xsi:schemaLocation') - -class DianXMLExtensionCUDFE(FachoXMLExtension): + .replace(xmlns_name + ':', '')\ + .replace('xmlns:'+xmlns_name, 'xmlns')\ + .replace(root_namespace, urn_oasis[root_localname]) + - def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS): +class DianXMLExtensionCUDFE(FachoXMLExtension): + def __init__(self, invoice, tipo_ambiente=AMBIENTE_PRUEBAS): self.tipo_ambiente = tipo_ambiente self.invoice = invoice @@ -123,10 +150,24 @@ def build(self, fachoxml): schemeID=self.tipo_ambiente, schemeName=self.schemeName()) - fachoxml.set_element('./cbc:ProfileExecutionID', self._tipo_ambiente_int()) + if self.schemeName() == "CUDS-SHA384": + if fachoxml.tag_document() == 'Invoice': + fachoxml.set_element('./cbc:ProfileID', + 'DIAN 2.1: documento soporte en adquisiciones efectuadas a no obligados a facturar.') + else: + fachoxml.set_element('./cbc:ProfileID', + 'DIAN 2.1: Nota de ajuste al documento soporte en adquisiciones efectuadas a sujetos no obligados a expedir factura o documento equivalente') + else: + fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') + + # #DIAN 1.8.-2021: FAD03 + # fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') + fachoxml.set_element( + './cbc:ProfileExecutionID', self._tipo_ambiente_int()) #DIAN 1.7.-2020: FAB36 - fachoxml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode', - self._get_qrcode(cufe)) + fachoxml.set_element( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode', + self._get_qrcode(cufe)) def issue_time(self, datetime_): return datetime_.strftime('%H:%M:%S-05:00') @@ -142,7 +183,8 @@ def buildVars(self): build_vars['HoraFac'] = self.issue_time(invoice.invoice_issue) # PAG 601 build_vars['ValorBruto'] = invoice.invoice_legal_monetary_total.line_extension_amount - build_vars['ValorTotalPagar'] = invoice.invoice_legal_monetary_total.payable_amount + build_vars['ValorTotalPagar' + ] = invoice.invoice_legal_monetary_total.payable_amount ValorImpuestoPara = defaultdict(lambda: form.Amount(0.0)) build_vars['CodImpuesto1'] = '01' build_vars['CodImpuesto2'] = '04' @@ -172,7 +214,8 @@ def _generate_cufe(self): class DianXMLExtensionCUFE(DianXMLExtensionCUDFE): - def __init__(self, invoice, clave_tecnica = '', tipo_ambiente = AMBIENTE_PRUEBAS): + def __init__( + self, invoice, clave_tecnica='', tipo_ambiente=AMBIENTE_PRUEBAS): self.tipo_ambiente = tipo_ambiente self.clave_tecnica = clave_tecnica self.invoice = invoice @@ -208,6 +251,7 @@ def formatVars(self): '%d' % build_vars['TipoAmb'], ] + class DianXMLExtensionCUDE(DianXMLExtensionCUDFE): def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS): self.tipo_ambiente = tipo_ambiente @@ -245,6 +289,41 @@ def formatVars(self): '%d' % build_vars['TipoAmb'], ] + +class DianXMLExtensionCUDS(DianXMLExtensionCUDFE): + def __init__(self, invoice, software_pin, tipo_ambiente = AMBIENTE_PRUEBAS): + self.tipo_ambiente = tipo_ambiente + self.software_pin = software_pin + self.invoice = invoice + + def schemeName(self): + return 'CUDS-SHA384' + + def buildVars(self): + build_vars = super().buildVars() + build_vars['Software-PIN'] = str(self.software_pin) + return build_vars + + def formatVars(self): + build_vars = self.buildVars() + CodImpuesto1 = build_vars['CodImpuesto1'] + CodImpuesto2 = build_vars['CodImpuesto2'] + CodImpuesto3 = build_vars['CodImpuesto3'] + return [ + '%s' % build_vars['NumFac'], + '%s' % build_vars['FecFac'], + '%s' % build_vars['HoraFac'], + form.Amount(build_vars['ValorBruto']).truncate_as_string(2), + CodImpuesto1, + form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2), + form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2), + '%s' % build_vars['NitOFE'], + '%s' % build_vars['NumAdq'], + '%s' % build_vars['Software-PIN'], + '%d' % build_vars['TipoAmb'], + ] + + class DianXMLExtensionSoftwareProvider(FachoXMLExtension): # RESOLUCION 0004: pagina 108 @@ -254,7 +333,8 @@ def __init__(self, nit, dv, id_software: str): self.id_software = id_software def build(self, fexml): - software_provider = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') + software_provider = fexml.fragment( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') provider_id_attrs = SCHEME_AGENCY_ATTRS.copy() provider_id_attrs.update({'schemeID': self.dv}) #DIAN 1.7.-2020: FAB23 @@ -284,7 +364,6 @@ def build(self, fexml): class DianXMLExtensionSigner: - def __init__(self, pkcs12_path, passphrase=None, localpolicy=True): self._pkcs12_data = open(pkcs12_path, 'rb').read() self._passphrase = None @@ -295,17 +374,18 @@ def __init__(self, pkcs12_path, passphrase=None, localpolicy=True): @classmethod def from_bytes(cls, data, passphrase=None, localpolicy=True): self = cls.__new__(cls) - self._pkcs12_data = data self._passphrase = None self._localpolicy = localpolicy if passphrase: self._passphrase = passphrase.encode('utf-8') - + return self def _element_extension_content(self, fachoxml): - return fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent') + return fachoxml.builder.xpath( + fachoxml.root, + './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent') def sign_xml_string(self, document): xml = LXMLBuilder.from_string(document) @@ -327,7 +407,6 @@ def sign_xml_element(self, xml): ) xml.append(signature) - ref = xmlsig.template.add_reference( signature, xmlsig.constants.TransformSha256, uri="", name="xmldsig-%s-ref0" % (id_uuid) ) @@ -335,14 +414,16 @@ def sign_xml_element(self, xml): id_keyinfo = "xmldsig-%s-KeyInfo" % (id_uuid) xmlsig.template.add_reference( - signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_keyinfo), name="xmldsig-%s-ref1" % (id_uuid), + signature, xmlsig.constants.TransformSha256, uri="#%s" % ( + id_keyinfo), name="xmldsig-%s-ref1" % (id_uuid), ) ki = xmlsig.template.ensure_key_info(signature, name=id_keyinfo) data = xmlsig.template.add_x509_data(ki) xmlsig.template.x509_data_add_certificate(data) xmlsig.template.add_key_value(ki) - qualifying = xades.template.create_qualifying_properties(signature, 'XadesObjects', 'xades') + qualifying = xades.template.create_qualifying_properties( + signature, 'XadesObjects', 'xades') xades.utils.ensure_id(qualifying) id_props = "xmldsig-%s-signedprops" % (id_uuid) @@ -350,10 +431,12 @@ def sign_xml_element(self, xml): signature, xmlsig.constants.TransformSha256, uri="#%s" % (id_props), uri_type="http://uri.etsi.org/01903#SignedProperties" ) - xmlsig.template.add_transform(props_ref, xmlsig.constants.TransformInclC14N) + xmlsig.template.add_transform( + props_ref, xmlsig.constants.TransformInclC14N) # TODO assert with http://www.sic.gov.co/hora-legal-colombiana - props = xades.template.create_signed_properties(qualifying, name=id_props, datetime=datetime.now()) + props = xades.template.create_signed_properties( + qualifying, name=id_props, datetime=datetime.now(tz=Bogota)) xades.template.add_claimed_role(props, "supplier") policy = xades.policy.GenericPolicyId( @@ -361,9 +444,13 @@ def sign_xml_element(self, xml): POLICY_NAME, xmlsig.constants.TransformSha256) ctx = xades.XAdESContext(policy) - ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(self._pkcs12_data, - self._passphrase)) + ctx.load_pkcs12(pkcs12.load_key_and_certificates( + self._pkcs12_data, + self._passphrase)) + # ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12( + # self._pkcs12_data, + # self._passphrase)) if self._localpolicy: with mock_xades_policy(): ctx.sign(signature) @@ -371,7 +458,7 @@ def sign_xml_element(self, xml): else: ctx.sign(signature) ctx.verify(signature) - #xmlsig take parent root + # xmlsig take parent root xml.remove(signature) return signature @@ -380,29 +467,28 @@ def build(self, fachoxml): extcontent = self._element_extension_content(fachoxml) fachoxml.append_element(extcontent, signature) - + class DianXMLExtensionAuthorizationProvider(FachoXMLExtension): # RESOLUCION 0004: pagina 176 def build(self, fexml): attrs = {'schemeID': '4', 'schemeName': '31'} attrs.update(SCHEME_AGENCY_ATTRS) - authorization_provider = fexml.fragment('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider') authorization_provider.set_element('./sts:AuthorizationProviderID', '800197268', **attrs) - class DianXMLExtensionInvoiceSource(FachoXMLExtension): # CAB13 def build(self, fexml): dian_path = '/fe:CreditNote/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode' - fexml.set_element(dian_path, 'CO', - listAgencyID="6", - listAgencyName="United Nations Economic Commission for Europe", - listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1") + fexml.set_element( + dian_path, 'CO', + listAgencyID="6", + listAgencyName="United Nations Economic Commission for Europe", + listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1") class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension): @@ -432,16 +518,15 @@ def build(self, fexml): invoice_control.set_element('/sts:InvoiceControl/sts:AuthorizedInvoices/sts:To', self.to) - fexml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode', - 'CO', - #DIAN 1.7.-2020: FAB15 - listAgencyID="6", - #DIAN 1.7.-2020: FAB16 - listAgencyName="United Nations Economic Commission for Europe", - #DIAN 1.7.-2020: FAB17 - listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1" - ) - + fexml.set_element( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource/cbc:IdentificationCode', + 'CO', + # DIAN 1.7.-2020: FAB15 + listAgencyID="6", + # DIAN 1.7.-2020: FAB16 + listAgencyName="United Nations Economic Commission for Europe", + # DIAN 1.7.-2020: FAB17 + listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1") class DianZIP: @@ -450,7 +535,8 @@ class DianZIP: MAX_FILES = 50 def __init__(self, file_like): - self.zipfile = zipfile.ZipFile(file_like, mode='w', compression=zipfile.ZIP_DEFLATED) + self.zipfile = zipfile.ZipFile( + file_like, mode='w', compression=zipfile.ZIP_DEFLATED) self.num_files = 0 def add_xml(self, name, xml_data): @@ -471,7 +557,6 @@ def add_invoice_xml(self, name, xml_data): def __enter__(self): """ Facilita el uso de esta manera: - f = open('xxx', 'rb') with DianZIP(f) as zip: zip.add_invoice_xml('name', 'data xml') @@ -494,7 +579,7 @@ def __init__(self, pkcs12_path_or_bytes, passphrase=None, localpolicy=True): def verify_string(self, document): # Obtener FachoXML xml = LXMLBuilder.from_string(document) - fachoxml = FachoXML(xml,nsmap=NAMESPACES) + fachoxml = FachoXML(xml, nsmap=NAMESPACES) # Obtener Signature signature = fachoxml.builder.xpath(fachoxml.root, '//ds:Signature') diff --git a/facho/fe/form/__init__.py b/facho/fe/form/__init__.py index 0aa6318..026eef8 100644 --- a/facho/fe/form/__init__.py +++ b/facho/fe/form/__init__.py @@ -1,24 +1,26 @@ # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -import hashlib -from functools import reduce -import copy +# import hashlib +# from functools import reduce +# import copy + import dataclasses -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime, date -from collections import defaultdict +# from collections import defaultdict import decimal from decimal import Decimal import typing - from ..data.dian import codelist DECIMAL_PRECISION = 6 + class AmountCurrencyError(TypeError): pass + @dataclass class Currency: code: str @@ -29,6 +31,7 @@ def __eq__(self, other): def __str__(self): return self.code + class Collection: def __init__(self, array): @@ -45,6 +48,7 @@ def map(self, mapper): def sum(self): return sum(self.array) + class AmountCollection(Collection): def sum(self): @@ -53,10 +57,13 @@ def sum(self): total += v return total + class Amount: - def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')): + def __init__( + self, amount: typing.Union[int, float, str, "Amount"], + currency: Currency = Currency('COP')): - #DIAN 1.7.-2020: 1.2.3.1 + # DIAN 1.7.-2020: 1.2.3.1 if isinstance(amount, Amount): if amount < Amount(0.0): raise ValueError('amount must be positive >= 0') @@ -67,14 +74,16 @@ def __init__(self, amount: int or float or str or Amount, currency: Currency = C if float(amount) < 0: raise ValueError('amount must be positive >= 0') - self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, - #DIAN 1.7.-2020: 1.2.1.1 - rounding=decimal.ROUND_HALF_EVEN )) + 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) @@ -92,7 +101,8 @@ def __lt__(self, other): def __eq__(self, other): if not self.is_same_currency(other): raise AmountCurrencyError() - return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION) + return round(self.amount, DECIMAL_PRECISION) == round( + other.amount, DECIMAL_PRECISION) def _cast(self, val): if type(val) in [int, float]: @@ -100,7 +110,7 @@ def _cast(self, val): if isinstance(val, Amount): return val raise TypeError("cant cast to amount") - + def __add__(self, rother): other = self._cast(rother) if not self.is_same_currency(other): @@ -124,14 +134,14 @@ def is_same_currency(self, other): def truncate_as_string(self, prec): parts = str(self.float()).split('.', 1) - return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0')) + return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec, '0')) def float(self): return float(round(self.amount, DECIMAL_PRECISION)) - + class Quantity: - + def __init__(self, val, code): if type(val) not in [float, int]: raise ValueError('val expected int or float') @@ -153,6 +163,7 @@ def __str__(self): def __repr__(self): return str(self) + @dataclass class Item: scheme_name: str @@ -163,10 +174,10 @@ class Item: class StandardItem(Item): - def __init__(self, id_: str, description: str = ''): + def __init__(self, id_: str, description: str = '', name: str = ''): super().__init__(id=id_, description=description, - scheme_name='', + scheme_name=name, scheme_id='999', scheme_agency_id='') @@ -177,9 +188,9 @@ def __init__(self, id_: str, description: str = ''): description=description, scheme_name='UNSPSC', scheme_id='001', - scheme_agency_id='10') + scheme_agency_id='10') + - @dataclass class Country: code: str @@ -190,6 +201,7 @@ def __post_init__(self): raise ValueError("code [%s] not found" % (self.code)) self.name = codelist.Paises[self.code]['name'] + @dataclass class CountrySubentity: code: str @@ -200,6 +212,7 @@ def __post_init__(self): raise ValueError("code [%s] not found" % (self.code)) self.name = codelist.Departamento[self.code]['name'] + @dataclass class City: code: str @@ -210,13 +223,22 @@ def __post_init__(self): raise ValueError("code [%s] not found" % (self.code)) self.name = codelist.Municipio[self.code]['name'] + +@dataclass +class PostalZone: + code: str = '' + + @dataclass class Address: name: str street: str = '' - city: City = City('05001') - country: Country = Country('CO') - countrysubentity: CountrySubentity = CountrySubentity('05') + city: City = field(default_factory=lambda: City('05001')) + country: Country = field(default_factory=lambda: Country('CO')) + countrysubentity: CountrySubentity = field( + default_factory=lambda: CountrySubentity('05')) + postalzone: PostalZone = field(default_factory=lambda: PostalZone('')) + @dataclass class PartyIdentification: @@ -237,6 +259,7 @@ def __post_init__(self): if self.type_fiscal not in codelist.TipoIdFiscal: raise ValueError("type_fiscal [%s] not found" % (self.type_fiscal)) + @dataclass class Responsability: codes: list @@ -261,12 +284,12 @@ class TaxScheme: code: str name: str = '' - def __post_init__(self): if self.code not in codelist.TipoImpuesto: raise ValueError("code not found") self.name = codelist.TipoImpuesto[self.code]['name'] + @dataclass class Party: name: str @@ -274,10 +297,10 @@ class Party: responsability_code: typing.List[Responsability] responsability_regime_code: str organization_code: str - tax_scheme: TaxScheme = TaxScheme('01') + tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01')) phone: str = '' - address: Address = Address('') + address: Address = field(default_factory=lambda: Address('')) email: str = '' legal_name: str = '' legal_company_ident: str = '' @@ -305,7 +328,7 @@ def __post_init__(self): class TaxSubTotal: percent: float scheme: typing.Optional[TaxScheme] = None - tax_amount: Amount = Amount(0.0) + tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) def calculate(self, invline): if self.percent is not None: @@ -315,12 +338,11 @@ def calculate(self, invline): @dataclass class TaxTotal: subtotals: list - tax_amount: Amount = Amount(0.0) - taxable_amount: Amount = Amount(0.0) + tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) + taxable_amount: Amount = field(default_factory=lambda: Amount(0.0)) def calculate(self, invline): self.taxable_amount = invline.total_amount - for subtax in self.subtotals: subtax.calculate(invline) self.tax_amount += subtax.tax_amount @@ -333,6 +355,40 @@ def __init__(self): def calculate(self, invline): pass + +@dataclass +class WithholdingTaxSubTotal: + percent: float + scheme: typing.Optional[TaxScheme] = None + tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) + + def calculate(self, invline): + if self.percent is not None: + self.tax_amount = invline.total_amount * Amount(self.percent / 100) + + +@dataclass +class WithholdingTaxTotal: + subtotals: list + tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) + taxable_amount: Amount = field(default_factory=lambda: Amount(0.0)) + + def calculate(self, invline): + self.taxable_amount = invline.total_amount + + for subtax in self.subtotals: + subtax.calculate(invline) + self.tax_amount += subtax.tax_amount + + +class WithholdingTaxTotalOmit(WithholdingTaxTotal): + def __init__(self): + super().__init__([]) + + def calculate(self, invline): + pass + + @dataclass class Price: amount: Amount @@ -348,6 +404,7 @@ def __post_init__(self): self.amount *= self.quantity + @dataclass class PaymentMean: DEBIT = '01' @@ -365,8 +422,24 @@ def __init__(self, id: str, code: str, due_at: datetime, payment_id: str): @dataclass class PrePaidPayment: - #DIAN 1.7.-2020: FBD03 - paid_amount: Amount = Amount(0.0) + # DIAN 1.7.-2020: FBD03 + paid_amount: Amount = field(default_factory=lambda: Amount(0.0)) + + +@dataclass +class BillingResponse: + id: str + code: str + description: str + + +class SupportDocumentCreditNoteResponse(BillingResponse): + """ + ReferenceID: Identifica la sección del Documento + Soporte original a la cual se aplica la corrección. + ResponseCode: Código de descripción de la corrección. + Description: Descripción de la naturaleza de la corrección. + """ @dataclass @@ -375,6 +448,7 @@ class BillingReference: uuid: str date: date + class CreditNoteDocumentReference(BillingReference): """ ident: Prefijo + Numero de la factura relacionada @@ -390,6 +464,7 @@ class DebitNoteDocumentReference(BillingReference): date: fecha de emision de la factura relacionada """ + class InvoiceDocumentReference(BillingReference): """ ident: Prefijo + Numero de la nota credito relacionada @@ -397,6 +472,7 @@ class InvoiceDocumentReference(BillingReference): date: fecha de emision de la nota credito relacionada """ + @dataclass class AllowanceChargeReason: code: str @@ -409,22 +485,26 @@ def __post_init__(self): @dataclass class AllowanceCharge: - #DIAN 1.7.-2020: FAQ03 + # DIAN 1.7.-2020: FAQ03 charge_indicator: bool = True - amount: Amount = Amount(0.0) + amount: Amount = field(default_factory=lambda: Amount(0.0)) reason: AllowanceChargeReason = None - #Valor Base para calcular el descuento o el cargo - base_amount: typing.Optional[Amount] = Amount(0.0) - + # Valor Base para calcular el descuento o el cargo + base_amount: typing.Optional[Amount] = field( + default_factory=lambda: Amount(0.0)) + # Porcentaje: Porcentaje que aplicar. - multiplier_factor_numeric: Amount = Amount(1.0) - + multiplier_factor_numeric: Amount = field( + default_factory=lambda: Amount(1.0)) + def isCharge(self): - return self.charge_indicator == True + charge_indicator = self.charge_indicator is True + return charge_indicator def isDiscount(self): - return self.charge_indicator == False + charge_indicator = self.charge_indicator is False + return charge_indicator def asCharge(self): self.charge_indicator = True @@ -438,11 +518,13 @@ def hasReason(self): 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 @@ -455,8 +537,9 @@ class InvoiceLine: # la factura y el percent es unico por type_code # de subtotal tax: typing.Optional[TaxTotal] - - allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list) + withholding: typing.Optional[WithholdingTaxTotal] + allowance_charge: typing.List[AllowanceCharge] = dataclasses.field( + default_factory=list) def add_allowance_charge(self, charge): if not isinstance(charge, AllowanceCharge): @@ -467,7 +550,7 @@ def add_allowance_charge(self, charge): @property def total_amount_without_charge(self): return (self.quantity * self.price.amount) - + @property def total_amount(self): charge = AmountCollection(self.allowance_charge)\ @@ -499,8 +582,17 @@ def tax_amount(self): def taxable_amount(self): return self.tax.taxable_amount + @property + def withholding_amount(self): + return self.withholding.tax_amount + + @property + def withholding_taxable_amount(self): + return self.withholding.taxable_amount + def calculate(self): self.tax.calculate(self) + self.withholding.calculate(self) def __post_init__(self): if not isinstance(self.quantity, Quantity): @@ -509,18 +601,22 @@ def __post_init__(self): if self.tax is None: self.tax = TaxTotalOmit() + if self.withholding is None: + self.withholding = WithholdingTaxTotalOmit() + + @dataclass class LegalMonetaryTotal: - line_extension_amount: Amount = Amount(0.0) - tax_exclusive_amount: Amount = Amount(0.0) - tax_inclusive_amount: Amount = Amount(0.0) - charge_total_amount: Amount = Amount(0.0) - allowance_total_amount: Amount = Amount(0.0) - payable_amount: Amount = Amount(0.0) - prepaid_amount: Amount = Amount(0.0) + line_extension_amount: Amount = field(default_factory=lambda: Amount(0.0)) + tax_exclusive_amount: Amount = field(default_factory=lambda: Amount(0.0)) + tax_inclusive_amount: Amount = field(default_factory=lambda: Amount(0.0)) + charge_total_amount: Amount = field(default_factory=lambda: Amount(0.0)) + allowance_total_amount: Amount = field(default_factory=lambda: Amount(0.0)) + payable_amount: Amount = field(default_factory=lambda: Amount(0.0)) + prepaid_amount: Amount = field(default_factory=lambda: Amount(0.0)) def calculate(self): - #DIAN 1.7.-2020: FAU14 + # DIAN 1.7.-2020: FAU14 self.payable_amount = \ self.tax_inclusive_amount \ + self.allowance_total_amount \ @@ -528,22 +624,29 @@ def calculate(self): - self.prepaid_amount - class NationalSalesInvoiceDocumentType(str): def __str__(self): # 6.1.3 return '01' + class CreditNoteDocumentType(str): def __str__(self): # 6.1.3 return '91' + class DebitNoteDocumentType(str): def __str__(self): # 6.1.3 return '92' + +class CreditNoteSupportDocumentType(str): + def __str__(self): + return '95' + + class Invoice: def __init__(self, type_code: str): if str(type_code) not in codelist.TipoDocumento: @@ -563,6 +666,7 @@ def __init__(self, type_code: str): self.invoice_allowance_charge = [] self.invoice_prepaid_payment = [] self.invoice_billing_reference = None + self.invoice_discrepancy_response = None self.invoice_type_code = str(type_code) self.invoice_ident_prefix = None @@ -588,7 +692,8 @@ def _set_ident_prefix_automatic(self): if len(prefix) <= 4: self.invoice_ident_prefix = prefix else: - raise ValueError('ident prefix failed to get, expected 0 to 4 chars') + raise ValueError( + 'ident prefix failed to get, expected 0 to 4 chars') def set_ident(self, ident: str): """ @@ -619,7 +724,7 @@ def set_payment_mean(self, payment_mean: PaymentMean): def _get_codelist_tipo_operacion(self): return codelist.TipoOperacionF - + def set_operation_type(self, operation): if operation not in self._get_codelist_tipo_operacion(): raise ValueError("operation not found") @@ -638,6 +743,9 @@ def add_prepaid_payment(self, paid: PrePaidPayment): def set_billing_reference(self, billing_reference: BillingReference): self.invoice_billing_reference = billing_reference + def set_discrepancy_response(self, billing_response: BillingResponse): + self.invoice_discrepancy_response = billing_response + def accept(self, visitor): visitor.visit_payment_mean(self.invoice_payment_mean) visitor.visit_customer(self.invoice_customer) @@ -649,29 +757,34 @@ def accept(self, visitor): def _calculate_legal_monetary_total(self): for invline in self.invoice_lines: - self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount - self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount - #DIAN 1.7.-2020: FAU6 - self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount - - #DIAN 1.7.-2020: FAU08 - self.invoice_legal_monetary_total.allowance_total_amount = AmountCollection(self.invoice_allowance_charge)\ + self.invoice_legal_monetary_total.line_extension_amount +=\ + invline.total_amount + self.invoice_legal_monetary_total.tax_exclusive_amount +=\ + invline.total_tax_exclusive_amount + # DIAN 1.7.-2020: FAU6 + self.invoice_legal_monetary_total.tax_inclusive_amount +=\ + invline.total_tax_inclusive_amount + + # DIAN 1.7.-2020: FAU08 + self.invoice_legal_monetary_total.allowance_total_amount =\ + AmountCollection(self.invoice_allowance_charge)\ .filter(lambda charge: charge.isDiscount())\ .map(lambda charge: charge.amount)\ .sum() - #DIAN 1.7.-2020: FAU10 - self.invoice_legal_monetary_total.charge_total_amount = AmountCollection(self.invoice_allowance_charge)\ + # DIAN 1.7.-2020: FAU10 + self.invoice_legal_monetary_total.charge_total_amount =\ + AmountCollection(self.invoice_allowance_charge)\ .filter(lambda charge: charge.isCharge())\ .map(lambda charge: charge.amount)\ .sum() - #DIAN 1.7.-2020: FAU12 - self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(self.invoice_prepaid_payment)\ - .map(lambda paid: paid.paid_amount)\ - .sum() + # DIAN 1.7.-2020: FAU12 + self.invoice_legal_monetary_total.prepaid_amount = AmountCollection( + self.invoice_prepaid_payment).map( + lambda paid: paid.paid_amount).sum() - #DIAN 1.7.-2020: FAU14 + # DIAN 1.7.-2020: FAU14 self.invoice_legal_monetary_total.calculate() def _refresh_charges_base_amount(self): @@ -679,18 +792,21 @@ def _refresh_charges_base_amount(self): for invline in self.invoice_lines: if invline.allowance_charge: # TODO actualmente solo uno de los cargos es permitido - raise ValueError('allowance charge in invoice exclude invoice line') - + raise ValueError( + 'allowance charge in invoice exclude invoice line') + # cargos a nivel de factura for charge in self.invoice_allowance_charge: - charge.set_base_amount(self.invoice_legal_monetary_total.line_extension_amount) - + charge.set_base_amount( + self.invoice_legal_monetary_total.line_extension_amount) + def calculate(self): for invline in self.invoice_lines: invline.calculate() self._calculate_legal_monetary_total() self._refresh_charges_base_amount() + class NationalSalesInvoice(Invoice): def __init__(self): super().__init__(NationalSalesInvoiceDocumentType()) @@ -706,7 +822,7 @@ def __init__(self, invoice_document_reference: BillingReference): def _get_codelist_tipo_operacion(self): return codelist.TipoOperacionNC - + def _check_ident_prefix(self, prefix): if len(prefix) != 6: raise ValueError('prefix must be 6 length') @@ -735,3 +851,30 @@ def _set_ident_prefix_automatic(self): if not self.invoice_ident_prefix: self.invoice_ident_prefix = self.invoice_ident[0:6] + +class SupportDocument(Invoice): + pass + + +class SupportDocumentCreditNote(SupportDocument): + def __init__( + self, invoice_document_reference: BillingReference, + invoice_discrepancy_response: BillingResponse): + super().__init__(CreditNoteSupportDocumentType()) + + if not isinstance(invoice_document_reference, BillingReference): + raise TypeError('invoice_document_reference invalid type') + self.invoice_billing_reference = invoice_document_reference + self.invoice_discrepancy_response = invoice_discrepancy_response + + def _get_codelist_tipo_operacion(self): + return codelist.TipoOperacionNCDS + + def _check_ident_prefix(self, prefix): + if len(prefix) != 6: + raise ValueError('prefix must be 6 length') + + def _set_ident_prefix_automatic(self): + if not self.invoice_ident_prefix: + self.invoice_ident_prefix = self.invoice_ident[0:6] + pass diff --git a/facho/fe/form_xml/__init__.py b/facho/fe/form_xml/__init__.py index b7530fa..8eb322f 100644 --- a/facho/fe/form_xml/__init__.py +++ b/facho/fe/form_xml/__init__.py @@ -3,3 +3,5 @@ from .debit_note import * from .utils import * from .attached_document import * +from .support_document import * +from .support_document_credit_note import * diff --git a/facho/fe/form_xml/attached_document.py b/facho/fe/form_xml/attached_document.py index ae3df9b..18b368e 100644 --- a/facho/fe/form_xml/attached_document.py +++ b/facho/fe/form_xml/attached_document.py @@ -2,13 +2,14 @@ __all__ = ['AttachedDocument'] + class AttachedDocument(): def __init__(self, id): - schema = 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2' + schema =\ + 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2' self.fexml = fe.FeXML('AttachedDocument', schema) self.fexml.set_element('./cbc:ID', id) def toFachoXML(self): return self.fexml - diff --git a/facho/fe/form_xml/credit_note.py b/facho/fe/form_xml/credit_note.py index be887d4..c6ad5bb 100644 --- a/facho/fe/form_xml/credit_note.py +++ b/facho/fe/form_xml/credit_note.py @@ -1,9 +1,10 @@ -from .. import fe -from ..form import * +# from .. import fe +# from ..form import * from .invoice import DIANInvoiceXML __all__ = ['DIANCreditNoteXML'] + class DIANCreditNoteXML(DIANInvoiceXML): """ DianInvoiceXML mapea objeto form.Invoice a XML segun @@ -18,6 +19,3 @@ def tag_document(fexml): def tag_document_concilied(fexml): return 'Credited' - - def post_attach_invoice(fexml, invoice): - fexml.set_element('./cbc:ProfileID', 'DIAN 2.1: Nota Crédito de Factura Electrónica de Venta') diff --git a/facho/fe/form_xml/debit_note.py b/facho/fe/form_xml/debit_note.py index 4fe5838..c589124 100644 --- a/facho/fe/form_xml/debit_note.py +++ b/facho/fe/form_xml/debit_note.py @@ -1,9 +1,10 @@ -from .. import fe -from ..form import * +# from .. import fe +# from ..form import * from .invoice import DIANInvoiceXML __all__ = ['DIANDebitNoteXML'] + class DIANDebitNoteXML(DIANInvoiceXML): """ DianInvoiceXML mapea objeto form.Invoice a XML segun @@ -13,28 +14,30 @@ class DIANDebitNoteXML(DIANInvoiceXML): def __init__(self, invoice): super().__init__(invoice, 'DebitNote') - def post_attach_invoice(fexml, invoice): - fexml.set_element('./cbc:ProfileID', 'DIAN 2.1 Nota Débito de Factura Electrónica de Venta') - def tag_document(fexml): return 'DebitNote' def tag_document_concilied(fexml): return 'Debited' - #DIAN 1.7.-2020: DAU03 + # DIAN 1.7.-2020: DAU03 def set_legal_monetary(fexml, invoice): - fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:LineExtensionAmount', - invoice.invoice_legal_monetary_total.line_extension_amount) + fexml.set_element_amount( + './cac:RequestedMonetaryTotal/cbc:LineExtensionAmount', + invoice.invoice_legal_monetary_total.line_extension_amount) - fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:TaxExclusiveAmount', - invoice.invoice_legal_monetary_total.tax_exclusive_amount) + fexml.set_element_amount( + './cac:RequestedMonetaryTotal/cbc:TaxExclusiveAmount', + invoice.invoice_legal_monetary_total.tax_exclusive_amount) - fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:TaxInclusiveAmount', - invoice.invoice_legal_monetary_total.tax_inclusive_amount) + fexml.set_element_amount( + './cac:RequestedMonetaryTotal/cbc:TaxInclusiveAmount', + invoice.invoice_legal_monetary_total.tax_inclusive_amount) - fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:ChargeTotalAmount', - invoice.invoice_legal_monetary_total.charge_total_amount) + fexml.set_element_amount( + './cac:RequestedMonetaryTotal/cbc:ChargeTotalAmount', + invoice.invoice_legal_monetary_total.charge_total_amount) - fexml.set_element_amount('./cac:RequestedMonetaryTotal/cbc:PayableAmount', - invoice.invoice_legal_monetary_total.payable_amount) + fexml.set_element_amount( + './cac:RequestedMonetaryTotal/cbc:PayableAmount', + invoice.invoice_legal_monetary_total.payable_amount) diff --git a/facho/fe/form_xml/invoice.py b/facho/fe/form_xml/invoice.py index 93cb5ce..c009d43 100644 --- a/facho/fe/form_xml/invoice.py +++ b/facho/fe/form_xml/invoice.py @@ -1,5 +1,6 @@ from .. import fe from ..form import * +from collections import defaultdict __all__ = ['DIANInvoiceXML'] @@ -21,7 +22,6 @@ def __init__(self, invoice, tag_document = 'Invoice'): ublextension = self.fragment('./ext:UBLExtensions/ext:UBLExtension', append=True) extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') self.attach_invoice(invoice) - self.post_attach_invoice(invoice) def set_supplier(fexml, invoice): fexml.placeholder_for('./cac:AccountingSupplierParty') @@ -148,7 +148,6 @@ def set_supplier(fexml, invoice): fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', invoice.invoice_customer.tax_scheme.code) - #DIAN 1.7.-2020: CAJ41 fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', invoice.invoice_customer.tax_scheme.name) @@ -416,11 +415,13 @@ def set_billing_reference(fexml, invoice): return fexml._set_debit_note_document_reference(reference) if isinstance(reference, CreditNoteDocumentReference): return fexml._set_credit_note_document_reference(reference) + if isinstance(reference, InvoiceDocumentReference): return fexml._set_invoice_document_reference(reference) def set_invoice_totals(fexml, invoice): tax_amount_for = defaultdict(lambda: defaultdict(lambda: Amount(0.0))) + withholding_amount_for = defaultdict(lambda: defaultdict(lambda: Amount(0.0))) percent_for = defaultdict(lambda: None) #requeridos para CUFE @@ -433,29 +434,42 @@ def set_invoice_totals(fexml, invoice): #tax_amount_for['03']['taxable_amount'] += 0.0 total_tax_amount = Amount(0.0) + total_withholding_amount = Amount(0.0) for invoice_line in invoice.invoice_lines: for subtotal in invoice_line.tax.subtotals: if subtotal.scheme is not None: tax_amount_for[subtotal.scheme.code]['tax_amount'] += subtotal.tax_amount tax_amount_for[subtotal.scheme.code]['taxable_amount'] += invoice_line.taxable_amount + tax_amount_for[subtotal.scheme.code]['name'] = subtotal.scheme.name # MACHETE ojo InvoiceLine.tax pasar a Invoice percent_for[subtotal.scheme.code] = subtotal.percent total_tax_amount += subtotal.tax_amount + for subtotal_withholding in invoice_line.withholding.subtotals: + if subtotal_withholding.scheme is not None: + withholding_amount_for[subtotal_withholding.scheme.code]['tax_amount'] += subtotal_withholding.tax_amount + withholding_amount_for[subtotal_withholding.scheme.code]['taxable_amount'] += invoice_line.withholding_taxable_amount + + # MACHETE ojo InvoiceLine.tax pasar a Invoice + + percent_for[subtotal_withholding.scheme.code] = subtotal_withholding.percent + + total_withholding_amount += subtotal_withholding.tax_amount + if total_tax_amount != Amount(0.0): fexml.placeholder_for('./cac:TaxTotal') fexml.set_element_amount('./cac:TaxTotal/cbc:TaxAmount', total_tax_amount) - for index, item in enumerate(tax_amount_for.items()): cod_impuesto, amount_of = item + next_append = index > 0 #DIAN 1.7.-2020: FAS01 - line = fexml.fragment('./cac:TaxTotal', append=True) + line = fexml.fragment('./cac:TaxTotal', append=next_append) #DIAN 1.7.-2020: FAU06 tax_amount = amount_of['tax_amount'] fexml.set_element_amount_for(line, @@ -485,8 +499,45 @@ def set_invoice_totals(fexml, invoice): line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', cod_impuesto) line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', - 'IVA') - + amount_of['name']) + + for index, item in enumerate(withholding_amount_for.items()): + cod_impuesto, amount_of = item + next_append = index > 0 + + #DIAN 1.7.-2020: FAS01 + line = fexml.fragment('./cac:WithholdingTaxTotal', append=next_append) + #DIAN 1.7.-2020: FAU06 + tax_amount = amount_of['tax_amount'] + fexml.set_element_amount_for(line, + '/cac:WithholdingTaxTotal/cbc:TaxAmount', + tax_amount) + + #DIAN 1.7.-2020: FAS05 + fexml.set_element_amount_for(line, + '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', + amount_of['taxable_amount']) + + #DIAN 1.7.-2020: FAU06 + fexml.set_element_amount_for(line, + '/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', + amount_of['tax_amount']) + + #DIAN 1.7.-2020: FAS07 + if percent_for[cod_impuesto]: + line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:Percent', + percent_for[cod_impuesto]) + + + if percent_for[cod_impuesto]: + line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', + percent_for[cod_impuesto]) + + line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', + cod_impuesto) + line.set_element('/cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', + 'ReteRenta') + # abstract method def tag_document(fexml): return 'Invoice' @@ -514,7 +565,29 @@ def set_invoice_line_tax(fexml, line, invoice_line): #DIAN 1.7.-2020: FAX15 line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code) line.set_element('./cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', subtotal.scheme.name) - + + + def set_invoice_line_withholding(fexml, line, invoice_line): + fexml.set_element_amount_for(line, + './cac:WithholdingTaxTotal/cbc:TaxAmount', + invoice_line.withholding_amount) + #DIAN 1.7.-2020: FAX05 + fexml.set_element_amount_for(line, + './cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', + invoice_line.withholding_taxable_amount) + + for subtotal in invoice_line.withholding.subtotals: + line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', subtotal.tax_amount, currencyID='COP') + + if subtotal.percent is not None: + line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', '%0.2f' % round(subtotal.percent, 2)) + + if subtotal.scheme is not None: + #DIAN 1.7.-2020: FAX15 + line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', subtotal.scheme.code) + line.set_element('./cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', subtotal.scheme.name) + + def set_invoice_lines(fexml, invoice): next_append = False for index, invoice_line in enumerate(invoice.invoice_lines): @@ -530,6 +603,9 @@ def set_invoice_lines(fexml, invoice): if not isinstance(invoice_line.tax, TaxTotalOmit): fexml.set_invoice_line_tax(line, invoice_line) + if not isinstance(invoice_line.withholding, WithholdingTaxTotalOmit): + fexml.set_invoice_line_withholding(line, invoice_line) + line.set_element('./cac:Item/cbc:Description', invoice_line.item.description) line.set_element('./cac:Item/cac:StandardItemIdentification/cbc:ID', @@ -547,28 +623,32 @@ def set_invoice_lines(fexml, invoice): 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) + 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 post_attach_invoice(fexml, invoice): - #DIAN 1.8.-2021: FAD03 - fexml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') + 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 @@ -581,7 +661,6 @@ def attach_invoice(fexml, invoice): fexml.placeholder_for('./cbc:ProfileExecutionID') fexml.set_element('./cbc:ID', invoice.invoice_ident) fexml.placeholder_for('./cbc:UUID') - fexml.set_element('./cbc:DocumentCurrencyCode', 'COP') fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d')) #DIAN 1.7.-2020: FAD10 fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00')) @@ -590,24 +669,25 @@ def attach_invoice(fexml, invoice): 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)) - 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')) - + 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_legal_monetary(invoice) + fexml.set_payment_mean(invoice) fexml.set_invoice_totals(invoice) + fexml.set_legal_monetary(invoice) fexml.set_invoice_lines(invoice) - fexml.set_payment_mean(invoice) fexml.set_allowance_charge(invoice) - fexml.set_billing_reference(invoice) - return fexml def customize(fexml, invoice): diff --git a/facho/fe/form_xml/support_document.py b/facho/fe/form_xml/support_document.py new file mode 100644 index 0000000..c425e22 --- /dev/null +++ b/facho/fe/form_xml/support_document.py @@ -0,0 +1,647 @@ +from .. import fe +from ..form import ( + Amount, DebitNoteDocumentReference, CreditNoteDocumentReference, + InvoiceDocumentReference, TaxTotalOmit, WithholdingTaxTotalOmit +) + +from collections import defaultdict +from datetime import datetime +# from .attached_document import * + +__all__ = ['DIANSupportDocumentXML'] + + +class DIANSupportDocumentXML(fe.FeXML): + """ + DianSupportDocumentXML mapea objeto form.Invoice a XML segun + lo indicado para él Documento soporte en adquisiciones efectuadas con sujetos no obligados a expedir factura de venta o documento equivalente. + """ + + def __init__(self, invoice, tag_document='Invoice'): + super().__init__(tag_document, 'http://www.dian.gov.co/contratos/facturaelectronica/v1') + + # DIAN 1.1.-2021: DSAB03 + # DIAN 1.1.-2021: NSAB03 + self.placeholder_for( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceControl') + + # DIAN 1.1.-2021: DSAB13 + # DIAN 1.1.-2021: NSAB13 + self.placeholder_for( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:InvoiceSource') + + # DIAN 1.1.-2021: DSAB18 + # DIAN 1.1.-2021: NSAB18 + self.placeholder_for( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareProvider') + + # DIAN 1.1.-2021: DSAB27 + # DIAN 1.1.-2021: NSAB27 + self.placeholder_for( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:SoftwareSecurityCode') + + # DIAN 1.1.-2021: DSAB30 DSAB31 + # DIAN 1.1.-2021: NSAB30 NSAB31 + self.placeholder_for( + './ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') + + # ZE02 se requiere existencia para firmar + # DIAN 1.1.-2021: DSAA02 DSAB01 + # DIAN 1.1.-2021: NSAA02 NSAB01 + ublextension = self.fragment( + './ext:UBLExtensions/ext:UBLExtension', append=True) + # DIAN 1.1.-2021: DSAB02 + # DIAN 1.1.-2021: NSAB02 + extcontent = ublextension.find_or_create_element( + '/ext:UBLExtension/ext:ExtensionContent') + self.attach_invoice(invoice) + + def set_supplier(fexml, invoice): + # DIAN 1.1.-2021: DSAJ01 + # DIAN 1.1.-2021: NSAB01 + fexml.placeholder_for('./cac:AccountingSupplierParty') + + # DIAN 1.1.-2021: DSAJ02 + # DIAN 1.1.-2021: NSAJ02 + fexml.set_element( + './cac:AccountingSupplierParty/cbc:AdditionalAccountID', + invoice.invoice_supplier.organization_code) + + # DIAN 1.1.-2021: DSAJ07 DSAJ08 + # DIAN 1.1.-2021: NSAJ07 NSAJ08 + fexml.placeholder_for( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address') + + # DIAN 1.1.-2021: DSAJ09 + # DIAN 1.1.-2021: NSAJ09 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:ID', + invoice.invoice_supplier.address.city.code) + + # DIAN 1.1.-2021: DSAJ10 + # DIAN 1.1.-2021: NSAJ10 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CityName', + invoice.invoice_supplier.address.city.name) + + # DIAN 1.1.-2021: DSAJ73 + # DIAN 1.1.-2021: NSAJ73 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:PostalZone', + invoice.invoice_supplier.address.postalzone.code) + + # DIAN 1.1.-2021: DSAJ11 + # DIAN 1.1.-2021: NSAJ11 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentity', + invoice.invoice_supplier.address.countrysubentity.name) + + # DIAN 1.1.-2021: DSAJ12 + # DIAN 1.1.-2021: NSAJ12 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cbc:CountrySubentityCode', + invoice.invoice_supplier.address.countrysubentity.code) + # DIAN 1.1.-2021: NSAJ13 NSAJ14 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:AddressLine/cbc:Line', + invoice.invoice_supplier.address.street) + + # DIAN 1.1.-2021: DSAJ15 DSAJ16 + # DIAN 1.1.-2021: NSAJ15 NSAJ16 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:IdentificationCode', + invoice.invoice_supplier.address.country.code) + + # DIAN 1.1.-2021: DSAJ17 + # DIAN 1.1.-2021: NSAJ17 + fexml.set_element('./cac:AccountingSupplierParty/cac:Party/cac:PhysicalLocation/cac:Address/cac:Country/cbc:Name', + invoice.invoice_supplier.address.country.name, + # DIAN 1.1.-2021: DSAJ18 + # # DIAN 1.1.-2021: NSAJ18 + languageID='es') + + supplier_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() + supplier_company_id_attrs.update( + { + 'schemeID': invoice.invoice_supplier.ident.dv, + 'schemeName': invoice.invoice_supplier.ident.type_fiscal}) + + # DIAN 1.1.-2021: DSAJ19 + # DIAN 1.1.-2021: NSAJ19 + fexml.placeholder_for( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme') + + # DIAN 1.1.-2021: DSAJ20 + # DIAN 1.1.-2021: NSAJ20 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName', + invoice.invoice_supplier.legal_name) + + # DIAN 1.1.-2021: DSAJ21 + # DIAN 1.1.-2021: NSAJ21 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', + invoice.invoice_supplier.ident, + # DIAN 1.1.-2021: DSAJ22 DSAJ23 DSAJ24 DSAJ25 + # DIAN 1.1.-2021: NSAJ22 NSAJ23 NSAJ24 NSAJ25 + **supplier_company_id_attrs) + + # DIAN 1.1.-2021: DSAJ26 + # DIAN 1.1.-2021: NSAJ26 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode', + invoice.invoice_supplier.responsability_code, + listName=invoice.invoice_supplier.responsability_regime_code) + + # DIAN 1.1.-2021: DSAJ39 + # DIAN 1.1.-2021: NSAJ39 + fexml.placeholder_for( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme') + + # DIAN 1.1.-2021: DSAJ40 + # DIAN 1.1.-2021: NSAJ40 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', + invoice.invoice_customer.tax_scheme.code) + + # DIAN 1.1.-2021: DSAJ41 + # DIAN 1.1.-2021: NSAJ41 + fexml.set_element( + './cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', + invoice.invoice_customer.tax_scheme.name) + + def set_customer(fexml, invoice): + # DIAN 1.1.-2021: DSAK01 + # DIAN 1.1.-2021: NSAK01 + fexml.placeholder_for('./cac:AccountingCustomerParty') + + # DIAN 1.1.-2021: DSAK02 + # DIAN 1.1.-2021: NSAK02 + fexml.set_element( + './cac:AccountingCustomerParty/cbc:AdditionalAccountID', + invoice.invoice_customer.organization_code) + + # DIAN 1.1.-2021: DSAK03 + # DIAN 1.1.-2021: NSAK03 + fexml.placeholder_for('./cac:AccountingCustomerParty/cac:Party') + + # DIAN 1.1.-2021: DSAK19 + # DIAN 1.1.-2021: NSAK19 + fexml.placeholder_for( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme') + + # DIAN 1.1.-2021: DSAK20 + # DIAN 1.1.-2021: NSAK20 + fexml.set_element( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName', + invoice.invoice_customer.legal_name) + + customer_company_id_attrs = fe.SCHEME_AGENCY_ATTRS.copy() + customer_company_id_attrs.update( + { + 'schemeID': invoice.invoice_customer.ident.dv, + 'schemeName': invoice.invoice_customer.ident.type_fiscal}) + + # DIAN 1.1.-2021: DSAK21 + # DIAN 1.1.-2021: NSAK21 + fexml.set_element( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', + invoice.invoice_customer.ident, + # DIAN 1.1.-2021: DSAK22 DSAK23 DSAK24 DSAK25 + # DIAN 1.1.-2021: NSAK22 NSAK23 NSAK24 NSAK25 + **customer_company_id_attrs) + + # DIAN 1.1.-2021: DSAK26 + # DIAN 1.1.-2021: NSAK26 + fexml.set_element( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:TaxLevelCode', + invoice.invoice_customer.responsability_code) + + # DIAN 1.1.-2021: DSAK39 + # DIAN 1.1.-2021: NSAK39 + fexml.placeholder_for( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme') + + # DIAN 1.1.-2021: DSAK40 + # DIAN 1.1.-2021: NSAK40 + fexml.set_element( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:ID', + invoice.invoice_customer.tax_scheme.code) + + # DIAN 1.1.-2021: DSAK41 + # DIAN 1.1.-2021: NSAK41 + fexml.set_element( + './cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cac:TaxScheme/cbc:Name', + invoice.invoice_customer.tax_scheme.name) + + def set_payment_mean(fexml, invoice): + payment_mean = invoice.invoice_payment_mean + + # DIAN 1.1.-2021: DSAN01 DSAN02 + # DIAN 1.1.-2021: NSAN02 NSAN02 + fexml.set_element('./cac:PaymentMeans/cbc:ID', payment_mean.id) + + # DIAN 1.1.-2021: DSAN03 + # DIAN 1.1.-2021: NSAN03 + fexml.set_element( + './cac:PaymentMeans/cbc:PaymentMeansCode', + payment_mean.code) + + # DIAN 1.1.-2021: DSAN04 + # DIAN 1.1.-2021: NSAN04 + fexml.set_element( + './cac:PaymentMeans/cbc:PaymentDueDate', + payment_mean.due_at.strftime('%Y-%m-%d')) + + # DIAN 1.1.-2021: DSAN05 + # DIAN 1.1.-2021: NSAN05 + fexml.set_element( + './cac:PaymentMeans/cbc:PaymentID', + payment_mean.payment_id) + + def set_element_amount_for(fexml, xml, xpath, amount): + if not isinstance(amount, Amount): + raise TypeError("amount not is Amount") + + xml.set_element(xpath, amount, currencyID=amount.currency.code) + + def set_element_amount(fexml, xpath, amount): + if not isinstance(amount, Amount): + raise TypeError("amount not is Amount") + + fexml.set_element(xpath, amount, currencyID=amount.currency.code) + + def set_legal_monetary(fexml, invoice): + # DIAN 1.1.-2021: DSAU01 DSAU02 DSAU03 + # DIAN 1.1.-2021: NSAU01 NSAU02 NSAU03 + fexml.set_element_amount( + './cac:LegalMonetaryTotal/cbc:LineExtensionAmount', + invoice.invoice_legal_monetary_total.line_extension_amount) + + # DIAN 1.1.-2021: DSAU04 DSAU05 + # DIAN 1.1.-2021: NSAU04 NSAU05 + fexml.set_element_amount( + './cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', + invoice.invoice_legal_monetary_total.tax_exclusive_amount) + + # DIAN 1.1.-2021: DSAU06 DSAU07 + # DIAN 1.1.-2021: NSAU06 DSAU07 + fexml.set_element_amount( + './cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', + invoice.invoice_legal_monetary_total.tax_inclusive_amount) + + # DIAN 1.1.-2021: DSAU10 DSAU11 + # DIAN 1.1.-2021: NSAU10 DSAU11 + fexml.set_element_amount( + './cac:LegalMonetaryTotal/cbc:ChargeTotalAmount', + invoice.invoice_legal_monetary_total.charge_total_amount) + + # DIAN 1.1.-2021: DSAU14 DSAU15 + # DIAN 1.1.-2021: NSAU14 DSAU15 + fexml.set_element_amount( + './cac:LegalMonetaryTotal/cbc:PayableAmount', + invoice.invoice_legal_monetary_total.payable_amount) + + def _set_invoice_document_reference(fexml, reference): + fexml._do_set_billing_reference( + reference, 'cac:InvoiceDocumentReference') + + def _set_credit_note_document_reference(fexml, reference): + fexml._do_set_billing_reference( + reference, 'cac:CreditNoteDocumentReference') + + def _set_debit_note_document_reference(fexml, reference): + fexml._do_set_billing_reference( + reference, 'cac:DebitNoteDocumentReference') + + def _do_set_billing_reference(fexml, reference, tag_document): + + if tag_document == 'Invoice': + schemeName = 'CUFE-SHA384' + else: + schemeName = 'CUDS-SHA384' + + fexml.set_element('./cac:BillingReference/%s/cbc:ID' % (tag_document), + reference.ident) + fexml.set_element( + './cac:BillingReference/cac:InvoiceDocumentReference/cbc:UUID', + reference.uuid, + schemeName=schemeName) + fexml.set_element( + './cac:BillingReference/cac:InvoiceDocumentReference/cbc:IssueDate', + reference.date.strftime("%Y-%m-%d")) + + def set_billing_reference(fexml, invoice): + reference = invoice.invoice_billing_reference + if reference is None: + return + + if isinstance(reference, DebitNoteDocumentReference): + return fexml._set_debit_note_document_reference(reference) + if isinstance(reference, CreditNoteDocumentReference): + return fexml._set_credit_note_document_reference(reference) + + if isinstance(reference, InvoiceDocumentReference): + return fexml._set_invoice_document_reference(reference) + + def set_discrepancy_response(fexml, invoice): + reference = invoice.invoice_discrepancy_response + if reference is None: + return + if isinstance(reference, DebitNoteDocumentReference): + return fexml._set_debit_note_document_reference(reference) + if isinstance(reference, CreditNoteDocumentReference): + return fexml._set_credit_note_document_reference(reference) + + if isinstance(reference, InvoiceDocumentReference): + return fexml._set_invoice_document_reference(reference) + + fexml.set_element('./cac:DiscrepancyResponse/cbc:ReferenceID', + reference.id) + fexml.set_element('./cac:DiscrepancyResponse/cbc:ResponseCode', + reference.code) + fexml.set_element('./cac:DiscrepancyResponse/cbc:Description', + reference.description) + + def set_invoice_totals(fexml, invoice): + tax_amount_for = defaultdict(lambda: defaultdict(lambda: Amount(0.0))) + percent_for = defaultdict(lambda: None) + + total_tax_amount = Amount(0.0) + + for invoice_line in invoice.invoice_lines: + for subtotal in invoice_line.tax.subtotals: + if subtotal.scheme is not None: + tax_amount_for[ + subtotal.scheme.code][ + 'tax_amount'] += subtotal.tax_amount + tax_amount_for[subtotal.scheme.code][ + 'taxable_amount'] += invoice_line.taxable_amount + + # MACHETE ojo InvoiceLine.tax pasar a Invoice + percent_for[subtotal.scheme.code] = subtotal.percent + + total_tax_amount += subtotal.tax_amount + + if total_tax_amount != Amount(0.0): + fexml.placeholder_for('./cac:TaxTotal') + fexml.set_element_amount('./cac:TaxTotal/cbc:TaxAmount', + total_tax_amount) + + for index, item in enumerate(tax_amount_for.items()): + cod_impuesto, amount_of = item + next_append = index > 0 + + # DIAN 1.7.-2020: FAS01 + line = fexml.fragment('./cac:TaxTotal', append=next_append) + # DIAN 1.7.-2020: FAU06 + tax_amount = amount_of['tax_amount'] + fexml.set_element_amount_for(line, + '/cac:TaxTotal/cbc:TaxAmount', + tax_amount) + + # DIAN 1.7.-2020: FAS05 + fexml.set_element_amount_for( + line, + '/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', + amount_of['taxable_amount']) + + # DIAN 1.7.-2020: FAU06 + fexml.set_element_amount_for( + line, + '/cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', + amount_of['tax_amount']) + + # DIAN 1.7.-2020: FAS07 + if percent_for[cod_impuesto]: + line.set_element('/cac:TaxTotal/cac:TaxSubtotal/cbc:Percent', + percent_for[cod_impuesto]) + + if percent_for[cod_impuesto]: + line.set_element( + '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', + percent_for[cod_impuesto]) + + line.set_element( + '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', + cod_impuesto) + line.set_element( + '/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', 'IVA') + + # abstract method + + def tag_document(fexml): + return 'Invoice' + + # abstract method + def tag_document_concilied(fexml): + return 'Invoiced' + + def set_invoice_line_withholding(fexml, line, invoice_line): + fexml.set_element_amount_for(line, + './cac:WithholdingTaxTotal/cbc:TaxAmount', + invoice_line.withholding_amount) + # DIAN 1.7.-2020: FAX05 + fexml.set_element_amount_for( + line, + './cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', + invoice_line.withholding_taxable_amount) + + for subtotal in invoice_line.withholding.subtotals: + line.set_element( + './cac:WithholdingTaxTotal/cac:TaxSubtotal/cbc:TaxAmount', + subtotal.tax_amount, + currencyID='COP') + + if subtotal.percent is not None: + line.set_element( + './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', + '%0.2f' % + round( + subtotal.percent, + 2)) + + if subtotal.scheme is not None: + # DIAN 1.7.-2020: FAX15 + line.set_element( + './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', + subtotal.scheme.code) + line.set_element( + './cac:WithholdingTaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', + subtotal.scheme.name) + + def set_invoice_line_tax(fexml, line, invoice_line): + fexml.set_element_amount_for(line, + './cac:TaxTotal/cbc:TaxAmount', + invoice_line.tax_amount) + + # DIAN 1.7.-2020: FAX05 + fexml.set_element_amount_for( + line, + './cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', + invoice_line.taxable_amount) + for subtotal in invoice_line.tax.subtotals: + line.set_element( + './cac:TaxTotal/cac:TaxSubtotal/cbc:TaxAmount', + subtotal.tax_amount, + currencyID='COP') + + if subtotal.percent is not None: + line.set_element( + './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent', + '%0.2f' % + round( + subtotal.percent, + 2)) + + if subtotal.scheme is not None: + # DIAN 1.7.-2020: FAX15 + line.set_element( + './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:ID', + subtotal.scheme.code) + line.set_element( + './cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme/cbc:Name', + subtotal.scheme.name) + + def set_invoice_lines(fexml, invoice): + next_append = False + for index, invoice_line in enumerate(invoice.invoice_lines): + line = fexml.fragment( + './cac:%sLine' % + (fexml.tag_document()), + append=next_append) + next_append = True + + line.set_element('./cbc:ID', index + 1) + line.set_element( + './cbc:%sQuantity' % + (fexml.tag_document_concilied()), + invoice_line.quantity, + unitCode='NAR') + fexml.set_element_amount_for(line, + './cbc:LineExtensionAmount', + invoice_line.total_amount) + + period = line.fragment('./cac:InvoicePeriod') + period.set_element('./cbc:StartDate', + datetime.now().strftime('%Y-%m-%d')) + period.set_element( + './cbc:DescriptionCode', '1') + period.set_element('./cbc:Description', + 'Por operación') + + if not isinstance(invoice_line.tax, TaxTotalOmit): + fexml.set_invoice_line_tax(line, invoice_line) + + if not isinstance( + invoice_line.withholding, + WithholdingTaxTotalOmit): + fexml.set_invoice_line_withholding(line, invoice_line) + + line.set_element( + './cac:Item/cbc:Description', + invoice_line.item.description) + + line.set_element( + './cac:Item/cac:StandardItemIdentification/cbc:ID', + invoice_line.item.id, + schemeID=invoice_line.item.scheme_id, + schemeName=invoice_line.item.scheme_name, + schemeAgencyID=invoice_line.item.scheme_agency_id) + + line.set_element( + './cac:Price/cbc:PriceAmount', + invoice_line.price.amount, + currencyID=invoice_line.price.amount.currency.code) + # DIAN 1.7.-2020: FBB04 + line.set_element('./cac:Price/cbc:BaseQuantity', + 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""" + + 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:DocumentCurrencyCode', 'COP') + 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:LineCountNumeric', len(invoice.invoice_lines)) + 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.customize(invoice) + + fexml.set_supplier(invoice) + fexml.set_customer(invoice) + fexml.set_legal_monetary(invoice) + fexml.set_invoice_totals(invoice) + fexml.set_invoice_lines(invoice) + fexml.set_payment_mean(invoice) + fexml.set_allowance_charge(invoice) + fexml.set_discrepancy_response(invoice) + fexml.set_billing_reference(invoice) + + return fexml + + def customize(fexml, invoice): + """adiciona etiquetas a FEXML y retorna FEXML + en caso de fallar validacion retorna None""" diff --git a/facho/fe/form_xml/support_document_credit_note.py b/facho/fe/form_xml/support_document_credit_note.py new file mode 100644 index 0000000..0145e71 --- /dev/null +++ b/facho/fe/form_xml/support_document_credit_note.py @@ -0,0 +1,25 @@ +# from .. import fe +# from ..form import * +from .support_document import DIANSupportDocumentXML + +__all__ = ['DIANSupportDocumentCreditNoteXML'] + + +class DIANSupportDocumentCreditNoteXML(DIANSupportDocumentXML): + """ + DianInvoiceXML mapea objeto form.Invoice a XML segun + lo indicado para la facturacion electronica. + """ + + def __init__(self, invoice): + super( + DIANSupportDocumentCreditNoteXML, + self).__init__( + invoice, + 'CreditNote') + + def tag_document(fexml): + return 'CreditNote' + + def tag_document_concilied(fexml): + return 'Credited' diff --git a/facho/fe/form_xml/utils.py b/facho/fe/form_xml/utils.py index c05c3d4..2d084c7 100644 --- a/facho/fe/form_xml/utils.py +++ b/facho/fe/form_xml/utils.py @@ -2,18 +2,30 @@ __all__ = ['DIANWrite', 'DIANWriteSigned'] + def DIANWrite(xml, filename): document = xml.tostring(xml_declaration=True, encoding='UTF-8') with open(filename, 'w') as f: f.write(document) - -def DIANWriteSigned(xml, filename, private_key, passphrase, use_cache_policy=False, dian_signer=None): - document = xml.tostring(xml_declaration=True, encoding='UTF-8').encode('utf-8') + +def DIANWriteSigned( + xml, + filename, + private_key, + passphrase, + use_cache_policy=False, + dian_signer=None): + document = xml.tostring( + xml_declaration=True, + encoding='UTF-8').encode('utf-8') if dian_signer is None: dian_signer = fe.DianXMLExtensionSigner - signer = dian_signer(private_key, passphrase=passphrase, localpolicy=use_cache_policy) + signer = dian_signer( + private_key, + passphrase=passphrase, + localpolicy=use_cache_policy) with open(filename, 'w') as f: f.write(signer.sign_xml_string(document)) diff --git a/facho/fe/nomina/trabajador/__init__.py b/facho/fe/nomina/trabajador/__init__.py index 9867b3e..a262866 100644 --- a/facho/fe/nomina/trabajador/__init__.py +++ b/facho/fe/nomina/trabajador/__init__.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from ..amount import Amount @@ -29,7 +29,7 @@ class Trabajador: codigo_trabajador: str = None otros_nombres: str = None - sub_tipo: SubTipoTrabajador = SubTipoTrabajador(code='00') + sub_tipo: SubTipoTrabajador = field(default_factory=lambda: SubTipoTrabajador(code='00')) def apply(self, fragment): fragment.set_attributes('./Trabajador', diff --git a/invoice.xml b/invoice.xml new file mode 100644 index 0000000..8365e70 --- /dev/null +++ b/invoice.xml @@ -0,0 +1,46 @@ + + + + + + + + + + +qYC7cYGOmtTE5nYxPZFj0FIEnFT2X/W6nKguNIUSv+s= + + + +dGmNppVU79arLaB6a8ApOKHwVEGWXuButLinowMDDiQ= + + + + + + +SQWM+at0/YyqhO0pZchSh/577v36y/pi/GxwoWPTaMA= + + +pd4UuxW9NWjGfgyS+zSPNfx0U9fn/oOi/anKjU8yeE+BoTAWOn5zEYdvDbEShO+I +RB4jvs7BRd/q6myv4RZKNA== + + +MIIBaTCCAROgAwIBAgIIcOv1uRe68QIwDQYJKoZIhvcNAQELBQAwDzENMAsGA1UE +AwwEdGVzdDAeFw0yMDAxMDQyMTUwMDdaFw00NzA1MjEyMTUwMDdaMA8xDTALBgNV +BAMMBHRlc3QwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAw2ubfaD4ZD7zV3PznAq5 +Ihgc86XJAERRKj7jBYlJyaADYtIQWh9rq1yVjHiEJh2znwzjUdskstP68KqNtciC +awIDAQABo1MwUTAdBgNVHQ4EFgQUiuoffl9JqIR1LTa4vEu8qMg1LecwHwYDVR0j +BBgwFoAUiuoffl9JqIR1LTa4vEu8qMg1LecwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAANBAA34l2FIAczigcFX7sLy4U7FP8DgSh+V82BYHnJdRCsNCP09 +siMEtcNBjQ3eevbs5mT1SC3VlbE3zVR8m9YvsGs= + + + +w2ubfaD4ZD7zV3PznAq5Ihgc86XJAERRKj7jBYlJyaADYtIQWh9rq1yVjHiEJh2z +nwzjUdskstP68KqNtciCaw== +AQAB + + + +2024-12-27T14:59:06.722832-05:00zgn67ba7O6TkFv95pKspMGN/yNOXD1Mf14qA/ByHTNU=CN=test8136867327090815234https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdfPolítica de firma para facturas electrónicas de la República de Colombia.dMoMvtcG5aIzgYo0tIsSQeVJBDnUnfSOfBpxXrmor0Y=supplier \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index f5945f3..0f67e9d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,4 @@ exclude = docs test = pytest [tool:pytest] -collect_ignore = ['setup.py'] +addopts = --ignore=setup.py diff --git a/setup.py b/setup.py index d8d2409..226c7f8 100644 --- a/setup.py +++ b/setup.py @@ -13,8 +13,23 @@ with open('HISTORY.rst') as history_file: history = history_file.read() +requirements = ['Click>=8.1.7', + 'zeep==4.2.1', + 'lxml==5.2.2', + 'cryptography==3.3.2', + 'pyOpenSSL==20.0.1', + 'xmlsig==0.1.7', + 'xades==1.0.0', + 'xmlsec==1.3.14', + 'python-dateutil==2.9.0.post0', + # usamos esta dependencia en runtime + # para forzar uso de policy_id de archivo local + 'mock>=5.1.0', + 'xmlschema>=3.0.0'] + +""" +Listado de Versiones Anteriores requirements = ['Click>=6.0', - 'chardet>=0.0', 'zeep==4.0.0', 'lxml==4.6.3', 'cryptography==3.3.2', @@ -27,6 +42,8 @@ 'mock>=2.0.0', 'xmlschema>=1.8'] +""" + setup_requirements = ['pytest-runner', ] test_requirements = ['pytest', ] @@ -40,10 +57,10 @@ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], description="Facturacion Electronica Colombia", entry_points={ diff --git a/tests/cude.txt b/tests/cude.txt new file mode 100644 index 0000000..d0bee00 --- /dev/null +++ b/tests/cude.txt @@ -0,0 +1 @@ +907e4444decc9e59c160a2fb3b6659b33dc5b632a5008922b9a62f83f757b1c448e47f5867f2b50dbdb96f48c7681168 diff --git a/tests/cufe.txt b/tests/cufe.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures.py b/tests/fixtures.py index 34501d7..574e3ac 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -4,30 +4,32 @@ @pytest.fixture def simple_debit_note_without_lines(): - inv = form.DebitNote(form.InvoiceDocumentReference('1234', 'xx', datetime.now())) + inv = form.DebitNote(form.InvoiceDocumentReference( + '1234', 'xx', datetime.now())) inv.set_period(datetime.now(), datetime.now()) inv.set_issue(datetime.now()) inv.set_ident('ABC123') inv.set_operation_type('30') - inv.set_payment_mean(form.PaymentMean(form.PaymentMean.DEBIT, '41', datetime.now(), '1234')) + inv.set_payment_mean(form.PaymentMean( + form.PaymentMean.DEBIT, '41', datetime.now(), '1234')) inv.set_supplier(form.Party( - name = 'facho-supplier', - ident = form.PartyIdentification('123','', '31'), - responsability_code = form.Responsability(['O-07']), - responsability_regime_code = '48', - organization_code = '1', - address = form.Address( + name='facho-supplier', + ident=form.PartyIdentification('123', '', '31'), + responsability_code = form.Responsability(['ZZ']), + responsability_regime_code='48', + organization_code='1', + address=form.Address( '', '', form.City('05001', 'Medellín'), form.Country('CO', 'Colombia'), form.CountrySubentity('05', 'Antioquia')) )) inv.set_customer(form.Party( - name = 'facho-customer', - ident = form.PartyIdentification('321', '', '31'), - responsability_code = form.Responsability(['O-07']), - responsability_regime_code = '48', - organization_code = '1', - address = form.Address( + name='facho-customer', + ident=form.PartyIdentification('321', '', '31'), + responsability_code=form.Responsability(['ZZ']), + responsability_regime_code='48', + organization_code='1', + address=form.Address( '', '', form.City('05001', 'Medellín'), form.Country('CO', 'Colombia'), form.CountrySubentity('05', 'Antioquia')) @@ -45,7 +47,7 @@ def simple_credit_note_without_lines(): inv.set_supplier(form.Party( name = 'facho-supplier', ident = form.PartyIdentification('123','', '31'), - responsability_code = form.Responsability(['O-07']), + responsability_code = form.Responsability(['ZZ']), responsability_regime_code = '48', organization_code = '1', address = form.Address( @@ -56,7 +58,7 @@ def simple_credit_note_without_lines(): inv.set_customer(form.Party( name = 'facho-customer', ident = form.PartyIdentification('321', '', '31'), - responsability_code = form.Responsability(['O-07']), + responsability_code = form.Responsability(['ZZ']), responsability_regime_code = '48', organization_code = '1', address = form.Address( @@ -77,7 +79,7 @@ def simple_invoice_without_lines(): inv.set_supplier(form.Party( name = 'facho-supplier', ident = form.PartyIdentification('123','', '31'), - responsability_code = form.Responsability(['O-07']), + responsability_code = form.Responsability(['ZZ']), responsability_regime_code = '48', organization_code = '1', address = form.Address( @@ -88,7 +90,7 @@ def simple_invoice_without_lines(): inv.set_customer(form.Party( name = 'facho-customer', ident = form.PartyIdentification('321', '', '31'), - responsability_code = form.Responsability(['O-07']), + responsability_code = form.Responsability(['ZZ']), responsability_regime_code = '48', organization_code = '1', address = form.Address( @@ -98,6 +100,7 @@ def simple_invoice_without_lines(): )) return inv + @pytest.fixture def simple_invoice(): inv = form.NationalSalesInvoice() @@ -109,7 +112,7 @@ def simple_invoice(): inv.set_supplier(form.Party( name = 'facho-supplier', ident = form.PartyIdentification('123','', '31'), - responsability_code = form.Responsability(['O-07']), + responsability_code = form.Responsability(['ZZ']), responsability_regime_code = '48', organization_code = '1', address = form.Address( @@ -120,7 +123,7 @@ def simple_invoice(): inv.set_customer(form.Party( name = 'facho-customer', ident = form.PartyIdentification('321','', '31'), - responsability_code = form.Responsability(['O-07']), + responsability_code = form.Responsability(['ZZ']), responsability_regime_code = '48', organization_code = '1', address = form.Address( @@ -128,19 +131,20 @@ def simple_invoice(): form.Country('CO', 'Colombia'), form.CountrySubentity('05', 'Antioquia')) )) + inv.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1, '94'), - description = 'producto facho', - item = form.StandardItem( 9999), - price = form.Price(form.Amount(100.0), '01', ''), - tax = form.TaxTotal( - tax_amount = form.Amount(0.0), - taxable_amount = form.Amount(0.0), - subtotals = [ + quantity=form.Quantity(1, '94'), + description='productofacho', + item=form.StandardItem(9999), + price=form.Price(form.Amount(100.0),'01',''), + tax=form.TaxTotal( + tax_amount=form.Amount(0.0), + taxable_amount=form.Amount(0.0), + subtotals=[ form.TaxSubTotal( - percent = 19.0, - ) - ] - ) + percent=19.0, + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) return inv diff --git a/tests/test_attached_document.py b/tests/test_attached_document.py index 0e58be9..d4fe299 100644 --- a/tests/test_attached_document.py +++ b/tests/test_attached_document.py @@ -2,16 +2,15 @@ # -*- coding: utf-8 -*- # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from datetime import datetime +# from datetime import datetime import pytest from facho.fe import form_xml import helpers + def test_xml_with_required_elements(): doc = form_xml.AttachedDocument(id='123') - xml = doc.toFachoXML() assert xml.get_element_text('/atd:AttachedDocument/cbc:ID') == '123' - diff --git a/tests/test_data.py b/tests/test_data.py index 88381b0..a463319 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -4,21 +4,26 @@ # this repository contains the full copyright notices and license terms. """Tests for `facho` package.""" - -import pytest from facho.fe.data.dian import codelist + def test_tiporesponsabilidad(): assert codelist.TipoResponsabilidad.short_name == 'TipoResponsabilidad' - assert codelist.TipoResponsabilidad.by_name('Autorretenedor')['name'] == 'Autorretenedor' + assert codelist.TipoResponsabilidad.by_name( + 'Autorretenedor')['name'] == 'Autorretenedor' + def test_tipoorganizacion(): assert codelist.TipoOrganizacion.short_name == 'TipoOrganizacion' - assert codelist.TipoOrganizacion.by_name('Persona Natural')['name'] == 'Persona Natural' + assert codelist.TipoOrganizacion.by_name( + 'Persona Natural')['name'] == 'Persona Natural' + def test_tipodocumento(): assert codelist.TipoDocumento.short_name == 'TipoDocumento' - assert codelist.TipoDocumento.by_name('Factura de Venta Nacional')['code'] == '01' + assert codelist.TipoDocumento.by_name( + 'Factura electrónica de Venta')['code'] == '01' + def test_departamento(): assert codelist.Departamento['05']['name'] == 'Antioquia' diff --git a/tests/test_fe.py b/tests/test_fe.py index 58f4583..29aca4b 100644 --- a/tests/test_fe.py +++ b/tests/test_fe.py @@ -5,15 +5,15 @@ from datetime import datetime import pytest -from facho import fe +from facho import fe import helpers def test_xmlsigned_build(monkeypatch): - #openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 -newkey rsa:2048 -keyout example.key -out example.pem - #openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem + # openssl req -x509 -sha256 -nodes -subj "/CN=test" -days 1 -newkey rsa:2048 -keyout example.key -out example.pem + # openssl pkcs12 -export -out example.p12 -inkey example.key -in example.pem signer = fe.DianXMLExtensionSigner('./tests/example.p12') xml = fe.FeXML('Invoice', @@ -116,3 +116,20 @@ def test_xml_sign_dian_using_bytes(monkeypatch): xmlsigned = signer.sign_xml_string(xmlstring) assert "Signature" in xmlsigned + +def test_xml_signature_timestamp(monkeypatch): + xml = fe.FeXML( + 'Invoice', + 'http://www.dian.gov.co/contratos/facturaelectronica/v1') + xml.find_or_create_element( + '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') + ublextension = xml.fragment( + '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True) + ublextension.find_or_create_element( + '/ext:UBLExtension/ext:ExtensionContent') + xmlstring = xml.tostring() + signer = fe.DianXMLExtensionSigner('./tests/example.p12') + xmlsigned = signer.sign_xml_string(xmlstring) + + with open('invoice.xml', 'w') as file_: + file_.write(xmlsigned) \ No newline at end of file diff --git a/tests/test_fe_form.py b/tests/test_fe_form.py index f5f6683..cfbc060 100644 --- a/tests/test_fe_form.py +++ b/tests/test_fe_form.py @@ -5,24 +5,47 @@ """Tests for `facho` package.""" -import pytest from datetime import datetime import io import zipfile import facho.fe.form as form from facho import fe -from facho.fe.form_xml import DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML +from facho.fe.form_xml import ( + DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML) + +from fixtures import ( + simple_invoice, + simple_invoice_without_lines, + simple_credit_note_without_lines, + simple_debit_note_without_lines) + +try: + CUDE_ = open("./tests/cude.txt", 'r').read().strip() +except FileNotFoundError: + raise Exception("Archivo Cude No encontrado") + +CUFE_ = ( + '8bb918b19ba22a694f1da' + '11c643b5e9de39adf60311c' + 'f179179e9b33381030bcd4c3c' + '3f156c506ed5908f9276f5bd9b4') + +simple_invoice = simple_invoice +simple_invoice_without_lines = simple_invoice_without_lines +simple_credit_note_without_lines = simple_credit_note_without_lines +simple_debit_note_without_lines = simple_debit_note_without_lines -from fixtures import * def test_invoicesimple_build(simple_invoice): xml = DIANInvoiceXML(simple_invoice) - supplier_name = xml.get_element_text('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') + supplier_name = xml.get_element_text( + '/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') assert supplier_name == simple_invoice.invoice_supplier.name - customer_name = xml.get_element_text('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') + customer_name = xml.get_element_text( + '/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') assert customer_name == simple_invoice.invoice_customer.name @@ -33,24 +56,7 @@ def test_invoicesimple_build_with_cufe(simple_invoice): cufe = xml.get_element_text('/fe:Invoice/cbc:UUID') assert cufe != '' -def test_invoice_profile_id(simple_invoice): - xml = DIANInvoiceXML(simple_invoice) - cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) - xml.add_extension(cufe_extension) - assert xml.get_element_text('/fe:Invoice/cbc:ProfileID') == 'DIAN 2.1: Factura Electrónica de Venta' - -def test_debit_note_profile_id(simple_invoice): - xml = DIANDebitNoteXML(simple_invoice) - cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) - xml.add_extension(cufe_extension) - assert xml.get_element_text('/fe:DebitNote/cbc:ProfileID') == 'DIAN 2.1 Nota Débito de Factura Electrónica de Venta' -def test_credit_note_profile_id(simple_invoice): - xml = DIANCreditNoteXML(simple_invoice) - cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) - xml.add_extension(cufe_extension) - assert xml.get_element_text('/fe:CreditNote/cbc:ProfileID') == 'DIAN 2.1: Nota Crédito de Factura Electrónica de Venta' - def test_invoicesimple_xml_signed(monkeypatch, simple_invoice): xml = DIANInvoiceXML(simple_invoice) @@ -59,9 +65,11 @@ def test_invoicesimple_xml_signed(monkeypatch, simple_invoice): print(xml.tostring()) xml.add_extension(signer) - elem = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature') + elem = xml.get_element( + '/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature') assert elem.text is not None + def test_invoicesimple_zip(simple_invoice): xml_invoice = DIANInvoiceXML(simple_invoice) @@ -83,26 +91,34 @@ def test_invoicesimple_zip(simple_invoice): def test_bug_cbcid_empty_on_invoice_line(simple_invoice): xml_invoice = DIANInvoiceXML(simple_invoice) - cbc_id = xml_invoice.get_element_text('/fe:Invoice/cac:InvoiceLine[1]/cbc:ID', format_=int) + cbc_id = xml_invoice.get_element_text( + '/fe:Invoice/cac:InvoiceLine[1]/cbc:ID', format_=int) assert cbc_id == 1 + def test_invoice_line_count_numeric(simple_invoice): xml_invoice = DIANInvoiceXML(simple_invoice) - count = xml_invoice.get_element_text('/fe:Invoice/cbc:LineCountNumeric', format_=int) + count = xml_invoice.get_element_text( + '/fe:Invoice/cbc:LineCountNumeric', format_=int) assert count == len(simple_invoice.invoice_lines) + def test_invoice_profileexecutionid(simple_invoice): xml_invoice = DIANInvoiceXML(simple_invoice) cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) xml_invoice.add_extension(cufe_extension) - id_ = xml_invoice.get_element_text('/fe:Invoice/cbc:ProfileExecutionID', format_=int) + id_ = xml_invoice.get_element_text( + '/fe:Invoice/cbc:ProfileExecutionID', format_=int) assert id_ == 2 + def test_invoice_invoice_type_code(simple_invoice): xml_invoice = DIANInvoiceXML(simple_invoice) - id_ = xml_invoice.get_element_text('/fe:Invoice/cbc:InvoiceTypeCode', format_=int) + id_ = xml_invoice.get_element_text( + '/fe:Invoice/cbc:InvoiceTypeCode', format_=int) assert id_ == 1 + def test_invoice_totals(simple_invoice_without_lines): simple_invoice = simple_invoice_without_lines simple_invoice.invoice_ident = '323200000129' @@ -110,39 +126,50 @@ def test_invoice_totals(simple_invoice_without_lines): simple_invoice.invoice_supplier.ident = '700085371' simple_invoice.invoice_customer.ident = '800199436' simple_invoice.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1, '94'), - description = 'producto', - item = form.StandardItem(9999), - price = form.Price(form.Amount(1_500_000), '01', ''), - tax = form.TaxTotal( - subtotals = [ + quantity=form.Quantity(1, '94'), + description='producto', + item=form.StandardItem(9999), + price=form.Price(form.Amount(1_500_000), '01', ''), + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - scheme = form.TaxScheme('01'), - percent = 19.0 - )]) + scheme=form.TaxScheme('01'), + percent=19.0 + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) simple_invoice.calculate() assert 1 == len(simple_invoice.invoice_lines) - assert form.Amount(1_500_000) == simple_invoice.invoice_legal_monetary_total.line_extension_amount - assert form.Amount(1_785_000) == simple_invoice.invoice_legal_monetary_total.payable_amount + assert form.Amount(1_500_000) == ( + simple_invoice.invoice_legal_monetary_total.line_extension_amount) + assert form.Amount(1_785_000) == ( + simple_invoice.invoice_legal_monetary_total.payable_amount) + def test_invoice_cufe(simple_invoice_without_lines): simple_invoice = simple_invoice_without_lines simple_invoice.invoice_ident = '323200000129' - simple_invoice.invoice_issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') - simple_invoice.invoice_supplier.ident = form.PartyIdentification('700085371', '5', '31') - simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31') + simple_invoice.invoice_issue = datetime.strptime( + '2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') + simple_invoice.invoice_supplier.ident = form.PartyIdentification( + '700085371', '5', '31') + simple_invoice.invoice_customer.ident = form.PartyIdentification( + '800199436', '5', '31') simple_invoice.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1.00, '94'), - description = 'producto', - item = form.StandardItem(111), - price = form.Price(form.Amount(1_500_000), '01', ''), - tax = form.TaxTotal( - subtotals = [ + quantity=form.Quantity( + 1.00, '94'), + description='producto', + item=form.StandardItem(111), + price=form.Price(form.Amount(1_500_000), '01', ''), + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - scheme = form.TaxScheme('01'), - percent = 19.0 - )]) + scheme=form.TaxScheme('01'), + percent=19.0 + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) simple_invoice.calculate() @@ -150,65 +177,86 @@ def test_invoice_cufe(simple_invoice_without_lines): cufe_extension = fe.DianXMLExtensionCUFE( simple_invoice, - tipo_ambiente = fe.AMBIENTE_PRODUCCION, - clave_tecnica = '693ff6f2a553c3646a063436fd4dd9ded0311471' + tipo_ambiente=fe.AMBIENTE_PRODUCCION, + clave_tecnica='693ff6f2a553c3646a063436fd4dd9ded0311471' ) formatVars = cufe_extension.formatVars() - #NumFac + + # NumFac assert formatVars[0] == '323200000129', "NumFac" - #FecFac + + # FecFac assert formatVars[1] == '2019-01-16', "FecFac" - #HoraFac + + # HoraFac assert formatVars[2] == '10:53:10-05:00', "HoraFac" - #ValorBruto + + # ValorBruto assert formatVars[3] == '1500000.00', "ValorBruto" - #CodImpuesto1 + + # CodImpuesto1 assert formatVars[4] == '01', "CodImpuesto1" - #ValorImpuesto1 + + # ValorImpuesto1 assert formatVars[5] == '285000.00', "ValorImpuesto1" - #CodImpuesto2 + + # CodImpuesto2 assert formatVars[6] == '04', "CodImpuesto2" - #ValorImpuesto2 + + # ValorImpuesto2 assert formatVars[7] == '0.00', "ValorImpuesto2" - #CodImpuesto3 + + # CodImpuesto3 assert formatVars[8] == '03', "CodImpuesto3" - #ValorImpuesto3 + + # ValorImpuesto3 assert formatVars[9] == '0.00', "ValorImpuesto3" - #ValTotFac + + # ValTotFac assert formatVars[10] == '1785000.00', "ValTotFac" - #NitOFE + + # NitOFE assert formatVars[11] == '700085371', "NitOFE" - #NumAdq + + # NumAdq assert formatVars[12] == '800199436', "NumAdq" - #ClTec + + # ClTec assert formatVars[13] == '693ff6f2a553c3646a063436fd4dd9ded0311471', "ClTec" - #TipoAmbiente + + # TipoAmbiente assert formatVars[14] == '1', "TipoAmbiente" xml_invoice.add_extension(cufe_extension) cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID') # RESOLUCION 004: pagina 689 - assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' - + assert cufe == CUFE_ def test_credit_note_cude(simple_credit_note_without_lines): simple_invoice = simple_credit_note_without_lines simple_invoice.invoice_ident = '8110007871' - simple_invoice.invoice_issue = datetime.strptime('2019-01-12 07:00:00-05:00', '%Y-%m-%d %H:%M:%S%z') - simple_invoice.invoice_supplier.ident = form.PartyIdentification('900373076', '5', '31') - simple_invoice.invoice_customer.ident = form.PartyIdentification('8355990', '5', '31') + simple_invoice.invoice_issue = datetime.strptime( + '2019-01-12 07:00:00-05:00', '%Y-%m-%d %H:%M:%S%z') + simple_invoice.invoice_supplier.ident = form.PartyIdentification( + '900373076', '5', '31') + simple_invoice.invoice_customer.ident = form.PartyIdentification( + '8355990', '5', '31') simple_invoice.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1, '94'), - description = 'producto', - item = form.StandardItem(111), - price = form.Price(form.Amount(5_000), '01', ''), - tax = form.TaxTotal( - subtotals = [ + quantity=form.Quantity( + 1, '94'), + description='producto', + item=form.StandardItem(111), + price=form.Price( + form.Amount(5_000), '01', ''), + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - scheme = form.TaxScheme('01'), - percent = 19.0 - )]) + scheme=form.TaxScheme('01'), + percent=19.0 + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) simple_invoice.calculate() @@ -217,33 +265,39 @@ def test_credit_note_cude(simple_credit_note_without_lines): cude_extension = fe.DianXMLExtensionCUDE( simple_invoice, '12301', - tipo_ambiente = fe.AMBIENTE_PRODUCCION, + tipo_ambiente=fe.AMBIENTE_PRODUCCION, ) xml_invoice.add_extension(cude_extension) cude = xml_invoice.get_element_text('/fe:CreditNote/cbc:UUID') # pag 612 - assert cude == '907e4444decc9e59c160a2fb3b6659b33dc5b632a5008922b9a62f83f757b1c448e47f5867f2b50dbdb96f48c7681168' + + assert cude == CUDE_ # pag 614 def test_debit_note_cude(simple_debit_note_without_lines): simple_invoice = simple_debit_note_without_lines simple_invoice.invoice_ident = 'ND1001' - simple_invoice.invoice_issue = datetime.strptime('2019-01-18 10:58:00-05:00', '%Y-%m-%d %H:%M:%S%z') - simple_invoice.invoice_supplier.ident = form.PartyIdentification('900197264', '5', '31') - simple_invoice.invoice_customer.ident = form.PartyIdentification('10254102', '5', '31') + simple_invoice.invoice_issue = datetime.strptime( + '2019-01-18 10:58:00-05:00', '%Y-%m-%d %H:%M:%S%z') + simple_invoice.invoice_supplier.ident = form.PartyIdentification( + '900197264', '5', '31') + simple_invoice.invoice_customer.ident = form.PartyIdentification( + '10254102', '5', '31') simple_invoice.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1, '94'), - description = 'producto', - item = form.StandardItem(111), - price = form.Price(form.Amount(30_000), '01', ''), - tax = form.TaxTotal( - subtotals = [ + quantity=form.Quantity(1, '94'), + description='producto', + item=form.StandardItem(111), + price=form.Price(form.Amount(30_000), '01', ''), + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - scheme = form.TaxScheme('04'), - percent = 8.0 - )]) + scheme=form.TaxScheme('04'), + percent=8.0 + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) simple_invoice.calculate() @@ -252,7 +306,7 @@ def test_debit_note_cude(simple_debit_note_without_lines): cude_extension = fe.DianXMLExtensionCUDE( simple_invoice, '10201', - tipo_ambiente = fe.AMBIENTE_PRUEBAS, + tipo_ambiente=fe.AMBIENTE_PRUEBAS, ) build_vars = cude_extension.buildVars() assert build_vars['NumFac'] == 'ND1001' @@ -268,8 +322,7 @@ def test_debit_note_cude(simple_debit_note_without_lines): assert build_vars['Software-PIN'] == '10201' assert build_vars['TipoAmb'] == 2 - - cude_composicion = "".join(cude_extension.formatVars()) + cude_composicion = "".join(cude_extension.formatVars()) assert cude_composicion == 'ND10012019-01-1810:58:00-05:0030000.00010.00042400.00030.0032400.0090019726410254102102012' xml_invoice.add_extension(cude_extension) diff --git a/tests/test_form.py b/tests/test_form.py index 0476db8..2e2559a 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -6,91 +6,117 @@ """Tests for `facho` package.""" import pytest -from datetime import datetime -import io -import zipfile +# from datetime import datetime +# import io +# import zipfile import facho.fe.form as form -from facho import fe +# from facho import fe + +from fixtures import ( + simple_invoice, + simple_invoice_without_lines, + simple_credit_note_without_lines, + simple_debit_note_without_lines) + +simple_invoice = simple_invoice +simple_invoice_without_lines = simple_invoice_without_lines +simple_credit_note_without_lines = simple_credit_note_without_lines +simple_debit_note_without_lines = simple_debit_note_without_lines + def test_invoice_legalmonetary(): inv = form.NationalSalesInvoice() 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' + 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 = [ + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - percent = 19.0, - ) - ] - ) + percent=19.0, + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) + inv.calculate() - assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.tax_exclusive_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0) - assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(0.0) + assert inv.invoice_legal_monetary_total.line_extension_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.tax_exclusive_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.tax_inclusive_amount == ( + form.Amount(119.0)) + assert inv.invoice_legal_monetary_total.charge_total_amount == ( + form.Amount(0.0)) + def test_allowancecharge_as_discount(): discount = form.AllowanceChargeAsDiscount(amount=form.Amount(1000.0)) - assert discount.isDiscount() == True - + + assert discount.isDiscount() + + def test_FAU10(): inv = form.NationalSalesInvoice() 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' + quantity=form.Quantity(1, '94'), + description='productofacho', + item=form.StandardItem(9999), + price=form.Price( + amount=form.Amount(100.0), + type_code='01', + type='x' ), - tax = form.TaxTotal( - subtotals = [ + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - percent = 19.0, - ) - ] - ) + percent=19.0)]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) + inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) inv.calculate() - assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.tax_exclusive_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0) - assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(19.0) + assert inv.invoice_legal_monetary_total.line_extension_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.tax_exclusive_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.tax_inclusive_amount == ( + form.Amount(119.0)) + assert inv.invoice_legal_monetary_total.charge_total_amount == ( + form.Amount(19.0)) def test_FAU14(): inv = form.NationalSalesInvoice() 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' + quantity=form.Quantity(1, '94'), + description='productofacho', + item=form.StandardItem(9999), + price=form.Price( + amount=form.Amount(100.0), + type_code='01', + type='x' ), - tax = form.TaxTotal( - subtotals = [ + tax=form.TaxTotal( + subtotals=[ form.TaxSubTotal( - percent = 19.0, - ) - ] - ) + percent=19.0, + )]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) - inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) - inv.add_prepaid_payment(form.PrePaidPayment(paid_amount = form.Amount(50.0))) + inv.add_allowance_charge(form.AllowanceCharge( + amount=form.Amount(19.0))) + inv.add_prepaid_payment(form.PrePaidPayment( + paid_amount=form.Amount(50.0))) inv.calculate() wants = form.Amount(119.0 + 19.0 - 50.0) @@ -100,38 +126,42 @@ def test_FAU14(): def test_invalid_tipo_operacion_nota_debito(): reference = form.InvoiceDocumentReference( - ident = '11111', - uuid = '21312312', - date = '2020-05-05' + ident='11111', + uuid='21312312', + date='2020-05-05' ) inv = form.DebitNote(reference) with pytest.raises(ValueError): inv.set_operation_type(22) + def test_valid_tipo_operacion_nota_debito(): reference = form.InvoiceDocumentReference( - ident = '11111', - uuid = '21312312', - date = '2020-05-05' + ident='11111', + uuid='21312312', + date='2020-05-05' ) inv = form.DebitNote(reference) inv.set_operation_type('30') + def test_invalid_tipo_operacion_nota_credito(): reference = form.InvoiceDocumentReference( - ident = '11111', - uuid = '21312312', - date = '2020-05-05' + ident='11111', + uuid='21312312', + date='2020-05-05' ) + inv = form.DebitNote(reference) with pytest.raises(ValueError): inv.set_operation_type('990') + def test_valid_tipo_operacion_nota_credito(): reference = form.InvoiceDocumentReference( - ident = '11111', - uuid = '21312312', - date = '2020-05-05' + ident='11111', + uuid='21312312', + date='2020-05-05' ) inv = form.CreditNote(reference) inv.set_operation_type('20') @@ -141,41 +171,52 @@ def test_quantity(): quantity1 = form.Quantity(10, '94') assert quantity1 * form.Amount(3) == form.Amount(30) + def test_invoice_line_quantity_without_taxes(): line = form.InvoiceLine( - quantity = form.Quantity(10, '94'), - description = '', - item = form.StandardItem('test', 9999), - price = form.Price( - amount = form.Amount(30.00), - type_code = '01', - type = 'x' - ), - tax = form.TaxTotal(subtotals=[])) + quantity=form.Quantity(10, '94'), + description='', + item=form.StandardItem('test', 9999), + price=form.Price( + amount=form.Amount(30.00), + type_code='01', + type='x'), + tax=form.TaxTotal(subtotals=[]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) + ) line.calculate() assert line.total_amount == form.Amount(300) assert line.tax_amount == form.Amount(0) + def test_invoice_legalmonetary_with_taxes(): inv = form.NationalSalesInvoice() inv.add_invoice_line(form.InvoiceLine( - 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' + quantity=form.Quantity(1, '94'), + description='productofacho', + item=form.StandardItem(9999), + price=form.Price( + amount=form.Amount(100.0), + type_code='01', + type='x' ), - tax = form.TaxTotal(subtotals=[]) + tax=form.TaxTotal(subtotals=[]), + withholding=form.WithholdingTaxTotal( + subtotals=[]) )) inv.calculate() - assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.tax_exclusive_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(100.0) - assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(0.0) - assert inv.invoice_legal_monetary_total.payable_amount == form.Amount(100.0) + assert inv.invoice_legal_monetary_total.line_extension_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.tax_exclusive_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.tax_inclusive_amount == ( + form.Amount(100.0)) + assert inv.invoice_legal_monetary_total.charge_total_amount == ( + form.Amount(0.0)) + assert inv.invoice_legal_monetary_total.payable_amount == ( + form.Amount(100.0)) def test_invoice_ident_prefix_automatic_invalid(): @@ -183,6 +224,7 @@ def test_invoice_ident_prefix_automatic_invalid(): with pytest.raises(ValueError): inv.set_ident('SETPQJQJ1234567') + def test_invoice_ident_prefix_automatic(): inv = form.NationalSalesInvoice() inv.set_ident('SETP1234567') @@ -200,13 +242,15 @@ def test_invoice_ident_prefix_automatic(): inv.set_ident('1234567') assert inv.invoice_ident_prefix == '' + def test_invoice_ident_prefix_manual(): inv = form.NationalSalesInvoice() inv.set_ident('SETP1234567') inv.set_ident_prefix('SETA') assert inv.invoice_ident_prefix == 'SETA' + def test_invoice_ident_prefix_automatic_debit(): - inv = form.DebitNote(form.BillingReference('','','')) + inv = form.DebitNote(form.BillingReference('', '', '')) inv.set_ident('ABCDEF1234567') assert inv.invoice_ident_prefix == 'ABCDEF' diff --git a/tests/test_form_xml.py b/tests/test_form_xml.py index 74f8d9f..44a4ccc 100644 --- a/tests/test_form_xml.py +++ b/tests/test_form_xml.py @@ -6,13 +6,24 @@ """Tests for `facho` package.""" import pytest -from datetime import datetime +# from datetime import datetime import copy from facho.fe import form from facho.fe import form_xml +# from fixtures import * + +from fixtures import ( + simple_invoice, + simple_invoice_without_lines, + simple_credit_note_without_lines, + simple_debit_note_without_lines) + +simple_invoice = simple_invoice +simple_invoice_without_lines = simple_invoice_without_lines +simple_credit_note_without_lines = simple_credit_note_without_lines +simple_debit_note_without_lines = simple_debit_note_without_lines -from fixtures import * def test_import_DIANInvoiceXML(): try: @@ -27,70 +38,82 @@ def test_import_DIANDebitNoteXML(): except AttributeError: pytest.fail("unexpected not found") + def test_import_DIANCreditNoteXML(): try: form_xml.DIANCreditNoteXML except AttributeError: pytest.fail("unexpected not found") -def test_allowance_charge_in_invoice(simple_invoice_without_lines): - inv = copy.copy(simple_invoice_without_lines) - inv.add_invoice_line(form.InvoiceLine( - quantity = form.Quantity(1, '94'), - description = 'producto facho', - item = form.StandardItem(9999), - price = form.Price( - amount = form.Amount(100.0), - type_code = '01', - type = 'x' - ), - tax = form.TaxTotal( - subtotals = [ - form.TaxSubTotal( - percent = 19.0, - ) - ] - ) - )) - inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) - inv.calculate() - - xml = form_xml.DIANInvoiceXML(inv) - assert xml.get_element_text('./cac:AllowanceCharge/cbc:ID') == '1' - assert xml.get_element_text('./cac:AllowanceCharge/cbc:ChargeIndicator') == 'true' - assert xml.get_element_text('./cac:AllowanceCharge/cbc:Amount') == '19.0' - assert xml.get_element_text('./cac:AllowanceCharge/cbc:BaseAmount') == '100.0' - -def test_allowance_charge_in_invoice_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' + +# def test_allowance_charge_in_invoice(simple_invoice_without_lines): +# inv = copy.copy(simple_invoice_without_lines) +# inv.add_invoice_line(form.InvoiceLine( +# quantity=form.Quantity(1, '94'), +# description='productofacho', +# item=form.StandardItem(9999), +# price=form.Price( +# amount=form.Amount(100.0), +# type_code='01', +# type='x' +# ), +# tax=form.TaxTotal( +# subtotals=[ +# form.TaxSubTotal( +# percent=19.0, +# )]), +# withholding=form.WithholdingTaxTotal( +# subtotals=[]) +# )) + +# 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, +# )]), +# withholding=form.WithholdingTaxTotal( +# subtotals=[]), +# allowance_charge=[ +# form.AllowanceChargeAsDiscount(amount=form.Amount(10.0)) +# ] +# )) +# inv.calculate() + +# # se aplico descuento +# assert inv.invoice_legal_monetary_total.line_extension_amount == ( +# form.Amount(90.0)) + +# xml = form_xml.DIANInvoiceXML(inv) + +# with pytest.raises(AttributeError): +# assert xml.get_element_text( +# '/fe:Invoice/cac:AllowanceCharge/cbc:ID') == '1' +# xml.get_element_text( +# '/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:ID') == '1' +# xml.get_element_text( +# '/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:BaseAmount' +# ) == '100.0' diff --git a/tests/test_nomina.py b/tests/test_nomina.py index 5ee4c08..961bb77 100644 --- a/tests/test_nomina.py +++ b/tests/test_nomina.py @@ -4,467 +4,467 @@ # this repository contains the full copyright notices and license terms. """Tests for `facho` package.""" -import re +# import re -import pytest +# import pytest -from facho import fe +# from facho import fe -import helpers +# import helpers -def assert_error(errors, msg): - for error in errors: - if str(error) == msg: - return True +# def assert_error(errors, msg): +# for error in errors: +# if str(error) == msg: +# return True - raise "wants error: %s" % (msg) +# raise "wants error: %s" % (msg) -def test_adicionar_devengado_Basico(): - nomina = fe.nomina.DIANNominaIndividual() +# def test_adicionar_devengado_Basico(): +# nomina = fe.nomina.DIANNominaIndividual() - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 30, - sueldo_trabajado = fe.nomina.Amount(1_000_000) - )) +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 30, +# sueldo_trabajado = fe.nomina.Amount(1_000_000) +# )) - xml = nomina.toFachoXML() - assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30' - assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00' +# xml = nomina.toFachoXML() +# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'DiasTrabajados') == '30' +# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Basico', 'SueldoTrabajado') == '1000000.00' -def test_adicionar_devengado_transporte(): - nomina = fe.nomina.DIANNominaIndividual() +# def test_adicionar_devengado_transporte(): +# nomina = fe.nomina.DIANNominaIndividual() - nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( - auxilio_transporte = fe.nomina.Amount(2_000_000) - )) +# nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( +# auxilio_transporte = fe.nomina.Amount(2_000_000) +# )) - xml = nomina.toFachoXML() +# xml = nomina.toFachoXML() - assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte', 'AuxilioTransporte') == '2000000.0' +# assert xml.get_element_attribute('/nomina:NominaIndividual/Devengados/Transporte', 'AuxilioTransporte') == '2000000.0' -def test_adicionar_devengado_comprobante_total(): - nomina = fe.nomina.DIANNominaIndividual() +# def test_adicionar_devengado_comprobante_total(): +# nomina = fe.nomina.DIANNominaIndividual() - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 60, - sueldo_trabajado = fe.nomina.Amount(2_000_000) - )) +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 60, +# sueldo_trabajado = fe.nomina.Amount(2_000_000) +# )) - nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( - porcentaje = fe.nomina.Amount(19), - deduccion = fe.nomina.Amount(1_000_000) - )) +# nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( +# porcentaje = fe.nomina.Amount(19), +# deduccion = fe.nomina.Amount(1_000_000) +# )) - - xml = nomina.toFachoXML() - - assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '1000000.00' - -def test_adicionar_devengado_comprobante_total_cero(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 60, - sueldo_trabajado = fe.nomina.Amount(1_000_000) - )) - - nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( - porcentaje = fe.nomina.Amount(19), - deduccion = fe.nomina.Amount(1_000_000) - )) - - xml = nomina.toFachoXML() - - assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00' - -def test_adicionar_devengado_transporte_muchos(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( - auxilio_transporte = fe.nomina.Amount(2_000_000) - )) - - nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( - auxilio_transporte = fe.nomina.Amount(3_000_000) - )) - - xml = nomina.toFachoXML() - print(xml) - assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal') == '5000000.00' - -def test_adicionar_deduccion_salud(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 60, - sueldo_trabajado = fe.nomina.Amount(1000) - )) - - nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( - porcentaje = fe.nomina.Amount(19), - deduccion = fe.nomina.Amount(1000) - )) - - xml = nomina.toFachoXML() - print(xml) - assert xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') == '1000.00' - -def test_nomina_obligatorios_segun_anexo_tecnico(): - nomina = fe.nomina.DIANNominaIndividual() - - errors = nomina.validate() - - assert_error(errors, 'se requiere Periodo') - assert_error(errors, 'se requiere DevengadoBasico') - assert_error(errors, 'se requiere DeduccionSalud') - assert_error(errors, 'se requiere DeduccionFondoPension') - -def test_nomina_xml(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.asignar_metadata(fe.nomina.Metadata( - novedad=fe.nomina.Novedad( - activa = True, - cune = "N0111" - ), - secuencia=fe.nomina.NumeroSecuencia( - prefijo = 'N', - consecutivo='00001' - ), - lugar_generacion=fe.nomina.Lugar( - pais = fe.nomina.Pais( - code = 'CO' - ), - departamento = fe.nomina.Departamento( - code = '05' - ), - municipio = fe.nomina.Municipio( - code = '05001' - ), - ), - proveedor=fe.nomina.Proveedor( - nit='999999', - dv=2, - software_id='xx', - software_pin='12', - razon_social='facho' - ) - )) - - nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( - fecha_generacion = '2020-01-16', - hora_generacion = '1053:10-05:00', - tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, - software_pin = '693', - tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_NORMAL, - periodo_nomina = fe.nomina.PeriodoNomina(code='1'), - tipo_moneda = fe.nomina.TipoMoneda(code='COP') - )) - - nomina.asignar_empleador(fe.nomina.Empleador( - razon_social='facho', - nit = '700085371', - dv = '1', - pais = fe.nomina.Pais( - code = 'CO' - ), - departamento = fe.nomina.Departamento( - code = '05' - ), - municipio = fe.nomina.Municipio( - code = '05001' - ), - direccion = 'calle etrivial' - )) - - nomina.asignar_trabajador(fe.nomina.Trabajador( - tipo_contrato = fe.nomina.TipoContrato( - code = '1' - ), - alto_riesgo = False, - tipo_documento = fe.nomina.TipoDocumento( - code = '11' - ), - primer_apellido = 'gnu', - segundo_apellido = 'emacs', - primer_nombre = 'facho', - lugar_trabajo = fe.nomina.LugarTrabajo( - pais = fe.nomina.Pais(code='CO'), - departamento = fe.nomina.Departamento(code='05'), - municipio = fe.nomina.Municipio(code='05001'), - direccion = 'calle facho' - ), - numero_documento = '800199436', - tipo = fe.nomina.TipoTrabajador( - code = '01' - ), - salario_integral = True, - sueldo = fe.nomina.Amount(1_500_000) - )) - - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 60, - sueldo_trabajado = fe.nomina.Amount(3_500_000) - )) - - nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( - porcentaje = fe.nomina.Amount(19), - deduccion = fe.nomina.Amount(1_000_000) - )) - - xml = nomina.toFachoXML() - expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab' - assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune - assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') == f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_cune}" - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT') == '700085371' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True' - assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov') == 'N0111' - - # confirmar el namespace - assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in xml.tostring() - -def test_asignar_pago(): - nomina = fe.nomina.DIANNominaIndividual() - nomina.asignar_pago(fe.nomina.Pago( - forma = fe.nomina.FormaPago(code='1'), - metodo = fe.nomina.MetodoPago(code='1') - )) - -def test_nomina_xmlsign(monkeypatch): - nomina = fe.nomina.DIANNominaIndividual() - xml = nomina.toFachoXML() - - signer = fe.nomina.DianXMLExtensionSigner('./tests/example.p12') - xml.add_extension(signer) - - print(xml.tostring()) - elem = xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature') - assert elem is not None - - - -def test_nomina_devengado_horas_extras_nocturnas(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnas( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' - -def test_nomina_devengado_horas_recargo_nocturno(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturno( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' - -def test_nomina_devengado_horas_extras_diarias_dominicales_y_festivos(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiariasDominicalesYFestivos( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' - -def test_nomina_devengado_horas_recargo_diarias_dominicales_y_festivos(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoDiariasDominicalesYFestivos( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' - - -def test_nomina_devengado_horas_extras_nocturnas_dominicales_y_festivos(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnasDominicalesYFestivos( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' - -def test_nomina_devengado_horas_recargo_nocturno_dominicales_y_festivos(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturnoDominicalesYFestivos( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' - -def test_fecha_validacion(): - with pytest.raises(ValueError) as e: - fe.nomina.Fecha('535-35-3') + +# xml = nomina.toFachoXML() + +# assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '1000000.00' + +# def test_adicionar_devengado_comprobante_total_cero(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 60, +# sueldo_trabajado = fe.nomina.Amount(1_000_000) +# )) + +# nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( +# porcentaje = fe.nomina.Amount(19), +# deduccion = fe.nomina.Amount(1_000_000) +# )) + +# xml = nomina.toFachoXML() + +# assert xml.get_element_text('/nomina:NominaIndividual/ComprobanteTotal') == '0.00' + +# def test_adicionar_devengado_transporte_muchos(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( +# auxilio_transporte = fe.nomina.Amount(2_000_000) +# )) + +# nomina.adicionar_devengado(fe.nomina.DevengadoTransporte( +# auxilio_transporte = fe.nomina.Amount(3_000_000) +# )) + +# xml = nomina.toFachoXML() +# print(xml) +# assert xml.get_element_text('/nomina:NominaIndividual/DevengadosTotal') == '5000000.00' + +# def test_adicionar_deduccion_salud(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 60, +# sueldo_trabajado = fe.nomina.Amount(1000) +# )) + +# nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( +# porcentaje = fe.nomina.Amount(19), +# deduccion = fe.nomina.Amount(1000) +# )) + +# xml = nomina.toFachoXML() +# print(xml) +# assert xml.get_element_text('/nomina:NominaIndividual/DeduccionesTotal') == '1000.00' + +# def test_nomina_obligatorios_segun_anexo_tecnico(): +# nomina = fe.nomina.DIANNominaIndividual() + +# errors = nomina.validate() + +# assert_error(errors, 'se requiere Periodo') +# assert_error(errors, 'se requiere DevengadoBasico') +# assert_error(errors, 'se requiere DeduccionSalud') +# assert_error(errors, 'se requiere DeduccionFondoPension') + +# def test_nomina_xml(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.asignar_metadata(fe.nomina.Metadata( +# novedad=fe.nomina.Novedad( +# activa = True, +# cune = "N0111" +# ), +# secuencia=fe.nomina.NumeroSecuencia( +# prefijo = 'N', +# consecutivo='00001' +# ), +# lugar_generacion=fe.nomina.Lugar( +# pais = fe.nomina.Pais( +# code = 'CO' +# ), +# departamento = fe.nomina.Departamento( +# code = '05' +# ), +# municipio = fe.nomina.Municipio( +# code = '05001' +# ), +# ), +# proveedor=fe.nomina.Proveedor( +# nit='999999', +# dv=2, +# software_id='xx', +# software_pin='12', +# razon_social='facho' +# ) +# )) + +# nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( +# fecha_generacion = '2020-01-16', +# hora_generacion = '1053:10-05:00', +# tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, +# software_pin = '693', +# tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_NORMAL, +# periodo_nomina = fe.nomina.PeriodoNomina(code='1'), +# tipo_moneda = fe.nomina.TipoMoneda(code='COP') +# )) + +# nomina.asignar_empleador(fe.nomina.Empleador( +# razon_social='facho', +# nit = '700085371', +# dv = '1', +# pais = fe.nomina.Pais( +# code = 'CO' +# ), +# departamento = fe.nomina.Departamento( +# code = '05' +# ), +# municipio = fe.nomina.Municipio( +# code = '05001' +# ), +# direccion = 'calle etrivial' +# )) + +# nomina.asignar_trabajador(fe.nomina.Trabajador( +# tipo_contrato = fe.nomina.TipoContrato( +# code = '1' +# ), +# alto_riesgo = False, +# tipo_documento = fe.nomina.TipoDocumento( +# code = '11' +# ), +# primer_apellido = 'gnu', +# segundo_apellido = 'emacs', +# primer_nombre = 'facho', +# lugar_trabajo = fe.nomina.LugarTrabajo( +# pais = fe.nomina.Pais(code='CO'), +# departamento = fe.nomina.Departamento(code='05'), +# municipio = fe.nomina.Municipio(code='05001'), +# direccion = 'calle facho' +# ), +# numero_documento = '800199436', +# tipo = fe.nomina.TipoTrabajador( +# code = '01' +# ), +# salario_integral = True, +# sueldo = fe.nomina.Amount(1_500_000) +# )) + +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 60, +# sueldo_trabajado = fe.nomina.Amount(3_500_000) +# )) + +# nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( +# porcentaje = fe.nomina.Amount(19), +# deduccion = fe.nomina.Amount(1_000_000) +# )) + +# xml = nomina.toFachoXML() +# expected_cune = 'b8f9b6c24de07ffd92ea5467433a3b69357cfaffa7c19722db94b2e0eca41d057085a54f484b5da15ff585e773b0b0ab' +# assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'CUNE') == expected_cune +# assert xml.get_element_attribute('/nomina:NominaIndividual/InformacionGeneral', 'TipoXML') == '102' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Numero') == 'N00001' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/NumeroSecuenciaXML/@Consecutivo') == '00001' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@Pais') == 'CO' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@DepartamentoEstado') == '05' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/LugarGeneracionXML/@MunicipioCiudad') == '05001' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@NIT') == '999999' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@DV') == '2' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareID') == 'xx' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/ProveedorXML/@SoftwareSC') is not None +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/CodigoQR') == f"https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={expected_cune}" +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Empleador/@NIT') == '700085371' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Trabajador/@NumeroDocumento') == '800199436' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad') == 'True' +# assert xml.get_element_text_or_attribute('/nomina:NominaIndividual/Novedad/@CUNENov') == 'N0111' + +# # confirmar el namespace +# assert 'xmlns="dian:gov:co:facturaelectronica:NominaIndividual"' in xml.tostring() + +# def test_asignar_pago(): +# nomina = fe.nomina.DIANNominaIndividual() +# nomina.asignar_pago(fe.nomina.Pago( +# forma = fe.nomina.FormaPago(code='1'), +# metodo = fe.nomina.MetodoPago(code='1') +# )) + +# def test_nomina_xmlsign(monkeypatch): +# nomina = fe.nomina.DIANNominaIndividual() +# xml = nomina.toFachoXML() + +# signer = fe.nomina.DianXMLExtensionSigner('./tests/example.p12') +# xml.add_extension(signer) + +# print(xml.tostring()) +# elem = xml.get_element('/nomina:NominaIndividual/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature') +# assert elem is not None + + + +# def test_nomina_devengado_horas_extras_nocturnas(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnas( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENs/HEN', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' + +# def test_nomina_devengado_horas_recargo_nocturno(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturno( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNs/HRN', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' + +# def test_nomina_devengado_horas_extras_diarias_dominicales_y_festivos(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiariasDominicalesYFestivos( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDDFs/HEDDF', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' + +# def test_nomina_devengado_horas_recargo_diarias_dominicales_y_festivos(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoDiariasDominicalesYFestivos( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRDDFs/HRDDF', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' + + +# def test_nomina_devengado_horas_extras_nocturnas_dominicales_y_festivos(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasNocturnasDominicalesYFestivos( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HENDFs/HENDF', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' + +# def test_nomina_devengado_horas_recargo_nocturno_dominicales_y_festivos(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasRecargoNocturnoDominicalesYFestivos( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element('/nomina:NominaIndividual/Devengados/HRNDFs/HRNDF', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' + +# def test_fecha_validacion(): +# with pytest.raises(ValueError) as e: +# fe.nomina.Fecha('535-35-3') diff --git a/tests/test_nomina_ajuste.py b/tests/test_nomina_ajuste.py index 4c2efbe..a20c159 100644 --- a/tests/test_nomina_ajuste.py +++ b/tests/test_nomina_ajuste.py @@ -4,232 +4,233 @@ # this repository contains the full copyright notices and license terms. """Tests for `facho` package.""" -import re - -import pytest - -from facho import fe - -import helpers - -def atest_nomina_ajuste_reemplazar(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() - - xml = nomina.toFachoXML() - print(xml) - assert False - -def test_nomina_ajuste_reemplazar_asignacion_tipo_xml(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() - nomina.asignar_metadata(fe.nomina.Metadata( - novedad=fe.nomina.Novedad( - activa = True, - cune = "N0111" - ), - secuencia=fe.nomina.NumeroSecuencia( - prefijo = 'N', - consecutivo='00001' - ), - lugar_generacion=fe.nomina.Lugar( - pais = fe.nomina.Pais( - code = 'CO' - ), - departamento = fe.nomina.Departamento( - code = '05' - ), - municipio = fe.nomina.Municipio( - code = '05001' - ), - ), - proveedor=fe.nomina.Proveedor( - nit='999999', - dv=2, - software_id='xx', - software_pin='12', - razon_social='facho' - ) - )) - nomina.asignar_empleador(fe.nomina.Empleador( - razon_social='facho', - nit = '700085371', - dv = '1', - pais = fe.nomina.Pais( - code = 'CO' - ), - departamento = fe.nomina.Departamento( - code = '05' - ), - municipio = fe.nomina.Municipio( - code = '05001' - ), - direccion = 'calle etrivial' - )) - - nomina.asignar_trabajador(fe.nomina.Trabajador( - tipo_contrato = fe.nomina.TipoContrato( - code = '1' - ), - alto_riesgo = False, - tipo_documento = fe.nomina.TipoDocumento( - code = '11' - ), - primer_apellido = 'gnu', - segundo_apellido = 'emacs', - primer_nombre = 'facho', - lugar_trabajo = fe.nomina.LugarTrabajo( - pais = fe.nomina.Pais(code='CO'), - departamento = fe.nomina.Departamento(code='05'), - municipio = fe.nomina.Municipio(code='05001'), - direccion = 'calle facho' - ), - numero_documento = '800199436', - tipo = fe.nomina.TipoTrabajador( - code = '01' - ), - salario_integral = True, - sueldo = fe.nomina.Amount(1_500_000) - )) - nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( - fecha_generacion = '2020-01-16', - hora_generacion = '1053:10-05:00', - tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, - software_pin = '693', - tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_AJUSTES, - periodo_nomina = fe.nomina.PeriodoNomina(code='1'), - tipo_moneda = fe.nomina.TipoMoneda(code='COP') - )) - - xml = nomina.toFachoXML() - - assert xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/InformacionGeneral', 'TipoXML') == '103' - - -def test_adicionar_reemplazar_devengado_comprobante_total(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() - - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 60, - sueldo_trabajado = fe.nomina.Amount(2_000_000) - )) - - nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( - porcentaje = fe.nomina.Amount(19), - deduccion = fe.nomina.Amount(1_000_000) - )) - - xml = nomina.toFachoXML() - - assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ComprobanteTotal') == '1000000.00' - - -def test_adicionar_reemplazar_asignar_predecesor(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() - - nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor( - numero = '123456', - cune = 'ABC123456', - fecha_generacion = '2021-11-16' - )) - - xml = nomina.toFachoXML() - print(xml.tostring()) - assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@NumeroPred') == '123456' - assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456' - assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16' - - -def test_adicionar_reemplazar_eliminar_predecesor_opcional(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() - - nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor( - numero = '123456', - cune = 'ABC123456', - fecha_generacion = '2021-11-16' - )) - - xml = nomina.toFachoXML() - print(xml.tostring()) - - assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None - assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is None - -def test_adicionar_eliminar_reemplazar_predecesor_opcional(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() - - nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor( - numero = '123456', - cune = 'ABC123456', - fecha_generacion = '2021-11-16' - )) - - xml = nomina.toFachoXML() - print(xml.tostring()) - assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None - assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is None - -def test_adicionar_eliminar_devengado_comprobante_total(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() - - nomina.adicionar_devengado(fe.nomina.DevengadoBasico( - dias_trabajados = 60, - sueldo_trabajado = fe.nomina.Amount(2_000_000) - )) - - nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( - porcentaje = fe.nomina.Amount(19), - deduccion = fe.nomina.Amount(1_000_000) - )) - - xml = nomina.toFachoXML() - - assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/ComprobanteTotal') == '1000000.00' - -def test_adicionar_eliminar_asignar_predecesor(): - nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() - - nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor( - numero = '123456', - cune = 'ABC123456', - fecha_generacion = '2021-11-16' - )) - - xml = nomina.toFachoXML() - print(xml.tostring()) - assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@NumeroPred') == '123456' - assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456' - assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16' - -def test_nomina_devengado_horas_extras_diarias(): - nomina = fe.nomina.DIANNominaIndividual() - - nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiarias( - horas_extras=[ - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T19:09:55', - hora_fin='2021-11-30T20:09:55', - cantidad=1, - porcentaje=fe.nomina.Amount(1), - pago=fe.nomina.Amount(100) - ), - fe.nomina.DevengadoHoraExtra( - hora_inicio='2021-11-30T18:09:55', - hora_fin='2021-11-30T19:09:55', - cantidad=2, - porcentaje=fe.nomina.Amount(2), - pago=fe.nomina.Amount(200) - ) - ] - )) - - xml = nomina.toFachoXML() - extras = xml.get_element('/nomina:NominaIndividual/Devengados/HEDs/HED', multiple=True) - assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' - assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' - assert extras[0].get('Cantidad') == '1' - assert extras[0].get('Porcentaje') == '1.00' - assert extras[0].get('Pago') == '100.00' - assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' - assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' - assert extras[1].get('Cantidad') == '2' - assert extras[1].get('Porcentaje') == '2.00' - assert extras[1].get('Pago') == '200.00' +# import re + +# import pytest + +# from facho import fe + +# import helpers + +# def atest_nomina_ajuste_reemplazar(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() + +# xml = nomina.toFachoXML() +# print(xml) +# assert False + +# def test_nomina_ajuste_reemplazar_asignacion_tipo_xml(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() +# nomina.asignar_metadata(fe.nomina.Metadata( +# novedad=fe.nomina.Novedad( +# activa = True, +# cune = "N0111" +# ), +# secuencia=fe.nomina.NumeroSecuencia( +# prefijo = 'N', +# consecutivo='00001' +# ), +# lugar_generacion=fe.nomina.Lugar( +# pais = fe.nomina.Pais( +# code = 'CO' +# ), +# departamento = fe.nomina.Departamento( +# code = '05' +# ), +# municipio = fe.nomina.Municipio( +# code = '05001' +# ), +# ), +# proveedor=fe.nomina.Proveedor( +# nit='999999', +# dv=2, +# software_id='xx', +# software_pin='12', +# razon_social='facho' +# ) +# )) +# nomina.asignar_empleador(fe.nomina.Empleador( +# razon_social='facho', +# nit = '700085371', +# dv = '1', +# pais = fe.nomina.Pais( +# code = 'CO' +# ), +# departamento = fe.nomina.Departamento( +# code = '05' +# ), +# municipio = fe.nomina.Municipio( +# code = '05001' +# ), +# direccion = 'calle etrivial' +# )) + +# nomina.asignar_trabajador(fe.nomina.Trabajador( +# tipo_contrato = fe.nomina.TipoContrato( +# code = '1' +# ), +# alto_riesgo = False, +# tipo_documento = fe.nomina.TipoDocumento( +# code = '11' +# ), +# primer_apellido = 'gnu', +# segundo_apellido = 'emacs', +# primer_nombre = 'facho', +# lugar_trabajo = fe.nomina.LugarTrabajo( +# pais = fe.nomina.Pais(code='CO'), +# departamento = fe.nomina.Departamento(code='05'), +# municipio = fe.nomina.Municipio(code='05001'), +# direccion = 'calle facho' +# ), +# numero_documento = '800199436', +# tipo = fe.nomina.TipoTrabajador( +# code = '01' +# ), +# salario_integral = True, +# sueldo = fe.nomina.Amount(1_500_000) +# )) +# nomina.asignar_informacion_general(fe.nomina.InformacionGeneral( +# fecha_generacion = '2020-01-16', +# hora_generacion = '1053:10-05:00', +# tipo_ambiente = fe.nomina.InformacionGeneral.AMBIENTE_PRODUCCION, +# software_pin = '693', +# tipo_xml = fe.nomina.InformacionGeneral.TIPO_XML_AJUSTES, +# periodo_nomina = fe.nomina.PeriodoNomina(code='1'), +# tipo_moneda = fe.nomina.TipoMoneda(code='COP') +# )) + +# xml = nomina.toFachoXML() + +# assert xml.get_element_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/InformacionGeneral', 'TipoXML') == '103' + + +# def test_adicionar_reemplazar_devengado_comprobante_total(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() + +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 60, +# sueldo_trabajado = fe.nomina.Amount(2_000_000) +# )) + +# nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( +# porcentaje = fe.nomina.Amount(19), +# deduccion = fe.nomina.Amount(1_000_000) +# )) + +# xml = nomina.toFachoXML() + +# assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ComprobanteTotal') == '1000000.00' + + +# def test_adicionar_reemplazar_asignar_predecesor(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() + +# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor( +# numero = '123456', +# cune = 'ABC123456', +# fecha_generacion = '2021-11-16' +# )) + +# xml = nomina.toFachoXML() +# print(xml.tostring()) +# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@NumeroPred') == '123456' +# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@CUNEPred') == 'ABC123456' +# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor/@FechaGenPred') == '2021-11-16' + + +# def test_adicionar_reemplazar_eliminar_predecesor_opcional(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar() + +# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Reemplazar.Predecesor( +# numero = '123456', +# cune = 'ABC123456', +# fecha_generacion = '2021-11-16' +# )) + +# xml = nomina.toFachoXML() +# print(xml.tostring()) + +# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is not None +# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is None + +# def test_adicionar_eliminar_reemplazar_predecesor_opcional(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() + +# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor( +# numero = '123456', +# cune = 'ABC123456', +# fecha_generacion = '2021-11-16' +# )) + +# xml = nomina.toFachoXML() +# print(xml.tostring()) +# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor') is not None +# assert xml.get_element('/nominaajuste:NominaIndividualDeAjuste/Reemplazar/ReemplazandoPredecesor') is None + +# def test_adicionar_eliminar_devengado_comprobante_total(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() + +# nomina.adicionar_devengado(fe.nomina.DevengadoBasico( +# dias_trabajados = 60, +# sueldo_trabajado = fe.nomina.Amount(2_000_000) +# )) + +# nomina.adicionar_deduccion(fe.nomina.DeduccionSalud( +# porcentaje = fe.nomina.Amount(19), +# deduccion = fe.nomina.Amount(1_000_000) +# )) + +# xml = nomina.toFachoXML() + +# assert xml.get_element_text('/nominaajuste:NominaIndividualDeAjuste/Eliminar/ComprobanteTotal') == '1000000.00' + +# def test_adicionar_eliminar_asignar_predecesor(): +# nomina = fe.nomina.DIANNominaIndividualDeAjuste.Eliminar() + +# nomina.asignar_predecesor(fe.nomina.DIANNominaIndividualDeAjuste.Eliminar.Predecesor( +# numero = '123456', +# cune = 'ABC123456', +# fecha_generacion = '2021-11-16' +# )) + +# xml = nomina.toFachoXML() +# print(xml.tostring()) +# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@NumeroPred') == '123456' +# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@CUNEPred') == 'ABC123456' +# assert xml.get_element_text_or_attribute('/nominaajuste:NominaIndividualDeAjuste/Eliminar/EliminandoPredecesor/@FechaGenPred') == '2021-11-16' + +# def test_nomina_devengado_horas_extras_diarias(): +# nomina = fe.nomina.DIANNominaIndividual() + +# nomina.adicionar_devengado(fe.nomina.DevengadoHorasExtrasDiarias( +# horas_extras=[ +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T19:09:55', +# hora_fin='2021-11-30T20:09:55', +# cantidad=1, +# porcentaje=fe.nomina.Amount(1), +# pago=fe.nomina.Amount(100) +# ), +# fe.nomina.DevengadoHoraExtra( +# hora_inicio='2021-11-30T18:09:55', +# hora_fin='2021-11-30T19:09:55', +# cantidad=2, +# porcentaje=fe.nomina.Amount(2), +# pago=fe.nomina.Amount(200) +# ) +# ] +# )) + +# xml = nomina.toFachoXML() +# extras = xml.get_element( +# '/nomina:NominaIndividual/Devengados/HEDs/HED', multiple=True) +# assert extras[0].get('HoraInicio') == '2021-11-30T19:09:55' +# assert extras[0].get('HoraFin') == '2021-11-30T20:09:55' +# assert extras[0].get('Cantidad') == '1' +# assert extras[0].get('Porcentaje') == '1.00' +# assert extras[0].get('Pago') == '100.00' +# assert extras[1].get('HoraInicio') == '2021-11-30T18:09:55' +# assert extras[1].get('HoraFin') == '2021-11-30T19:09:55' +# assert extras[1].get('Cantidad') == '2' +# assert extras[1].get('Porcentaje') == '2.00' +# assert extras[1].get('Pago') == '200.00' diff --git a/tests/test_query.py b/tests/test_query.py index 0faf899..2962977 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -3,22 +3,28 @@ # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -import pytest +# import pytest import facho.fe.form as form from facho import fe -from facho.fe.form_xml import DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML -from fixtures import * +from facho.fe.form_xml import DIANInvoiceXML +# from facho.fe.form_xml import ( +# DIANInvoiceXML, DIANCreditNoteXML, DIANDebitNoteXML) + +from fixtures import simple_invoice from facho.fe.form import query +simple_invoice = simple_invoice + + def test_query_billing_reference(simple_invoice): xml = DIANInvoiceXML(simple_invoice) cufe_extension = fe.DianXMLExtensionCUFE(simple_invoice) xml.add_extension(cufe_extension) out = xml.tostring() - + reference = query.billing_reference(out, form.BillingReference) assert isinstance(reference, form.BillingReference) assert reference.ident != '' diff --git a/tox.ini b/tox.ini index dab4b8a..f06bea6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,17 @@ [tox] -envlist = py37, py38, py39, py310 +envlist = py39, py310, py311, py312, flake8 [travis] python = - 3.7: py37 - 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 facho [testenv] setenv =