diff --git a/facho/fe/__init__.py b/facho/fe/__init__.py index 7e4692e..590426f 100644 --- a/facho/fe/__init__.py +++ b/facho/fe/__init__.py @@ -3,8 +3,11 @@ from .fe import NAMESPACES from .fe import DianXMLExtensionSigner from .fe import DianXMLExtensionSoftwareSecurityCode from .fe import DianXMLExtensionCUFE +from .fe import DianXMLExtensionCUDE from .fe import DianXMLExtensionInvoiceAuthorization from .fe import DianXMLExtensionSoftwareProvider from .fe import DianXMLExtensionAuthorizationProvider from .fe import DianZIP +from .fe import AMBIENTE_PRUEBAS +from .fe import AMBIENTE_PRODUCCION from . import form_xml diff --git a/facho/fe/fe.py b/facho/fe/fe.py index db16ff6..b711156 100644 --- a/facho/fe/fe.py +++ b/facho/fe/fe.py @@ -14,6 +14,10 @@ from contextlib import contextmanager from .data.dian import codelist from . import form +AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] +AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] + + SCHEME_AGENCY_ATTRS = { 'schemeAgencyName': 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)', 'schemeAgencyID': '195' @@ -76,20 +80,18 @@ class FeXML(FachoXML): .replace("xmlns:fe", "xmlns") -class DianXMLExtensionCUFE(FachoXMLExtension): - AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] - AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] - def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS, clave_tecnica = ''): +class DianXMLExtensionCUDFE(FachoXMLExtension): + + def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS): self.tipo_ambiente = tipo_ambiente - self.clave_tecnica = clave_tecnica self.invoice = invoice def _tipo_ambiente(self): return int(self.tipo_ambiente) def build(self, fachoxml): - cufe = self._generate_cufe(self.invoice, fachoxml) + cufe = self._generate_cufe(fachoxml) fachoxml.set_element('./cbc:UUID', cufe, schemeID=self.tipo_ambiente, schemeName='CUFE-SHA384') @@ -105,49 +107,38 @@ class DianXMLExtensionCUFE(FachoXMLExtension): def issue_date(self, datetime_): return datetime_.strftime('%Y-%m-%d') - def formatVars(self, invoice): - NumFac = invoice.invoice_ident - FecFac = self.issue_date(invoice.invoice_issue) - HoraFac = self.issue_time(invoice.invoice_issue) + def buildVars(self): + invoice = self.invoice + build_vars = {} + build_vars['NumFac'] = invoice.invoice_ident + build_vars['FecFac'] = self.issue_date(invoice.invoice_issue) + build_vars['HoraFac'] = self.issue_time(invoice.invoice_issue) # PAG 601 - ValorBruto = invoice.invoice_legal_monetary_total.line_extension_amount - ValorTotalPagar = invoice.invoice_legal_monetary_total.payable_amount + build_vars['ValorBruto'] = invoice.invoice_legal_monetary_total.line_extension_amount + build_vars['ValorTotalPagar'] = invoice.invoice_legal_monetary_total.payable_amount ValorImpuestoPara = {} - CodImpuesto1 = 1 - CodImpuesto2 = 4 - CodImpuesto3 = 3 + build_vars['CodImpuesto1'] = 1 + build_vars['CodImpuesto2'] = 4 + build_vars['CodImpuesto3'] = 3 for invoice_line in invoice.invoice_lines: for subtotal in invoice_line.tax.subtotals: # TODO cual es la naturaleza de tax_scheme_ident? codigo_impuesto = int(subtotal.tax_scheme_ident) ValorImpuestoPara.setdefault(codigo_impuesto, form.Amount(0.0)) ValorImpuestoPara[codigo_impuesto] += subtotal.tax_amount + build_vars['ValorImpuestoPara'] = ValorImpuestoPara + build_vars['NitOFE'] = invoice.invoice_supplier.ident + build_vars['NumAdq'] = invoice.invoice_customer.ident + build_vars['TipoAmb'] = self._tipo_ambiente() - NitOFE = invoice.invoice_supplier.ident - NumAdq = invoice.invoice_customer.ident - TipoAmb = self._tipo_ambiente() - ClTec = str(self.clave_tecnica) + return build_vars - return [ - '%s' % NumFac, - '%s' % FecFac, - '%s' % HoraFac, - '%.02f' % round(ValorBruto, 2), - '%02d' % CodImpuesto1, - '%.02f' % round(ValorImpuestoPara.get(CodImpuesto1, 0.0), 2), - '%02d' % CodImpuesto2, - '%.02f' % round(ValorImpuestoPara.get(CodImpuesto2, 0.0), 2), - '%02d' % CodImpuesto3, - '%.02f' % round(ValorImpuestoPara.get(CodImpuesto3, 0.0), 2), - '%.02f' % round(ValorTotalPagar, 2), - '%s' % NitOFE, - '%s' % NumAdq, - '%s' % ClTec, - '%d' % TipoAmb, - ] - def _generate_cufe(self, invoice, fachoxml): - formatVars = self.formatVars(invoice) + def formatVars(self, invoice): + raise NotImplementedError() + + def _generate_cufe(self, fachoxml): + formatVars = self.formatVars() cufe = "".join(formatVars) # crear hash... @@ -156,6 +147,74 @@ class DianXMLExtensionCUFE(FachoXMLExtension): return h.hexdigest() +class DianXMLExtensionCUFE(DianXMLExtensionCUDFE): + def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS, clave_tecnica = ''): + self.tipo_ambiente = tipo_ambiente + self.clave_tecnica = clave_tecnica + self.invoice = invoice + + def buildVars(self): + build_vars = super().buildVars() + build_vars['ClTec'] = str(self.clave_tecnica) + 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'], + '%.02f' % round(build_vars['ValorBruto'], 2), + '%02d' % CodImpuesto1, + '%.02f' % round(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0), 2), + '%02d' % CodImpuesto2, + '%.02f' % round(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0), 2), + '%02d' % CodImpuesto3, + '%.02f' % round(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0), 2), + '%.02f' % round(build_vars['ValorTotalPagar'], 2), + '%s' % build_vars['NitOFE'], + '%s' % build_vars['NumAdq'], + '%s' % build_vars['ClTec'], + '%d' % build_vars['TipoAmb'], + ] + +class DianXMLExtensionCUDE(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 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'], + '%.02f' % round(build_vars['ValorBruto'], 2), + '%02d' % CodImpuesto1, + '%.02f' % round(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0), 2), + '%02d' % CodImpuesto2, + '%.02f' % round(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0), 2), + '%02d' % CodImpuesto3, + '%.02f' % round(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0), 2), + '%.02f' % round(build_vars['ValorTotalPagar'], 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 @@ -300,7 +359,7 @@ class DianXMLExtensionInvoiceSource(FachoXMLExtension): listAgencyID="6", listAgencyName="United Nations Economic Commission for Europe", listSchemeURI="urn:oasis:names:specification:ubl:codelist:gc:CountryIdentificationCode-2.1") - + class DianXMLExtensionInvoiceAuthorization(FachoXMLExtension): # RESOLUCION 0004: pagina 106 diff --git a/facho/fe/form.py b/facho/fe/form.py index e7f3651..dab1fda 100644 --- a/facho/fe/form.py +++ b/facho/fe/form.py @@ -470,6 +470,8 @@ class NationalSalesInvoice(Invoice): class CreditNote(Invoice): def __init__(self, invoice_document_reference: BillingReference): + super().__init__(CreditNoteDocumentType()) + if not isinstance(invoice_document_reference, BillingReference): raise TypeError('invoice_document_reference invalid type') self.invoice_billing_reference = invoice_document_reference diff --git a/tests/test_fe_form.py b/tests/test_fe_form.py index 6bf2fbc..060ced3 100644 --- a/tests/test_fe_form.py +++ b/tests/test_fe_form.py @@ -12,7 +12,39 @@ import zipfile import facho.fe.form as form from facho import fe -from facho.fe.form_xml import DIANInvoiceXML +from facho.fe.form_xml import DIANInvoiceXML, DIANCreditNoteXML + +@pytest.fixture +def simple_credit_note_without_lines(): + inv = form.CreditNote(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('10') + 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( + '', '', 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( + '', '', form.City('05001', 'Medellín'), + form.Country('CO', 'Colombia'), + form.CountrySubentity('05', 'Antioquia')) + )) + return inv @pytest.fixture def simple_invoice_without_lines(): @@ -207,10 +239,10 @@ def test_invoice_cufe(simple_invoice_without_lines): cufe_extension = fe.DianXMLExtensionCUFE( simple_invoice, - tipo_ambiente = fe.DianXMLExtensionCUFE.AMBIENTE_PRODUCCION, + tipo_ambiente = fe.AMBIENTE_PRODUCCION, clave_tecnica = '693ff6f2a553c3646a063436fd4dd9ded0311471' ) - formatVars = cufe_extension.formatVars(simple_invoice) + formatVars = cufe_extension.formatVars() #NumFac assert formatVars[0] == '323200000129', "NumFac" #FecFac @@ -246,3 +278,37 @@ def test_invoice_cufe(simple_invoice_without_lines): cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID') # RESOLUCION 004: pagina 689 assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' + + +def test_credit_note_cude(simple_credit_note_without_lines): + simple_invoice = simple_credit_note_without_lines + simple_invoice.invoice_ident = '8110007871' + 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 = 1, + description = 'producto', + item = form.StandardItem('test', 111), + price = form.Price(form.Amount(5_000), '01', ''), + tax = form.TaxTotal( + subtotals = [ + form.TaxSubTotal( + tax_scheme_ident = '01', + percent = 19.0 + )]) + )) + + simple_invoice.calculate() + xml_invoice = DIANCreditNoteXML(simple_invoice) + + cude_extension = fe.DianXMLExtensionCUDE( + simple_invoice, + '12301', + 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'