diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4200623..d1c43c3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -57,6 +57,17 @@ 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 . @@ -94,13 +105,6 @@ 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 ee5d265..1cc3feb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,23 @@ FROM ubuntu:18.04 RUN apt-get -qq update + +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 \ wget \ ca-certificates RUN wget https://bootstrap.pypa.io/get-pip.py \ - && python3 get-pip.py pip==21.3 \ - && python3.7 get-pip.py pip==21.3 \ - && python3.8 get-pip.py pip==21.3 \ + && 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 \ && rm get-pip.py RUN apt-get install -y --no-install-recommends \ @@ -20,12 +27,14 @@ RUN apt-get install -y --no-install-recommends \ build-essential \ zip -RUN python3.6 --version RUN python3.7 --version RUN python3.8 --version +RUN python3.9 --version +RUN python3.10 --version -RUN pip3.6 install setuptools setuptools-rust 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 install tox pytest diff --git a/docs/DIAN/Anexo_tecnico_vr18_09022021.pdf b/docs/DIAN/Anexo_tecnico_vr18_09022021.pdf new file mode 100644 index 0000000..244dc2d Binary files /dev/null and b/docs/DIAN/Anexo_tecnico_vr18_09022021.pdf 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 new file mode 100644 index 0000000..961dc9d Binary files /dev/null and b/docs/DIAN/Caja_de_herramientas_Factura_Electronica_Validacion_Previa-09-02-2021.zip differ diff --git a/experimental/facho-signer/configure.ac b/experimental/facho-signer/configure.ac index da9e2b6..037ce25 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.71]) +AC_PREREQ([2.69]) AC_INIT([facho-signer], [0.0.1], [bit4bit@riseup.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([src/facho_signer.c]) diff --git a/facho/facho.py b/facho/facho.py index c88c83e..27d31e3 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -181,8 +181,6 @@ class FachoXML: return etree.QName(self.root).namespace 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): diff --git a/facho/fe/client/dian.py b/facho/fe/client/dian.py index 3da2cbf..732108e 100644 --- a/facho/fe/client/dian.py +++ b/facho/fe/client/dian.py @@ -21,10 +21,10 @@ __all__ = ['DianClient', class SOAPService: - def get_wsdl(self): + def wsdl(self): raise NotImplementedError() - def get_service(self): + def service(self): raise NotImplementedError() def build_response(self, as_dict): @@ -63,10 +63,10 @@ class GetNumberingRange(SOAPService): accountCodeT: str softwareCode: str - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def service(self): return 'GetNumberingRange' def build_response(self, as_dict): @@ -78,10 +78,10 @@ class SendBillAsync(SOAPService): fileName: str contentFile: str - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def service(self): return 'SendBillAsync' def build_response(self, as_dict): @@ -106,10 +106,10 @@ class SendTestSetAsync(SOAPService): contentFile: str testSetId: str = '' - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def service(self): return 'SendTestSetAsync' def build_response(self, as_dict): @@ -120,10 +120,10 @@ class SendBillSync(SOAPService): fileName: str contentFile: bytes - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def service(self): return 'SendBillSync' def build_response(self, as_dict): @@ -153,10 +153,10 @@ class GetStatusResponse: class GetStatus(SOAPService): trackId: bytes - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def service(self): return 'GetStatus' def build_response(self, as_dict): @@ -166,10 +166,10 @@ class GetStatus(SOAPService): class GetStatusZip(SOAPService): trackId: bytes - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def service(self): return 'GetStatusZip' def build_response(self, as_dict): @@ -179,10 +179,10 @@ class GetStatusZip(SOAPService): class SendNominaSync(SOAPService): contentFile: bytes - def get_wsdl(self): + def wsdl(self): return 'https://vpfe.dian.gov.co/WcfDianCustomerServices.svc?wsdl' - def get_service(self): + def 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 get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class SendBillAsync(SendBillAsync): - def get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class SendBillSync(SendBillSync): - def get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class SendTestSetAsync(SendTestSetAsync): - def get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class GetStatus(GetStatus): - def get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class GetStatusZip(GetStatusZip): - def get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class SendNominaSync(SendNominaSync): - def get_wsdl(self): + def wsdl(self): return Habilitacion.WSDL class DianGateway: @@ -226,7 +226,7 @@ class DianGateway: raise NotImplementedError() def _remote_service(self, conn, service): - return conn.service[service.get_service()] + return conn.service[service.service()] def _close(self, conn): return @@ -250,7 +250,7 @@ class DianClient(DianGateway): self._password = password def _open(self, service): - return zeep.Client(service.get_wsdl(), wsse=UsernameToken(self._username, self._password)) + return zeep.Client(service.wsdl(), wsse=UsernameToken(self._username, self._password)) class DianSignatureClient(DianGateway): @@ -264,7 +264,7 @@ class DianSignatureClient(DianGateway): # RESOLUCCION 0004: pagina 756 from zeep.wsse import utils - client = zeep.Client(service.get_wsdl(), wsse= + client = zeep.Client(service.wsdl(), wsse= BinarySignature( self.private_key_path, self.public_key_path, self.password, signature_method=xmlsec.Transform.RSA_SHA256, diff --git a/facho/fe/fe.py b/facho/fe/fe.py index 0e15c53..23f5448 100644 --- a/facho/fe/fe.py +++ b/facho/fe/fe.py @@ -122,8 +122,7 @@ class DianXMLExtensionCUDFE(FachoXMLExtension): fachoxml.set_element('./cbc:UUID', cufe, schemeID=self.tipo_ambiente, schemeName=self.schemeName()) - #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', diff --git a/facho/fe/form_xml/credit_note.py b/facho/fe/form_xml/credit_note.py index 89820c0..be887d4 100644 --- a/facho/fe/form_xml/credit_note.py +++ b/facho/fe/form_xml/credit_note.py @@ -18,3 +18,6 @@ class DIANCreditNoteXML(DIANInvoiceXML): 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 1ba42b6..4fe5838 100644 --- a/facho/fe/form_xml/debit_note.py +++ b/facho/fe/form_xml/debit_note.py @@ -13,6 +13,9 @@ 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' diff --git a/facho/fe/form_xml/invoice.py b/facho/fe/form_xml/invoice.py index 42b277a..52d8051 100644 --- a/facho/fe/form_xml/invoice.py +++ b/facho/fe/form_xml/invoice.py @@ -21,6 +21,7 @@ class DIANInvoiceXML(fe.FeXML): 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') @@ -415,7 +416,6 @@ class DIANInvoiceXML(fe.FeXML): 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) @@ -439,6 +439,7 @@ class DIANInvoiceXML(fe.FeXML): 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 @@ -453,10 +454,9 @@ class DIANInvoiceXML(fe.FeXML): 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) + line = fexml.fragment('./cac:TaxTotal', append=True) #DIAN 1.7.-2020: FAU06 tax_amount = amount_of['tax_amount'] fexml.set_element_amount_for(line, @@ -486,7 +486,7 @@ class DIANInvoiceXML(fe.FeXML): 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']) # abstract method def tag_document(fexml): @@ -566,7 +566,11 @@ class DIANInvoiceXML(fe.FeXML): 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') + def attach_invoice(fexml, invoice): """adiciona etiquetas a FEXML y retorna FEXML en caso de fallar validacion retorna None""" diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..e96da27 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,17 @@ +attrs==22.1.0 +distlib==0.3.6 +filelock==3.8.0 +iniconfig==1.1.1 +packaging==21.3 +platformdirs==2.5.2 +pluggy==1.0.0 +py==1.11.0 +pyparsing==3.0.9 +pytest==7.1.3 +semantic-version==2.10.0 +setuptools-rust==1.5.2 +six==1.16.0 +tomli==2.0.1 +tox==3.26.0 +typing_extensions==4.4.0 +virtualenv==20.16.5 diff --git a/tests/test_fe_form.py b/tests/test_fe_form.py index ca7f4b9..f5f6683 100644 --- a/tests/test_fe_form.py +++ b/tests/test_fe_form.py @@ -33,7 +33,24 @@ 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) diff --git a/tox.ini b/tox.ini index f7186a0..dab4b8a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,12 @@ [tox] -envlist = py27, py34, py35, py36, flake8 +envlist = py37, py38, py39, py310 [travis] python = - 3.6: py36 - 3.5: py35 - 3.4: py34 - 2.7: py27 - -[testenv:flake8] -basepython = python -deps = flake8 -commands = flake8 facho + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 [testenv] setenv =