diff --git a/facho/fe/form/__init__.py b/facho/fe/form/__init__.py index f49a1a0..0aa6318 100644 --- a/facho/fe/form/__init__.py +++ b/facho/fe/form/__init__.py @@ -4,6 +4,7 @@ import hashlib from functools import reduce import copy +import dataclasses from dataclasses import dataclass from datetime import datetime, date from collections import defaultdict @@ -413,6 +414,12 @@ class AllowanceCharge: amount: Amount = Amount(0.0) reason: AllowanceChargeReason = None + #Valor Base para calcular el descuento o el cargo + base_amount: typing.Optional[Amount] = Amount(0.0) + + # Porcentaje: Porcentaje que aplicar. + multiplier_factor_numeric: Amount = Amount(1.0) + def isCharge(self): return self.charge_indicator == True @@ -428,6 +435,9 @@ class AllowanceCharge: def hasReason(self): return self.reason is not None + 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 @@ -446,16 +456,31 @@ class InvoiceLine: # de subtotal tax: typing.Optional[TaxTotal] - allowance_charge = [] + allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list) - def add_allowance_charge(charge): + def add_allowance_charge(self, charge): if not isinstance(charge, AllowanceCharge): raise TypeError('charge invalid type expected AllowanceCharge') + charge.set_base_amount(self.total_amount_without_charge) self.allowance_charge.add(charge) + @property + def total_amount_without_charge(self): + return (self.quantity * self.price.amount) + @property def total_amount(self): - return self.quantity * self.price.amount + charge = AmountCollection(self.allowance_charge)\ + .filter(lambda charge: charge.isCharge())\ + .map(lambda charge: charge.amount)\ + .sum() + + discount = AmountCollection(self.allowance_charge)\ + .filter(lambda charge: charge.isDiscount())\ + .map(lambda charge: charge.amount)\ + .sum() + + return self.total_amount_without_charge + charge - discount @property def total_tax_inclusive_amount(self): @@ -649,11 +674,22 @@ class Invoice: #DIAN 1.7.-2020: FAU14 self.invoice_legal_monetary_total.calculate() + def _refresh_charges_base_amount(self): + if self.invoice_allowance_charge: + 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') + + # cargos a nivel de factura + for charge in self.invoice_allowance_charge: + 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): diff --git a/facho/fe/form_xml/invoice.py b/facho/fe/form_xml/invoice.py index b9d4708..f5e5106 100644 --- a/facho/fe/form_xml/invoice.py +++ b/facho/fe/form_xml/invoice.py @@ -543,13 +543,17 @@ class DIANInvoiceXML(fe.FeXML): invoice_line.price.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 = fexml.fragment('./cac:AllowanceCharge', append=append) + line = parent.fragment('./cac:AllowanceCharge', append=append) #DIAN 1.7.-2020: FAQ02 line.set_element('./cbc:ID', idx) #DIAN 1.7.-2020: FAQ03 @@ -557,8 +561,9 @@ class DIANInvoiceXML(fe.FeXML): 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 diff --git a/tests/test_amount.py b/tests/test_amount.py index fe671ba..d508c84 100644 --- a/tests/test_amount.py +++ b/tests/test_amount.py @@ -40,3 +40,6 @@ def test_amount_truncate(): assert form.Amount(10084.03).truncate_as_string(2) == '10084.03' assert form.Amount(10000.02245).truncate_as_string(2) == '10000.02' assert form.Amount(10000.02357).truncate_as_string(2) == '10000.02' + +def test_amount_format(): + assert str(round(form.Amount(1.1569),2)) == '1.16' diff --git a/tests/test_form_xml.py b/tests/test_form_xml.py index b4a01dd..74f8d9f 100644 --- a/tests/test_form_xml.py +++ b/tests/test_form_xml.py @@ -7,6 +7,7 @@ import pytest from datetime import datetime +import copy from facho.fe import form from facho.fe import form_xml @@ -32,8 +33,8 @@ def test_import_DIANCreditNoteXML(): except AttributeError: pytest.fail("unexpected not found") -def test_FAU10(simple_invoice_without_lines): - inv = simple_invoice_without_lines +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', @@ -52,8 +53,44 @@ def test_FAU10(simple_invoice_without_lines): ) )) 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'