diff --git a/facho/facho.py b/facho/facho.py index 24f907f..b1bc2f8 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -257,6 +257,9 @@ class FachoXML: def get_element_text(self, xpath, format_=str): xpath = self.fragment_prefix + self._path_xpath_for(xpath) elem = self.builder.xpath(self.root, xpath) + if elem is None: + raise ValueError('xpath %s invalid' % (xpath)) + text = self.builder.get_text(elem) return format_(text) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 5a1c546..5bc5be3 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -227,15 +227,18 @@ class LegalMonetaryTotal(model.Model): self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount +class DIANExtensionContent(model.Model): + __name__ = 'ExtensionContent' + + dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts') + class DIANExtension(model.Model): __name__ = 'UBLExtension' - _content = fields.Many2One(Element, name='ExtensionContent', namespace='ext') - - dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts') + content = fields.Many2One(DIANExtensionContent, namespace='ext') def __default_get__(self, name, value): - return self.dian + return self.content.dian class UBLExtension(model.Model): __name__ = 'UBLExtension' @@ -250,16 +253,7 @@ class UBLExtensions(model.Model): class Invoice(model.Model): __name__ = 'Invoice' - __namespace__ = { - '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', - 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', - 'sts': 'http://www.dian.gov.co/contratos/facturaelectronica/v1/Structures', - 'xades': 'http://uri.etsi.org/01903/v1.3.2#', - 'ds': 'http://www.w3.org/2000/09/xmldsig#' - } - + __namespace__ = fe.NAMESPACES _ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext') dian = fields.Virtual(getter='get_dian_extension') @@ -284,6 +278,7 @@ class Invoice(model.Model): taxtotal_03 = fields.Many2One(TaxTotal) def __setup__(self): + self._namespace_prefix = 'fe' # Se requieren minimo estos impuestos para # validar el cufe self._subtotal_01 = self.taxtotal_01.subtotals.create() @@ -351,3 +346,10 @@ class Invoice(model.Model): def get_dian_extension(self, name, _value): return self._ubl_extensions.dian + + def to_xml(self, **kw): + # al generar documento el namespace + # se hace respecto a la raiz + return super().to_xml(**kw)\ + .replace("fe:", "")\ + .replace("xmlns:fe", "xmlns") diff --git a/facho/fe/model/dian.py b/facho/fe/model/dian.py index 721f5ee..dbbe3c2 100644 --- a/facho/fe/model/dian.py +++ b/facho/fe/model/dian.py @@ -2,6 +2,19 @@ import facho.model as model import facho.model.fields as fields from .common import * +class DIANElement(Element): + """ + Elemento que contiene atributos por defecto. + + Puede extender esta clase y modificar los atributos nuevamente + """ + __name__ = 'DIANElement' + + scheme_id = fields.Attribute('schemeID', default='4') + scheme_name = fields.Attribute('schemeName', default='31') + scheme_agency_name = fields.Attribute('schemeAgencyName', default='CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)') + scheme_agency_id = fields.Attribute('schemeAgencyID', default='195') + class SoftwareProvider(model.Model): __name__ = 'SoftwareProvider' @@ -26,10 +39,18 @@ class InvoiceControl(model.Model): authorization = fields.Many2One(Element, name='InvoiceAuthorization', namespace='sts') period = fields.Many2One(Period, name='AuthorizationPeriod', namespace='sts') invoices = fields.Many2One(AuthorizedInvoices, namespace='sts') + +class AuthorizationProvider(model.Model): + __name__ = 'AuthorizationProvider' + + + id = fields.Many2One(DIANElement, name='AuthorizationProviderID', namespace='sts', default='800197268') class DianExtensions(model.Model): __name__ = 'DianExtensions' + authorization_provider = fields.Many2One(AuthorizationProvider, namespace='sts', create=True) + software_security_code = fields.Many2One(Element, name='SoftwareSecurityCode', namespace='sts') software_provider = fields.Many2One(SoftwareProvider, namespace='sts') source = fields.Many2One(InvoiceSource, namespace='sts') diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 01d94cc..9b156b3 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -13,7 +13,9 @@ import facho.fe.model as model import facho.fe.form as form from facho import fe -def test_simple_invoice(): +import helpers + +def simple_invoice(): invoice = model.Invoice() invoice.dian.software_security_code = '12345' invoice.dian.software_provider.provider_id = 'provider-id' @@ -21,25 +23,6 @@ def test_simple_invoice(): invoice.dian.control.prefix = 'SETP' invoice.dian.control.from_range = '1000' invoice.dian.control.to_range = '1000' - - invoice.id = '323200000129' - invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') - invoice.supplier.party.id = '700085371' - invoice.customer.party.id = '800199436' - - line = invoice.lines.create() - line.quantity = 1 - line.price = form.Amount(5_000) - subtotal = line.taxtotal.subtotals.create() - subtotal.percent = 19.0 - assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:0070008537180019943610.00.00.019.019.05000.05000.05000.00.00.00.00.019.019.0010.00.00.0040.00.00.003' == invoice.to_xml() - - -def test_simple_invoice_cufe(): - token = '693ff6f2a553c3646a063436fd4dd9ded0311471' - environment = fe.AMBIENTE_PRODUCCION - - invoice = model.Invoice() invoice.id = '323200000129' invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') invoice.supplier.party.id = '700085371' @@ -54,4 +37,34 @@ def test_simple_invoice_cufe(): line.quantity = 1 line.price = 1_500_000 + return invoice + +def test_simple_invoice_cufe(): + token = '693ff6f2a553c3646a063436fd4dd9ded0311471' + environment = fe.AMBIENTE_PRODUCCION + invoice = simple_invoice() assert invoice.cufe(token, environment) == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' + +def test_simple_invoice_sign_dian(monkeypatch): + invoice = simple_invoice() + + xmlstring = invoice.to_xml() + p12_data = open('./tests/example.p12', 'rb').read() + signer = fe.DianXMLExtensionSigner.from_bytes(p12_data) + + with monkeypatch.context() as m: + helpers.mock_urlopen(m) + xmlsigned = signer.sign_xml_string(xmlstring) + assert "Signature" in xmlsigned + + +def test_dian_extension_authorization_provider(): + invoice = simple_invoice() + xml = fe.FeXML.from_string(invoice.to_xml()) + provider_id = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') + + assert provider_id.attrib['schemeID'] == '4' + assert provider_id.attrib['schemeName'] == '31' + assert provider_id.attrib['schemeAgencyName'] == 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)' + assert provider_id.attrib['schemeAgencyID'] == '195' + assert provider_id.text == '800197268'