diff --git a/account_invoice_fixed_discount/__manifest__.py b/account_invoice_fixed_discount/__manifest__.py index a278b84b6d75..eb784cdb397e 100644 --- a/account_invoice_fixed_discount/__manifest__.py +++ b/account_invoice_fixed_discount/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Account Fixed Discount", "summary": "Allows to apply fixed amount discounts in invoices.", - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "category": "Accounting & Finance", "website": "https://github.com/OCA/account-invoicing", "author": "ForgeFlow, Odoo Community Association (OCA)", @@ -12,5 +12,9 @@ "application": False, "installable": True, "depends": ["account"], - "data": ["views/account_move_view.xml", "reports/report_account_invoice.xml"], + "data": [ + "security/res_groups.xml", + "views/account_move_view.xml", + "reports/report_account_invoice.xml", + ], } diff --git a/account_invoice_fixed_discount/models/__init__.py b/account_invoice_fixed_discount/models/__init__.py index 44b2c7a5e83a..cc589d1c321e 100644 --- a/account_invoice_fixed_discount/models/__init__.py +++ b/account_invoice_fixed_discount/models/__init__.py @@ -1,3 +1,3 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from . import account_move +from . import account_move_line diff --git a/account_invoice_fixed_discount/models/account_move.py b/account_invoice_fixed_discount/models/account_move.py deleted file mode 100644 index 553223ccdff5..000000000000 --- a/account_invoice_fixed_discount/models/account_move.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2017 ForgeFlow S.L. -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - - -class AccountMove(models.Model): - _inherit = "account.move" - - def _recompute_tax_lines( - self, recompute_tax_base_amount=False, tax_rep_lines_to_recompute=None - ): - vals = {} - for line in self.invoice_line_ids.filtered("discount_fixed"): - vals[line] = {"price_unit": line.price_unit} - price_unit = line.price_unit - line.discount_fixed - line.update({"price_unit": price_unit}) - res = super(AccountMove, self)._recompute_tax_lines( - recompute_tax_base_amount=recompute_tax_base_amount, - tax_rep_lines_to_recompute=tax_rep_lines_to_recompute, - ) - for line in vals.keys(): - line.update(vals[line]) - return res - - -class AccountMoveLine(models.Model): - _inherit = "account.move.line" - - discount_fixed = fields.Float( - string="Discount (Fixed)", - digits="Product Price", - default=0.00, - help="Fixed amount discount.", - ) - - @api.onchange("discount") - def _onchange_discount(self): - if self.discount: - self.discount_fixed = 0.0 - - @api.onchange("discount_fixed") - def _onchange_discount_fixed(self): - if self.discount_fixed: - self.discount = 0.0 - - @api.constrains("discount", "discount_fixed") - def _check_only_one_discount(self): - for rec in self: - for line in rec: - if line.discount and line.discount_fixed: - raise ValidationError( - _("You can only set one type of discount per line.") - ) - - @api.onchange("quantity", "discount", "price_unit", "tax_ids", "discount_fixed") - def _onchange_price_subtotal(self): - return super(AccountMoveLine, self)._onchange_price_subtotal() - - @api.model - def _get_price_total_and_subtotal_model( - self, - price_unit, - quantity, - discount, - currency, - product, - partner, - taxes, - move_type, - ): - if self.discount_fixed != 0: - discount = ((self.discount_fixed) / price_unit) * 100 or 0.00 - return super(AccountMoveLine, self)._get_price_total_and_subtotal_model( - price_unit, quantity, discount, currency, product, partner, taxes, move_type - ) - - @api.model - def _get_fields_onchange_balance_model( - self, - quantity, - discount, - amount_currency, - move_type, - currency, - taxes, - price_subtotal, - force_computation=False, - ): - if self.discount_fixed != 0: - discount = ((self.discount_fixed) / self.price_unit) * 100 or 0.00 - return super(AccountMoveLine, self)._get_fields_onchange_balance_model( - quantity, - discount, - amount_currency, - move_type, - currency, - taxes, - price_subtotal, - force_computation=force_computation, - ) - - @api.model_create_multi - def create(self, vals_list): - prev_discount = [] - for vals in vals_list: - if vals.get("discount_fixed"): - prev_discount.append( - {"discount_fixed": vals.get("discount_fixed"), "discount": 0.00} - ) - fixed_discount = ( - vals.get("discount_fixed") / vals.get("price_unit") - ) * 100 - vals.update({"discount": fixed_discount, "discount_fixed": 0.00}) - elif vals.get("discount"): - prev_discount.append({"discount": vals.get("discount")}) - res = super(AccountMoveLine, self).create(vals_list) - i = 0 - for rec in res: - if rec.discount and prev_discount: - rec.write(prev_discount[i]) - i += 1 - return res diff --git a/account_invoice_fixed_discount/models/account_move_line.py b/account_invoice_fixed_discount/models/account_move_line.py new file mode 100644 index 000000000000..6a80a5b5dcdf --- /dev/null +++ b/account_invoice_fixed_discount/models/account_move_line.py @@ -0,0 +1,66 @@ +# Copyright 2017 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_round + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + discount_fixed = fields.Monetary( + string="Discount (Fixed)", + default=0.0, + currency_field="currency_id", + help=( + "Apply a fixed amount discount to this line. The amount is multiplied by " + "the quantity of the product." + ), + ) + + @api.constrains("discount_fixed", "discount") + def _check_discounts(self): + """Check that the fixed discount and the discount percentage are consistent.""" + for line in self: + if line.discount_fixed and line.discount: + currency = line.currency_id + calculated_fixed_discount = float_round( + line._get_discount_from_fixed_discount(), + precision_rounding=currency.rounding, + ) + + if calculated_fixed_discount != line.discount: + raise ValidationError( + _( + "The fixed discount %(fixed)s does not match the calculated " + "discount %(discount)s %%. Please correct one of the discounts." + ) + % { + "fixed": line.discount_fixed, + "discount": line.discount, + } + ) + + def _get_discount_from_fixed_discount(self): + """Calculate the discount percentage from the fixed discount amount.""" + self.ensure_one() + if not self.discount_fixed: + return 0.0 + + return self.discount_fixed / self.price_unit * 100 + + @api.onchange("discount_fixed", "quantity", "price_unit") + def _onchange_discount_fixed(self): + """When the discount fixed is changed, we set the ``discount`` on the line to the + appropriate value and apply the default downstream implementation. + + When the display_type is not ``'product'`` we reset the discount_fixed to 0.0. + + """ + if self.display_type != "product" or not self.quantity or not self.price_unit: + self.discount_fixed = 0.0 + return + + if self.discount_fixed: + self.discount = self._get_discount_from_fixed_discount() diff --git a/account_invoice_fixed_discount/readme/CONTRIBUTORS.rst b/account_invoice_fixed_discount/readme/CONTRIBUTORS.rst index ec50e28845c7..44ef0c418c59 100644 --- a/account_invoice_fixed_discount/readme/CONTRIBUTORS.rst +++ b/account_invoice_fixed_discount/readme/CONTRIBUTORS.rst @@ -2,3 +2,4 @@ * Jordi Ballester * Rattapong Chokmasermkul * Kitti U. +* Pieter Paulussen diff --git a/account_invoice_fixed_discount/readme/ROADMAP.rst b/account_invoice_fixed_discount/readme/ROADMAP.rst deleted file mode 100644 index e1bf21e7ffa6..000000000000 --- a/account_invoice_fixed_discount/readme/ROADMAP.rst +++ /dev/null @@ -1,2 +0,0 @@ -* At the moment, the simultaneous use of percent and fixed discounts (at - line level) is not supported. diff --git a/account_invoice_fixed_discount/reports/report_account_invoice.xml b/account_invoice_fixed_discount/reports/report_account_invoice.xml index ca41330c4f9e..bfd66c8e42ec 100644 --- a/account_invoice_fixed_discount/reports/report_account_invoice.xml +++ b/account_invoice_fixed_discount/reports/report_account_invoice.xml @@ -8,18 +8,18 @@ t-value="any([l.discount_fixed for l in o.invoice_line_ids])" /> - + Disc. Fixed Amount - + diff --git a/account_invoice_fixed_discount/security/res_groups.xml b/account_invoice_fixed_discount/security/res_groups.xml new file mode 100644 index 000000000000..5b6ef73f5946 --- /dev/null +++ b/account_invoice_fixed_discount/security/res_groups.xml @@ -0,0 +1,21 @@ + + + + + + Fixed Discount + + + + + + + + + + + + diff --git a/account_invoice_fixed_discount/tests/test_account_fixed_discount.py b/account_invoice_fixed_discount/tests/test_account_fixed_discount.py index 12f690ed9e17..bd3e4ea0ecb0 100644 --- a/account_invoice_fixed_discount/tests/test_account_fixed_discount.py +++ b/account_invoice_fixed_discount/tests/test_account_fixed_discount.py @@ -3,29 +3,23 @@ from odoo.exceptions import ValidationError from odoo.tests import TransactionCase +from odoo.tests.common import Form class TestInvoiceFixedDiscount(TransactionCase): @classmethod def setUpClass(cls): super(TestInvoiceFixedDiscount, cls).setUpClass() + + cls.env.user.groups_id |= cls.env.ref("account.group_account_invoice") cls.partner = cls.env["res.partner"].create({"name": "Test"}) cls.product = cls.env.ref("product.product_product_3") cls.account = cls.env["account.account"].search( - [ - ( - "user_type_id", - "=", - cls.env.ref("account.data_account_type_revenue").id, - ) - ], + [("account_type", "=", "income")], limit=1, ) - type_current_liability = cls.env.ref( - "account.data_account_type_current_liabilities" - ) cls.output_vat_acct = cls.env["account.account"].create( - {"name": "10", "code": "10", "user_type_id": type_current_liability.id} + {"name": "10", "code": "10", "account_type": "liability_current"} ) cls.tax_group_vat = cls.env["account.tax.group"].create({"name": "VAT"}) cls.vat = cls.env["account.tax"].create( @@ -50,33 +44,35 @@ def setUpClass(cls): ], } ) + cls.invoice = cls._create_invoice() - def _create_invoice(self, discount=0.00, discount_fixed=0.00): + @classmethod + def _create_invoice(cls, discount=0.00, discount_fixed=0.00): invoice_vals = [ ( 0, 0, { - "product_id": self.product.id, + "product_id": cls.product.id, "quantity": 1.0, - "account_id": self.account.id, + "account_id": cls.account.id, "name": "Line 1", "price_unit": 200.00, "discount_fixed": discount_fixed, "discount": discount, - "tax_ids": [(6, 0, [self.vat.id])], + "tax_ids": [(6, 0, [cls.vat.id])], }, ) ] invoice = ( - self.env["account.move"] + cls.env["account.move"] .with_context(check_move_validity=False) .create( { - "journal_id": self.env["account.journal"] + "journal_id": cls.env["account.journal"] .search([("type", "=", "sale")], limit=1) .id, - "partner_id": self.partner.id, + "partner_id": cls.partner.id, "move_type": "out_invoice", "invoice_line_ids": invoice_vals, } @@ -84,24 +80,52 @@ def _create_invoice(self, discount=0.00, discount_fixed=0.00): ) return invoice - def test_01_discounts_fixed(self): + def test_01_discounts_fixed_single_unit(self): """Tests multiple discounts in line with taxes.""" - invoice = self._create_invoice(discount_fixed=57) - with self.assertRaises(ValidationError): - invoice.invoice_line_ids.discount = 50 - invoice.invoice_line_ids._onchange_discount_fixed() - self.assertEqual(invoice.invoice_line_ids.discount, 0.00) - invoice.invoice_line_ids._onchange_price_subtotal() - invoice.line_ids.write({"recompute_tax_line": True}) - invoice._onchange_invoice_line_ids() + + # Fixed discount 1.0 unit at 57.00 + with Form(self.invoice) as invoice_form: + with invoice_form.invoice_line_ids.edit(0) as line: + line.discount_fixed = 57.00 + + # compute discount (57 / 200) * 100 + self.assertEqual(self.invoice.invoice_line_ids.discount, 28.5) # compute amount total (200 - 57) * 10% - self.assertEqual(invoice.amount_total, 157.3) - self.assertEqual(invoice.invoice_line_ids.price_unit, 200.00) - self.assertEqual(invoice.invoice_line_ids.price_subtotal, 143.00) + self.assertEqual(self.invoice.amount_total, 157.3) + self.assertEqual(self.invoice.invoice_line_ids.price_unit, 200.00) + self.assertEqual(self.invoice.invoice_line_ids.price_subtotal, 143.00) + + # Reset to regular discount at 20.00% + with Form(self.invoice) as invoice_form: + with invoice_form.invoice_line_ids.edit(0) as line: + line.discount_fixed = 0 + line.discount = 20.0 + + self.assertEqual(self.invoice.amount_total, 176.0) + self.assertEqual(self.invoice.invoice_line_ids.price_unit, 200.00) + self.assertEqual(self.invoice.invoice_line_ids.price_subtotal, 160.00) + + def test_02_discounts_fixed_multiple_units(self): + """Tests multiple discounts in line with taxes.""" + + # Fixed discount 2.0 units at 50.00 + with Form(self.invoice) as invoice_form: + with invoice_form.invoice_line_ids.edit(0) as line: + line.discount_fixed = 25.00 + line.quantity = 2.0 + + # compute discount ((50 / 2) / 200) * 100 + self.assertEqual(self.invoice.invoice_line_ids.discount, 12.5) + self.assertEqual(self.invoice.amount_total, 385.0) + self.assertEqual(self.invoice.invoice_line_ids.price_unit, 200.00) + self.assertEqual(self.invoice.invoice_line_ids.price_subtotal, 350) + + def test_03_discount_validation(self): - def test_02_discounts(self): - invoice = self._create_invoice(discount=50) - invoice.invoice_line_ids._onchange_discount() - self.assertEqual(invoice.invoice_line_ids.discount_fixed, 0.00) - self.assertEqual(invoice.invoice_line_ids.price_unit, 200.00) - self.assertEqual(invoice.invoice_line_ids.price_subtotal, 100.00) + with self.assertRaisesRegex( + ValidationError, "Please correct one of the discounts" + ): + with Form(self.invoice) as invoice_form: + with invoice_form.invoice_line_ids.edit(0) as line: + line.discount_fixed = 57.00 + line.discount = 20.0 diff --git a/account_invoice_fixed_discount/views/account_move_view.xml b/account_invoice_fixed_discount/views/account_move_view.xml index c54fc7bca06d..a6fd476a5011 100644 --- a/account_invoice_fixed_discount/views/account_move_view.xml +++ b/account_invoice_fixed_discount/views/account_move_view.xml @@ -12,21 +12,20 @@ > - - - - +