diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index ba84f6f299..8d89858ad7 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -217,12 +217,18 @@ def _inverse_price_unit(self): for line in self.filtered(lambda x: not x.automatic_price): line.specific_price = line.price_unit + def _compute_price_subtotal_helper(self): + self.ensure_one() + subtotal = self.quantity * self.price_unit + discount = self.discount / 100 + subtotal *= 1 - discount + return subtotal + @api.depends("quantity", "price_unit", "discount") def _compute_price_subtotal(self): for line in self: - subtotal = line.quantity * line.price_unit - discount = line.discount / 100 - subtotal *= 1 - discount + subtotal = line._compute_price_subtotal_helper() + if line.contract_id.pricelist_id: cur = line.contract_id.pricelist_id.currency_id line.price_subtotal = cur.round(subtotal) diff --git a/contract_fixed_discount/README.rst b/contract_fixed_discount/README.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contract_fixed_discount/__init__.py b/contract_fixed_discount/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/contract_fixed_discount/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/contract_fixed_discount/__manifest__.py b/contract_fixed_discount/__manifest__.py new file mode 100644 index 0000000000..dfc5c5550c --- /dev/null +++ b/contract_fixed_discount/__manifest__.py @@ -0,0 +1,21 @@ +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +{ + "name": "Contract Fixed Discount", + "version": "14.0.1.0.0", + "category": "Contract Management", + "author": "Foodles, Odoo Community Association (OCA)", + "maintainers": [], + "website": "https://github.com/OCA/contract", + "depends": [ + "account_invoice_fixed_discount", + "contract", + ], + "data": [ + "views/abstract_contract_line.xml", + "views/contract_line.xml", + "views/contract.xml", + ], + "license": "AGPL-3", + "installable": True, +} diff --git a/contract_fixed_discount/models/__init__.py b/contract_fixed_discount/models/__init__.py new file mode 100644 index 0000000000..7e693e1cff --- /dev/null +++ b/contract_fixed_discount/models/__init__.py @@ -0,0 +1 @@ +from . import abstract_contract_line, contract_line diff --git a/contract_fixed_discount/models/abstract_contract_line.py b/contract_fixed_discount/models/abstract_contract_line.py new file mode 100644 index 0000000000..5ee6d2e689 --- /dev/null +++ b/contract_fixed_discount/models/abstract_contract_line.py @@ -0,0 +1,47 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ContractAbstractContractLine(models.AbstractModel): + _inherit = "contract.abstract.contract.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.") + ) + + def _compute_price_subtotal_helper(self): + self.ensure_one() + subtotal = self.quantity * self.price_unit + if self.discount: + subtotal = super()._compute_price_subtotal_helper() + elif self.discount_fixed: + subtotal -= self.discount_fixed + return subtotal + + @api.depends("quantity", "price_unit", "discount", "discount_fixed") + def _compute_price_subtotal(self): + super()._compute_price_subtotal() diff --git a/contract_fixed_discount/models/contract_line.py b/contract_fixed_discount/models/contract_line.py new file mode 100644 index 0000000000..06b284d6d4 --- /dev/null +++ b/contract_fixed_discount/models/contract_line.py @@ -0,0 +1,12 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ContractLine(models.Model): + _inherit = "contract.line" + + def _prepare_invoice_line(self, move_form): + vals = super()._prepare_invoice_line(move_form=move_form) + vals["discount_fixed"] = self.discount_fixed + return vals diff --git a/contract_fixed_discount/readme/CONTRIBUTORS.rst b/contract_fixed_discount/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..15086c2ef9 --- /dev/null +++ b/contract_fixed_discount/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Foodles `_: + + * Damien Crier + * Pierre Verkest diff --git a/contract_fixed_discount/readme/DESCRIPTION.rst b/contract_fixed_discount/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..ec25000d3e --- /dev/null +++ b/contract_fixed_discount/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module extends the functionality of contracts to allow you to apply fixed amount discounts at contract line level. diff --git a/contract_fixed_discount/tests/__init__.py b/contract_fixed_discount/tests/__init__.py new file mode 100644 index 0000000000..f6ebcaee35 --- /dev/null +++ b/contract_fixed_discount/tests/__init__.py @@ -0,0 +1 @@ +from . import test_contract_discount_fixed, test_contract_invoice_discount_fixed diff --git a/contract_fixed_discount/tests/test_contract_discount_fixed.py b/contract_fixed_discount/tests/test_contract_discount_fixed.py new file mode 100644 index 0000000000..c8c4497a87 --- /dev/null +++ b/contract_fixed_discount/tests/test_contract_discount_fixed.py @@ -0,0 +1,64 @@ +# Copyright 2023 Foodles (http://www.foodles.co/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests import Form + +from odoo.addons.contract.tests.test_contract import TestContractBase + + +class TestContractDiscounts(TestContractBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.contract4 = cls.env["contract.contract"].create( + { + "name": "Test Contract4", + "partner_id": cls.partner.id, + "pricelist_id": cls.partner.property_product_pricelist.id, + "line_recurrence": True, + "contract_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product_1.id, + "name": "Services from #START# to #END#", + "quantity": 1, + "uom_id": cls.product_1.uom_id.id, + "price_unit": 100, + "discount_fixed": 48, + "recurring_rule_type": "monthly", + "recurring_interval": 1, + "date_start": "2018-02-15", + "recurring_next_date": "2018-02-22", + }, + ) + ], + } + ) + + def test_onchange_discount(self): + contract = Form(self.contract) + line = contract.contract_line_ids.edit(0) + line.discount_fixed = 42 + self.assertFalse(line.discount) + + def test_onchange_discount_fixed(self): + contract = Form(self.contract) + line = contract.contract_line_ids.edit(0) + line.discount = 42 + self.assertFalse(line.discount_fixed) + + def test_constraint_discount_discount_fixed(self): + with self.assertRaisesRegex( + ValidationError, "You can only set one type of discount per line." + ): + self.contract4.contract_line_ids.discount = 42 + + def test_price_subtotal_discount_percent(self): + self.assertEqual(self.contract.contract_line_ids.price_subtotal, 50.0) + + def test_price_subtotal_discount_fixed(self): + self.assertEqual(self.contract4.contract_line_ids.price_subtotal, 52.0) diff --git a/contract_fixed_discount/tests/test_contract_invoice_discount_fixed.py b/contract_fixed_discount/tests/test_contract_invoice_discount_fixed.py new file mode 100644 index 0000000000..682c489a2c --- /dev/null +++ b/contract_fixed_discount/tests/test_contract_invoice_discount_fixed.py @@ -0,0 +1,42 @@ +# Copyright 2023 Foodles (http://www.foodles.co/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.contract.tests.test_contract import TestContractBase + + +class TestContractDiscounts(TestContractBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.contract4 = cls.env["contract.contract"].create( + { + "name": "Test Contract4", + "partner_id": cls.partner.id, + "pricelist_id": cls.partner.property_product_pricelist.id, + "line_recurrence": True, + "contract_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product_1.id, + "name": "Services from #START# to #END#", + "quantity": 1, + "uom_id": cls.product_1.uom_id.id, + "price_unit": 100, + "discount_fixed": 48, + "recurring_rule_type": "monthly", + "recurring_interval": 1, + "date_start": "2018-02-15", + "recurring_next_date": "2018-02-22", + }, + ) + ], + } + ) + + def test_invoice_lines_discount_fixed(self): + invoice = self.contract4.recurring_create_invoice() + self.assertEquals(invoice.invoice_line_ids.discount_fixed, 48) + self.assertEquals(invoice.invoice_line_ids.discount, 0) diff --git a/contract_fixed_discount/views/abstract_contract_line.xml b/contract_fixed_discount/views/abstract_contract_line.xml new file mode 100644 index 0000000000..62658a00bf --- /dev/null +++ b/contract_fixed_discount/views/abstract_contract_line.xml @@ -0,0 +1,27 @@ + + + + + contract.abstract.contract.line form view (in contract) + contract.abstract.contract.line + + + + + + + + + + diff --git a/contract_fixed_discount/views/contract.xml b/contract_fixed_discount/views/contract.xml new file mode 100644 index 0000000000..3199d6dff8 --- /dev/null +++ b/contract_fixed_discount/views/contract.xml @@ -0,0 +1,30 @@ + + + + contract.contract form view (in contract) + contract.contract + + + + + + + + + + + diff --git a/contract_fixed_discount/views/contract_line.xml b/contract_fixed_discount/views/contract_line.xml new file mode 100644 index 0000000000..29292d8dfd --- /dev/null +++ b/contract_fixed_discount/views/contract_line.xml @@ -0,0 +1,36 @@ + + + + + contract.contract_line_tree_view + contract.line + + + + + + + + + + + contract.contract_line_report_tree_view + contract.line + + + + + + + + + + diff --git a/setup/contract_fixed_discount/odoo/addons/contract_fixed_discount b/setup/contract_fixed_discount/odoo/addons/contract_fixed_discount new file mode 120000 index 0000000000..fce7ee26dc --- /dev/null +++ b/setup/contract_fixed_discount/odoo/addons/contract_fixed_discount @@ -0,0 +1 @@ +../../../../contract_fixed_discount \ No newline at end of file diff --git a/setup/contract_fixed_discount/setup.py b/setup/contract_fixed_discount/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/contract_fixed_discount/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)