diff --git a/account_global_discount/README.rst b/account_global_discount/README.rst index 3efd54c2583..127a9e66a83 100644 --- a/account_global_discount/README.rst +++ b/account_global_discount/README.rst @@ -7,7 +7,7 @@ Account Global Discount !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:99b4a5fc4c160372c6436d7866324cab44bc010f7436f88731b3419d83d76c63 + !! source digest: sha256:67f29e2f1d4c9dd6ef7dfaa58d84e53a86abb545b581d5e7c5c042a37190552b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -111,6 +111,12 @@ Contributors * Ernesto Tejeda * Víctor Martínez * Omar Castiñeira +* `TAKOBI `_: + + * Simone Rubino +* `Pordenone Linux User Group APS `_: + + * Sergio Zanchetta Maintainers ~~~~~~~~~~~ diff --git a/account_global_discount/models/account_move.py b/account_global_discount/models/account_move.py index 3ce1ed30177..ec328def857 100644 --- a/account_global_discount/models/account_move.py +++ b/account_global_discount/models/account_move.py @@ -1,8 +1,11 @@ # Copyright 2019 Tecnativa - David Vidal # Copyright 2020-2021 Tecnativa - Pedro M. Baeza # Copyright 2021 Tecnativa - Víctor Martínez +# Copyright 2022 Simone Rubino - TAKOBI +# Copyright 2024 Sergio Zanchetta - PNLUG APS # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, exceptions, fields, models +from odoo.fields import first from odoo.tools import config @@ -33,6 +36,22 @@ class AccountMove(models.Model): readonly=True, states={"draft": [("readonly", False)]}, ) + global_discount_base = fields.Selection( + selection=[ + ("subtotal", "Subtotal"), + ("total", "Total"), + ], + string="Discount Base", + compute="_compute_global_discount_selection", + ) + global_discount_type = fields.Selection( + selection=[ + ("percentage", "Percentage"), + ("fixed", "Fixed"), + ], + string="Discount Type", + compute="_compute_global_discount_selection", + ) amount_global_discount = fields.Monetary( string="Total Global Discounts", compute="_compute_amount", @@ -49,12 +68,29 @@ class AccountMove(models.Model): compute_sudo=True, store=True, ) + amount_total_before_global_discounts = fields.Monetary( + string="Amount Total Before Discounts", + compute="_compute_amount", + currency_field="currency_id", + readonly=True, + compute_sudo=True, + store=True, + ) invoice_global_discount_ids = fields.One2many( comodel_name="account.invoice.global.discount", inverse_name="invoice_id", readonly=True, ) + @api.depends("global_discount_ids") + def _compute_global_discount_selection(self): + for invoice in self: + # Only check first because sanity checks + # assure all global discounts in same invoice have same base + first_global_discount = first(invoice.global_discount_ids) + invoice.global_discount_base = first_global_discount.discount_base + invoice.global_discount_type = first_global_discount.discount_type + def _recompute_tax_lines( self, recompute_tax_base_amount=False, tax_rep_lines_to_recompute=None ): @@ -84,25 +120,29 @@ def _update_tax_lines_for_global_discount(self): We are altering the recently recreated tax move lines got calling super on ``_recompute_tax_lines``. """ - round_curr = self.currency_id.round - tax_lines = self.line_ids.filtered( - lambda r: r.tax_line_id.amount_type in ("percent", "division") - ) - for tax_line in tax_lines: - base = tax_line.tax_base_amount - tax_line.base_before_global_discounts = base - amount = tax_line.balance - for discount in self.global_discount_ids: - base = discount._get_global_discount_vals(base)["base_discounted"] - amount = discount._get_global_discount_vals(amount)["base_discounted"] - tax_line.tax_base_amount = round_curr(base) - tax_line.debit = amount > 0.0 and amount or 0.0 - tax_line.credit = amount < 0.0 and -amount or 0.0 - # Apply onchanges - tax_line._onchange_balance() - tax_line._onchange_amount_currency() - - def _prepare_global_discount_vals(self, global_discount, base, tax_ids): + if self.global_discount_base == "subtotal": + round_curr = self.currency_id.round + tax_lines = self.line_ids.filtered( + lambda r: r.tax_line_id.amount_type in ("percent", "division") + ) + for tax_line in tax_lines: + base = tax_line.tax_base_amount + tax_line.base_before_global_discounts = base + amount = tax_line.balance + for discount in self.global_discount_ids: + base = discount._get_global_discount_vals(base)["base_discounted"] + if discount.discount_type == "percentage": + amount = discount._get_global_discount_vals(amount)[ + "base_discounted" + ] + tax_line.tax_base_amount = round_curr(base) + tax_line.debit = amount > 0.0 and amount or 0.0 + tax_line.credit = amount < 0.0 and -amount or 0.0 + # Apply onchanges + tax_line._onchange_balance() + tax_line._onchange_amount_currency() + + def _prepare_global_discount_vals(self, global_discount, base, tax_ids=None): """Prepare the dictionary values for an invoice global discount line. """ @@ -112,11 +152,13 @@ def _prepare_global_discount_vals(self, global_discount, base, tax_ids): "name": global_discount.display_name, "invoice_id": self.id, "global_discount_id": global_discount.id, + "discount_type": global_discount.discount_type, "discount": global_discount.discount, + "discount_fixed": global_discount.discount_fixed, "base": base, "base_discounted": discount["base_discounted"], "account_id": global_discount.account_id.id, - "tax_ids": [(4, tax_id) for tax_id in tax_ids], + "tax_ids": [(4, tax_id) for tax_id in tax_ids] if tax_ids else None, } def _set_global_discounts_by_tax(self): @@ -128,10 +170,16 @@ def _set_global_discounts_by_tax(self): self.ensure_one() if not self.is_invoice(): return - in_draft_mode = self != self._origin + if not self.global_discount_ids: + return taxes_keys = {} # Perform a sanity check for discarding cases that will lead to # incorrect data in discounts + discount_base = set(self.global_discount_ids.mapped("discount_base")) + if len(discount_base) > 1: + raise exceptions.UserError(_("All global discount must have the same base")) + discount_base = discount_base.pop() + _self = self.filtered("global_discount_ids") for inv_line in _self.invoice_line_ids.filtered(lambda l: not l.display_type): for key in taxes_keys: @@ -141,6 +189,14 @@ def _set_global_discounts_by_tax(self): taxes_keys[tuple(inv_line.tax_ids.ids)] = True # Reset previous global discounts self.invoice_global_discount_ids -= self.invoice_global_discount_ids + + if discount_base == "subtotal": + self._apply_global_discount_by_subtotal(_self, taxes_keys) + elif discount_base == "total": + self._apply_global_discount_by_total() + + def _apply_global_discount_by_subtotal(self, _self, taxes_keys): + in_draft_mode = self != self._origin model = "account.invoice.global.discount" create_method = in_draft_mode and self.env[model].new or self.env[model].create for tax_line in _self.line_ids.filtered("tax_line_id"): @@ -171,6 +227,16 @@ def _set_global_discounts_by_tax(self): create_method(vals) base = vals["base_discounted"] + def _apply_global_discount_by_total(self): + in_draft_mode = self != self._origin + model = "account.invoice.global.discount" + create_method = in_draft_mode and self.env[model].new or self.env[model].create + base = self.amount_total + for global_discount in self.global_discount_ids: + vals = self._prepare_global_discount_vals(global_discount, base, None) + create_method(vals) + base = vals["base_discounted"] + def _recompute_global_discount_lines(self): """Append global discounts move lines. @@ -181,7 +247,9 @@ def _recompute_global_discount_lines(self): in_draft_mode = self != self._origin model = "account.move.line" create_method = in_draft_mode and self.env[model].new or self.env[model].create - for discount in self.invoice_global_discount_ids.filtered("discount"): + for discount in self.invoice_global_discount_ids.filtered( + lambda x: x.discount or x.discount_fixed + ): sign = -1 if self.move_type in {"in_invoice", "out_refund"} else 1 disc_amount = sign * discount.discount_amount create_method( @@ -248,8 +316,14 @@ def _compute_amount_one(self): for discount in self.invoice_global_discount_ids ) self.amount_untaxed_before_global_discounts = self.amount_untaxed - self.amount_untaxed = self.amount_untaxed + self.amount_global_discount - self.amount_total = self.amount_untaxed + self.amount_tax + self.amount_total_before_global_discounts = self.amount_total + + if self.global_discount_base == "subtotal": + self.amount_untaxed = self.amount_untaxed + self.amount_global_discount + self.amount_total = self.amount_untaxed + self.amount_tax + elif self.global_discount_base == "total": + self.amount_total += self.amount_global_discount + amount_untaxed_signed = self.amount_untaxed if ( self.currency_id @@ -296,6 +370,7 @@ def create(self, vals_list): def _check_balanced(self): """Add the check of proper taxes for global discounts.""" super()._check_balanced() + test_condition = not config["test_enable"] or self.env.context.get( "test_account_global_discount" ) @@ -358,7 +433,20 @@ class AccountInvoiceGlobalDiscount(models.Model): comodel_name="global.discount", string="Global Discount", ) - discount = fields.Float(string="Discount (number)") + discount_type = fields.Selection( + selection=[ + ("percentage", "Percentage"), + ("fixed", "Fixed"), + ], + string="Discount Type", + ) + discount = fields.Float( + string="Discount (number)", + ) + discount_fixed = fields.Float( + string="Discount (number)", + currency_field="currency_id", + ) discount_display = fields.Char( compute="_compute_discount_display", string="Discount", @@ -367,7 +455,7 @@ class AccountInvoiceGlobalDiscount(models.Model): base_discounted = fields.Float(string="Base after discount", digits="Product Price") currency_id = fields.Many2one(related="invoice_id.currency_id", readonly=True) discount_amount = fields.Monetary( - string="Discounted Amount", + string="Discount Amount", compute="_compute_discount_amount", currency_field="currency_id", compute_sudo=True, @@ -387,9 +475,16 @@ class AccountInvoiceGlobalDiscount(models.Model): def _compute_discount_display(self): """Given a discount type, we need to render a different symbol""" + for one in self: - precision = self.env["decimal.precision"].precision_get("Discount") - one.discount_display = "{0:.{1}f}%".format(one.discount * -1, precision) + if one.discount_type == "percentage": + precision = self.env["decimal.precision"].precision_get("Discount") + one.discount_display = "{0:.{1}f}%".format(one.discount * -1, precision) + elif one.discount_type == "fixed": + precision = self.env["decimal.precision"].precision_get("Product Price") + one.discount_display = "{0:.{1}f}".format( + one.discount_fixed * -1, precision + ) @api.depends("base", "base_discounted") def _compute_discount_amount(self): diff --git a/account_global_discount/readme/CONTRIBUTORS.rst b/account_global_discount/readme/CONTRIBUTORS.rst index bf340291ae6..8627f35c467 100644 --- a/account_global_discount/readme/CONTRIBUTORS.rst +++ b/account_global_discount/readme/CONTRIBUTORS.rst @@ -7,3 +7,9 @@ * Ernesto Tejeda * Víctor Martínez * Omar Castiñeira +* `TAKOBI `_: + + * Simone Rubino +* `Pordenone Linux User Group APS `_: + + * Sergio Zanchetta diff --git a/account_global_discount/static/description/index.html b/account_global_discount/static/description/index.html index 4a0474513ed..aed06e243f1 100644 --- a/account_global_discount/static/description/index.html +++ b/account_global_discount/static/description/index.html @@ -367,7 +367,7 @@

