diff --git a/account_invoice_triple_discount/__manifest__.py b/account_invoice_triple_discount/__manifest__.py index 42477a56069..78f76d0621f 100644 --- a/account_invoice_triple_discount/__manifest__.py +++ b/account_invoice_triple_discount/__manifest__.py @@ -16,6 +16,7 @@ ], 'data': [ 'views/account_invoice_view.xml', + 'views/res_partner_views.xml', ], 'installable': True, } diff --git a/account_invoice_triple_discount/models/__init__.py b/account_invoice_triple_discount/models/__init__.py index 6834786a795..655ebd643e5 100644 --- a/account_invoice_triple_discount/models/__init__.py +++ b/account_invoice_triple_discount/models/__init__.py @@ -1,3 +1,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import line_triple_discount from . import account_invoice +from . import res_partner diff --git a/account_invoice_triple_discount/models/account_invoice.py b/account_invoice_triple_discount/models/account_invoice.py index ad310130c88..e6e09c33ccc 100644 --- a/account_invoice_triple_discount/models/account_invoice.py +++ b/account_invoice_triple_discount/models/account_invoice.py @@ -3,59 +3,42 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models -from odoo.addons import decimal_precision as dp class AccountInvoice(models.Model): _inherit = "account.invoice" + discounting_type = fields.Selection( + related='partner_id.discounting_type', + ) + def get_taxes_values(self): - vals = {} - for line in self.invoice_line_ids: - vals[line] = { - 'price_unit': line.price_unit, - 'discount': line.discount, - } - price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - price_unit *= (1 - (line.discount2 or 0.0) / 100.0) - price_unit *= (1 - (line.discount3 or 0.0) / 100.0) - line.update({ - 'price_unit': price_unit, - 'discount': 0.0, - }) - tax_grouped = super(AccountInvoice, self).get_taxes_values() - for line in self.invoice_line_ids: - line.update(vals[line]) + lines = self.invoice_line_ids + prev_values = lines.triple_discount_preprocess() + tax_grouped = super().get_taxes_values() + lines.triple_discount_postprocess(prev_values) return tax_grouped + @api.onchange('partner_id', 'company_id') + def _onchange_partner_id(self): + self.ensure_one() + res = super()._onchange_partner_id() + partner_discounting_type = self.partner_id.discounting_type + if partner_discounting_type: + self.invoice_line_ids.update({ + 'discounting_type': partner_discounting_type, + }) + return res -class AccountInvoiceLine(models.Model): - _inherit = "account.invoice.line" - discount2 = fields.Float( - 'Discount 2 (%)', - digits=dp.get_precision('Discount'), - ) - discount3 = fields.Float( - 'Discount 3 (%)', - digits=dp.get_precision('Discount'), - ) +class AccountInvoiceLine(models.Model): + _name = "account.invoice.line" + _inherit = ["line.triple_discount.mixin", "account.invoice.line"] @api.multi - @api.depends('discount2', 'discount3') + @api.depends('discount2', 'discount3', 'discounting_type') def _compute_price(self): for line in self: - prev_price_unit = line.price_unit - prev_discount = line.discount - price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - price_unit *= (1 - (line.discount2 or 0.0) / 100.0) - price_unit *= (1 - (line.discount3 or 0.0) / 100.0) - line.update({ - 'price_unit': price_unit, - 'discount': 0.0, - }) + prev_values = line.triple_discount_preprocess() super(AccountInvoiceLine, line)._compute_price() - line.update({ - 'price_unit': prev_price_unit, - 'discount': prev_discount, - }) + line.triple_discount_postprocess(prev_values) diff --git a/account_invoice_triple_discount/models/line_triple_discount.py b/account_invoice_triple_discount/models/line_triple_discount.py new file mode 100644 index 00000000000..e1291b7c65a --- /dev/null +++ b/account_invoice_triple_discount/models/line_triple_discount.py @@ -0,0 +1,111 @@ +# Copyright 2017 Tecnativa - David Vidal +# Copyright 2017 Tecnativa - Pedro M. Baeza +# Copyright 2021 Simone Rubino - Agile Business Group +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.addons import decimal_precision as dp + + +class LineTripleDiscount (models.AbstractModel): + _name = "line.triple_discount.mixin" + _description = "Add triple discount fields and logic" + + discount = fields.Float() + discount2 = fields.Float( + 'Discount 2 (%)', + digits=dp.get_precision('Discount'), + ) + discount3 = fields.Float( + 'Discount 3 (%)', + digits=dp.get_precision('Discount'), + ) + discounting_type = fields.Selection( + string="Discounting type", + selection=[ + ('additive', 'Additive'), + ('multiplicative', 'Multiplicative'), + ], + default="multiplicative", + required=True, + help="Specifies whether discounts should be additive " + "or multiplicative.\nAdditive discounts are summed first and " + "then applied.\nMultiplicative discounts are applied sequentially.\n" + "Multiplicative discounts are default", + ) + + _sql_constraints = [ + ('discount2_limit', 'CHECK (discount2 <= 100.0)', + 'Discount 2 must be lower than 100%.'), + ('discount3_limit', 'CHECK (discount3 <= 100.0)', + 'Discount 3 must be lower than 100%.'), + ] + + def _get_final_discount(self): + self.ensure_one() + if self.discounting_type == "additive": + return self._additive_discount() + elif self.discounting_type == "multiplicative": + return self._multiplicative_discount() + + def _additive_discount(self): + self.ensure_one() + discount = sum( + [getattr(self, x) or 0.0 for x in self._discount_fields()] + ) + if discount <= 0: + return 0 + elif discount >= 100: + return 100 + return discount + + def _multiplicative_discount(self): + self.ensure_one() + discounts = [1 - (self[x] or 0.0) / 100 + for x in self._discount_fields()] + final_discount = 1 + for discount in discounts: + final_discount *= discount + return 100 - final_discount * 100 + + def _discount_fields(self): + return ['discount', 'discount2', 'discount3'] + + @api.multi + def triple_discount_preprocess(self): + """Save the values of the discounts in a dictionary, + to be restored in postprocess. + Resetting discount2 and discount3 to 0.0 avoids issues if + this method is called multiple times. + Updating the cache provides consistency through recomputations.""" + prev_values = dict() + + # The newly computed discount might have + # more digits than allowed from field's precision, + # so let's increase it just for saving it correctly in cache + discount_field = self._fields['discount'] + discount_original_digits = discount_field._digits + discount_field._digits = (16, 10) + + for line in self: + prev_values[line] = dict( + discount=line.discount, + discount2=line.discount2, + discount3=line.discount3, + ) + line.update({ + 'discount': line._get_final_discount(), + 'discount2': 0.0, + 'discount3': 0.0 + }) + + # Restore discount field's precision + discount_field._digits = discount_original_digits + return prev_values + + @api.model + def triple_discount_postprocess(self, prev_values): + """Restore the discounts of the lines in the dictionary prev_values. + Updating the cache provides consistency through recomputations.""" + for line, prev_vals_dict in list(prev_values.items()): + line.update(prev_vals_dict) diff --git a/account_invoice_triple_discount/models/res_partner.py b/account_invoice_triple_discount/models/res_partner.py new file mode 100644 index 00000000000..7d8a2baffc8 --- /dev/null +++ b/account_invoice_triple_discount/models/res_partner.py @@ -0,0 +1,24 @@ +# Copyright 2021 Simone Rubino - Agile Business Group +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResPartner (models.Model): + _inherit = 'res.partner' + + discounting_type = fields.Selection( + string="Discounting type", + selection=[ + ('additive', 'Additive'), + ('multiplicative', 'Multiplicative'), + ], + default="multiplicative", + required=True, + help=""" + Specifies whether discounts should be additive or multiplicative. + Additive discounts are summed first and then applied. + Multiplicative discounts (default) are applied sequentially. + This type of discount will be the default for this partner's invoices. + """, + ) diff --git a/account_invoice_triple_discount/tests/test_invoice_triple_discount.py b/account_invoice_triple_discount/tests/test_invoice_triple_discount.py index 7099a710f87..47c1635f43f 100644 --- a/account_invoice_triple_discount/tests/test_invoice_triple_discount.py +++ b/account_invoice_triple_discount/tests/test_invoice_triple_discount.py @@ -83,3 +83,52 @@ def test_02_discounts_multiple_lines(self): self.invoice_line1.discount = 50.0 self.invoice._onchange_invoice_line_ids() self.assertEqual(self.invoice.amount_total, 365.0) + + def test_03_discounts(self): + """ Tests discounts in edge case """ + invoice = self.env['account.invoice'].create({ + 'name': "Test Customer Invoice", + 'journal_id': self.env['account.journal'].search( + [('type', '=', 'sale')])[0].id, + 'partner_id': self.partner.id, + 'account_id': self.account.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Line 1', + 'price_unit': 25.0, + 'account_id': self.account.id, + 'quantity': 65, + 'discount': 50, + 'discount2': 13, + 'discount3': 0, + })], + }) + self.assertEqual(invoice.invoice_line_ids.price_subtotal, 706.88) + + def test_increased_precision(self): + """ + Check that final discount is computed correctly + when final discount's precision exceeds + discount field's allowed precision. + """ + invoice_line = self.invoice_line1 + discount_field = invoice_line._fields['discount'] + self.assertEqual(discount_field.digits[1], 2) + invoice_line.discount = 45.0 + invoice_line.discount2 = 10.0 + invoice_line.discount3 = 5.0 + + self.assertAlmostEqual( + invoice_line._get_final_discount(), + 52.975, + places=3, + ) + invoice_line.triple_discount_preprocess() + + # Check that the line's discount still has all the 3 digits + self.assertAlmostEqual( + invoice_line.discount, + 52.975, + places=3, + ) + # Check that field's precision is not changed + self.assertEqual(discount_field.digits[1], 2) diff --git a/account_invoice_triple_discount/views/account_invoice_view.xml b/account_invoice_triple_discount/views/account_invoice_view.xml index c85806edfcd..f57c82e9527 100755 --- a/account_invoice_triple_discount/views/account_invoice_view.xml +++ b/account_invoice_triple_discount/views/account_invoice_view.xml @@ -6,10 +6,17 @@ account.invoice + + + + + {'type': type, 'journal_id': journal_id, 'default_invoice_id': id, 'default_discounting_type': discounting_type} + + @@ -24,6 +31,7 @@ position="after"> + diff --git a/account_invoice_triple_discount/views/res_partner_views.xml b/account_invoice_triple_discount/views/res_partner_views.xml new file mode 100644 index 00000000000..a593bd89a75 --- /dev/null +++ b/account_invoice_triple_discount/views/res_partner_views.xml @@ -0,0 +1,20 @@ + + + + + + Add triple discount fields + res.partner + + + + + + + + + +