diff --git a/facho/fe/form.py b/facho/fe/form.py index acff0e5..c15260b 100644 --- a/facho/fe/form.py +++ b/facho/fe/form.py @@ -54,6 +54,7 @@ class TaxTotal: @dataclass class InvoiceLine: + # RESOLUCION 0004: pagina 155 quantity: int description: str item_ident: int @@ -66,11 +67,11 @@ class InvoiceLine: @property def total_tax_inclusive_amount(self): - return self.tax.taxable_amount + return self.tax.taxable_amount + self.tax.tax_amount @property def total_tax_exclusive_amount(self): - return self.tax.tax_amount + return self.tax.taxable_amount def calculate(self): self.tax.calculate(self) @@ -125,17 +126,18 @@ class Invoice: 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_amount + self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount + self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount self.invoice_legal_monetary_total.charge_total_amount += invline.total_amount - - self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_exclusive_amount \ - + self.invoice_legal_monetary_total.line_extension_amount \ - + self.invoice_legal_monetary_total.tax_inclusive_amount - + #self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_exclusive_amount \ + # + self.invoice_legal_monetary_total.line_extension_amount \ + # + self.invoice_legal_monetary_total.tax_inclusive_amount + self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_inclusive_amount + def calculate(self): - self._calculate_legal_monetary_total() for invline in self.invoice_lines: invline.calculate() + self._calculate_legal_monetary_total() class DianResolucion0001Validator: @@ -168,24 +170,32 @@ class DianResolucion0001Validator: class DIANInvoiceXML(fe.FeXML): - - def __init__(self, invoice, TipoAmbiente = 'Pruebas'): + AMBIENTE_PRUEBAS = 'Pruebas' + AMBIENTE_PRODUCCION = 'Producción' + + def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS, clave_tecnica = ''): super().__init__('Invoice', 'http://www.dian.gov.co/contratos/facturaelectronica/v1') - self.attach_invoice(invoice, TipoAmbiente) - - def attach_invoice(self, invoice, TipoAmbiente): + self.tipo_ambiente = tipo_ambiente + self.clave_tecnica = clave_tecnica + self.attach_invoice(invoice) + + def _tipo_ambiente(self): + return int(dian.TipoAmbiente[self.tipo_ambiente]['code']) + + def attach_invoice(self, invoice): """adiciona etiquetas a FEXML y retorna FEXML en caso de fallar validacion retorna None""" fexml = self invoice.calculate() - cufe = self._generate_cufe(invoice, TipoAmbiente) + cufe = self._generate_cufe(invoice) + fexml.set_element('/fe:Invoice/cbc:ProfileExecutionID', self._tipo_ambiente()) fexml.set_element('/fe:Invoice/cbc:ID', invoice.invoice_ident) fexml.set_element('/fe:Invoice/cbc:UUID[schemaName="CUFE-SHA384"]', cufe) - fexml.set_element('/fe:Invoice/cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d')) - fexml.set_element('/fe:Invoice/cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S')) + fexml.set_element('/fe:Invoice/cbc:IssueDate', self.issue_date(invoice.invoice_issue)) + fexml.set_element('/fe:Invoice/cbc:IssueTime', self.issue_time(invoice.invoice_issue)) fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:StartDate', invoice.invoice_period_start.strftime('%Y-%m-%d')) fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:EndDate', invoice.invoice_period_end.strftime('%Y-%m-%d')) @@ -193,6 +203,8 @@ class DIANInvoiceXML(fe.FeXML): fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyIdentification/cbc:ID', invoice.invoice_supplier.ident) + fexml.set_element('/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', + invoice.invoice_supplier.ident) fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PartyTaxScheme/cbc:TaxLevelCode', invoice.invoice_supplier.responsability_code) fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/cbc:AdditionalAccountID', @@ -211,6 +223,8 @@ class DIANInvoiceXML(fe.FeXML): invoice.invoice_customer.organization_code) fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyName/cbc:Name', invoice.invoice_customer.name) + fexml.set_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', + invoice.invoice_customer.ident) fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PartyLegalEntity/cbc:RegistrationName', invoice.invoice_customer.legal_name) fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line', @@ -232,6 +246,7 @@ class DIANInvoiceXML(fe.FeXML): invoice.invoice_legal_monetary_total.payable_amount, currencyID='COP') + fexml.set_element('/fe:Invoice/cbc:LineCountNumeric', len(invoice.invoice_lines)) next_append = False for index, invoice_line in enumerate(invoice.invoice_lines): line = fexml.fragment('/fe:Invoice/fe:InvoiceLine', append=next_append) @@ -243,13 +258,18 @@ class DIANInvoiceXML(fe.FeXML): line.set_element('/fe:InvoiceLine/fe:Price/cbc:PriceAmount', invoice_line.price_amount, currencyID="COP") line.set_element('/fe:InvoiceLine/fe:Item/cbc:Description', invoice_line.description) + return fexml - - def _generate_cufe(self, invoice, TipoAmbiente = 'Pruebas'): + def issue_time(self, datetime_): + return datetime_.strftime('%H:%M:%S%z') + def issue_date(self, datetime_): + return datetime_.strftime('%Y-%m-%d') + + def _generate_cufe(self, invoice): NumFac = invoice.invoice_ident - FecFac = invoice.invoice_issue.strftime('%Y-%m-%d') - HoraFac = invoice.invoice_issue.strftime('%H:%H:%S') + FecFac = self.issue_date(invoice.invoice_issue) + HoraFac = self.issue_time(invoice.invoice_issue) ValorBruto = invoice.invoice_legal_monetary_total.line_extension_amount ValorTotalPagar = invoice.invoice_legal_monetary_total.payable_amount ValorImpuestoPara = {} @@ -268,25 +288,27 @@ class DIANInvoiceXML(fe.FeXML): NitOFE = invoice.invoice_supplier.ident NumAdq = invoice.invoice_customer.ident - TipoAmb = int(dian.TipoAmbiente[TipoAmbiente]['code']) - - formatVars = { - '%s': NumFac, - '%s': FecFac, - '%.02f': HoraFac, - '%.02f': ValorBruto, - '%.02f': ValorTotalPagar, - '%.02f': ValorImpuestoPara.get(CodImpuesto1, 0.0), - '%02d': CodImpuesto1, - '%.02f': ValorImpuestoPara.get(CodImpuesto2, 0.0), - '%02d': CodImpuesto2, - '%.02f': ValorImpuestoPara.get(CodImpuesto3, 0.0), - '%02d': CodImpuesto3, - '%s': NitOFE, - '%s': NumAdq, - '%d': TipoAmb, - } - cufe = "".join(formatVars.keys()) % tuple(formatVars.values()) + TipoAmb = self._tipo_ambiente() + ClTec = str(self.clave_tecnica) + + formatVars = [ + '%s' % NumFac, + '%s' % FecFac, + '%s' % HoraFac, + '%.02f' % ValorBruto, + '%02d' % CodImpuesto1, + '%.02f' % ValorImpuestoPara.get(CodImpuesto1, 0.0), + '%02d' % CodImpuesto2, + '%.02f' % ValorImpuestoPara.get(CodImpuesto2, 0.0), + '%02d' % CodImpuesto3, + '%.02f' % ValorImpuestoPara.get(CodImpuesto3, 0.0), + '%.02f' % ValorTotalPagar, + '%s' % NitOFE, + '%s' % NumAdq, + '%s' % ClTec, + '%d' % TipoAmb, + ] + cufe = "".join(formatVars) # crear hash... h = hashlib.sha384() diff --git a/tests/test_fe_form.py b/tests/test_fe_form.py index d8b119b..6975074 100644 --- a/tests/test_fe_form.py +++ b/tests/test_fe_form.py @@ -14,6 +14,25 @@ import facho.fe.form as form from facho import fe +@pytest.fixture +def simple_invoice_without_lines(): + inv = form.Invoice() + inv.set_period(datetime.now(), datetime.now()) + inv.set_issue(datetime.now()) + inv.set_ident('ABC123') + inv.set_supplier(form.Party( + name = 'facho-supplier', + ident = 123, + responsability_code = 'No aplica', + organization_code = 'Persona Natural' + )) + inv.set_customer(form.Party( + name = 'facho-customer', + ident = 321, + responsability_code = 'No aplica', + organization_code = 'Persona Natural' + )) + return inv @pytest.fixture def simple_invoice(): @@ -110,7 +129,70 @@ def test_invoicesimple_zip(simple_invoice): def test_bug_cbcid_empty_on_invoice_line(simple_invoice): xml_invoice = form.DIANInvoiceXML(simple_invoice) - print(str(xml_invoice)) - cbc_id = xml_invoice.get_element_text('/fe:Invoice/fe:InvoiceLine[1]/cbc:ID', format_=int) assert cbc_id == 1 + +def test_invoice_line_count_numeric(simple_invoice): + xml_invoice = form.DIANInvoiceXML(simple_invoice) + 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 = form.DIANInvoiceXML(simple_invoice) + id_ = xml_invoice.get_element_text('/fe:Invoice/cbc:ProfileExecutionID', format_=int) + assert id_ == 2 + +def test_invoice_totals(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 = '700085371' + simple_invoice.invoice_customer.ident = '800199436' + simple_invoice.add_invoice_line(form.InvoiceLine( + quantity = 1, + description = 'producto', + item_ident = 9999, + price_amount = 1_500_000, + tax = form.TaxTotal( + subtotals = [ + form.TaxSubTotal( + tax_scheme_ident = '01', + percent = 19.0 + )]) + )) + simple_invoice.calculate() + assert 1 == len(simple_invoice.invoice_lines) + assert 1_500_000 == simple_invoice.invoice_legal_monetary_total.line_extension_amount + assert 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 = '700085371' + simple_invoice.invoice_customer.ident = '800199436' + simple_invoice.add_invoice_line(form.InvoiceLine( + quantity = 1, + description = 'producto', + item_ident = 9999, + price_amount = 1_500_000, + tax = form.TaxTotal( + subtotals = [ + form.TaxSubTotal( + tax_scheme_ident = '01', + percent = 19.0 + )]) + )) + + class FakeDIANInvoiceXML(form.DIANInvoiceXML): + def issue_time(self, datetime_): + return '10:53:10-05:00' + def issue_date(self, datetime_): + return '2019-01-16' + + xml_invoice = FakeDIANInvoiceXML(simple_invoice, + tipo_ambiente = form.DIANInvoiceXML.AMBIENTE_PRODUCCION, + clave_tecnica = '693ff6f2a553c3646a063436fd4dd9ded0311471') + cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID') + + assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4'