Account Global Discount

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:99b4a5fc4c160372c6436d7866324cab44bc010f7436f88731b3419d83d76c63 +!! source digest: sha256:67f29e2f1d4c9dd6ef7dfaa58d84e53a86abb545b581d5e7c5c042a37190552b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/account-invoicing Translate me on Weblate Try me on Runboat

Apply global discounts to invoices

@@ -462,6 +462,14 @@

Contributors

  • Omar Castiñeira <omar@comunitea.com>
  • +
  • TAKOBI: +
  • +
  • Pordenone Linux User Group APS:
      +
    • Sergio Zanchetta
    • +
    +
  • diff --git a/account_global_discount/tests/test_global_discount.py b/account_global_discount/tests/test_global_discount.py index 83a2a08c4a4..d22dfd71a04 100644 --- a/account_global_discount/tests/test_global_discount.py +++ b/account_global_discount/tests/test_global_discount.py @@ -1,6 +1,8 @@ # Copyright 2019 Tecnativa - David Vidal # Copyright 2020 Tecnativa - Pedro M. Baeza # Copyright 2021 Tecnativa - Víctor Martínez +# Copyright 2022 Simone Rubino - TAKOBI +# Copyright 2024 Sergio Zanchetta - PNLUG APS # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import exceptions from odoo.tests import Form, common @@ -85,6 +87,25 @@ def setUpClass(cls): "property_account_payable_id": cls.account_payable.id, } ) + cls.global_discount_total = cls.global_discount_obj.create( + { + "name": "Test Total Discount", + "discount_scope": "sale", + "discount_base": "total", + "discount": 50, + "account_id": cls.account.id, + } + ) + cls.global_discount_fixed_total = cls.global_discount_obj.create( + { + "name": "Test Total Fixed Discount", + "discount_scope": "sale", + "discount_base": "total", + "discount_type": "fixed", + "discount_fixed": 20, + "account_id": cls.account.id, + } + ) cls.partner_2.supplier_global_discount_ids = cls.global_discount_2 cls.tax = cls.env["account.tax"].create( { @@ -373,3 +394,86 @@ def test_09_customer_invoice(self): lines = invoice.line_ids line_15 = lines.filtered(lambda x: x.global_discount_item and x.tax_ids == tax) self.assertAlmostEqual(line_15.debit, 100) + + def test_mixed_discount_base(self): + """ + Check that an invoice can only have discount having the same base. + """ + with Form(self.invoice) as invoice_form: + invoice_form.global_discount_ids.clear() + with self.assertRaises(exceptions.UserError) as ue: + invoice_form.global_discount_ids.add(self.global_discount_1) + invoice_form.global_discount_ids.add(self.global_discount_total) + exception_message = ue.exception.name + self.assertIn("must have the same base", exception_message) + + def test_discount_total(self): + """ + Add global discounts on total to the invoice. + + Check that only the total amount is discounted, + taxes and subtotal remain the same. + """ + # Pre-condition: check starting amounts and the discount base + self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0) + self.assertAlmostEqual(self.invoice.amount_tax, 30.0) + self.assertAlmostEqual(self.invoice.amount_global_discount, 0) + self.assertAlmostEqual(self.invoice.amount_total, 230) + self.assertEqual(self.global_discount_total.discount_base, "total") + + # Act: set the global total discount + with Form(self.invoice) as invoice_form: + invoice_form.global_discount_ids.clear() + invoice_form.global_discount_ids.add(self.global_discount_total) + precision = self.env["decimal.precision"].precision_get("Discount") + + # Assert: global discounts are applied to the total + # and taxes remain the same: + # 230 - 50% (global disc. 1) = 115 + self.assertEqual( + self.invoice.invoice_global_discount_ids.discount_display, + "-50.{}%".format("0" * precision), + ) + self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0) + self.assertAlmostEqual(self.invoice.amount_tax, 30.0) + self.assertAlmostEqual(self.invoice.amount_global_discount, -115.0) + self.assertAlmostEqual(self.invoice.amount_total, 115.0) + + def test_global_invoice_total_mixed_discount(self): + """Add both global fixed and percentage discount on total to the invoice""" + # Pre-condition: check starting amounts and the discount base + self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0) + self.assertAlmostEqual(self.invoice.amount_tax, 30.0) + self.assertAlmostEqual(self.invoice.amount_global_discount, 0) + self.assertAlmostEqual(self.invoice.amount_total, 230) + self.assertEqual(self.global_discount_total.discount_base, "total") + + # Act: set the fixed global total discount + with Form(self.invoice) as invoice_form: + invoice_form.global_discount_ids.clear() + invoice_form.global_discount_ids.add(self.global_discount_fixed_total) + precision = self.env["decimal.precision"].precision_get("Product Price") + + # Assert: fixed discount is applied to the total + # and taxes remain the same: + # 230 - 20 (fixed disc.) = 210 + self.assertEqual( + self.invoice.invoice_global_discount_ids.discount_display, + "-20.{}".format("0" * precision), + ) + self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0) + self.assertAlmostEqual(self.invoice.amount_tax, 30.0) + self.assertAlmostEqual(self.invoice.amount_global_discount, -20.0) + self.assertAlmostEqual(self.invoice.amount_total, 210.0) + + # Act: set the next global total discount + with Form(self.invoice) as invoice_form: + invoice_form.global_discount_ids.add(self.global_discount_total) + + # Assert: percentage discount is applied to previous discounted total, + # and taxes remain the same: + # 210 - 50% (percentage disc.) = 105 + self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0) + self.assertAlmostEqual(self.invoice.amount_tax, 30.0) + self.assertAlmostEqual(self.invoice.amount_global_discount, -125.0) + self.assertAlmostEqual(self.invoice.amount_total, 105.0) diff --git a/account_global_discount/views/account_invoice_views.xml b/account_global_discount/views/account_invoice_views.xml index c3dc6c1afe9..750e5ca1ef3 100644 --- a/account_global_discount/views/account_invoice_views.xml +++ b/account_global_discount/views/account_invoice_views.xml @@ -1,5 +1,7 @@ @@ -25,17 +27,28 @@ readonly="1" groups="!base_global_discount.group_global_discount" /> + + + + + @@ -45,6 +58,13 @@ separator=" " /> + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000000..04428d01a9f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +# https://github.com/OCA/server-backend/pull/267 +git+https://github.com/OCA/server-backend@refs/pull/267/head#subdirectory=setup/base_global_discount