From bd8a4e3c3ce116c5a56218bbdb141f748fbf7def Mon Sep 17 00:00:00 2001 From: Matthias Barkat Date: Mon, 16 Jun 2025 09:04:01 +0200 Subject: [PATCH 1/6] ADD module contract_fixed_discount --- contract/models/abstract_contract_line.py | 20 +- contract_fixed_discount/README.rst | 80 ++++ contract_fixed_discount/__init__.py | 1 + contract_fixed_discount/__manifest__.py | 20 + contract_fixed_discount/models/__init__.py | 1 + .../models/abstract_contract_line.py | 44 ++ .../models/contract_line.py | 12 + contract_fixed_discount/pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 3 + contract_fixed_discount/readme/DESCRIPTION.md | 2 + .../static/description/index.html | 428 ++++++++++++++++++ contract_fixed_discount/tests/__init__.py | 1 + .../tests/test_contract_discount_fixed.py | 67 +++ .../test_contract_invoice_discount_fixed.py | 43 ++ .../views/abstract_contract_line.xml | 27 ++ contract_fixed_discount/views/contract.xml | 30 ++ .../views/contract_line.xml | 36 ++ 17 files changed, 813 insertions(+), 5 deletions(-) create mode 100644 contract_fixed_discount/README.rst create mode 100644 contract_fixed_discount/__init__.py create mode 100644 contract_fixed_discount/__manifest__.py create mode 100644 contract_fixed_discount/models/__init__.py create mode 100644 contract_fixed_discount/models/abstract_contract_line.py create mode 100644 contract_fixed_discount/models/contract_line.py create mode 100644 contract_fixed_discount/pyproject.toml create mode 100644 contract_fixed_discount/readme/CONTRIBUTORS.md create mode 100644 contract_fixed_discount/readme/DESCRIPTION.md create mode 100644 contract_fixed_discount/static/description/index.html create mode 100644 contract_fixed_discount/tests/__init__.py create mode 100644 contract_fixed_discount/tests/test_contract_discount_fixed.py create mode 100644 contract_fixed_discount/tests/test_contract_invoice_discount_fixed.py create mode 100644 contract_fixed_discount/views/abstract_contract_line.xml create mode 100644 contract_fixed_discount/views/contract.xml create mode 100644 contract_fixed_discount/views/contract_line.xml diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index 8b3fa62b80..46a1741434 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -224,17 +224,27 @@ def _inverse_price_unit(self): for line in self.filtered(lambda x: not x.automatic_price): line.specific_price = line.price_unit + def _get_discounted_price_subtotal(self): + """Return the discounted price subtotal""" + subtotal = self._get_price_subtotal() + discount = self.discount / 100 + subtotal *= 1 - discount + return subtotal + + def _get_price_subtotal(self): + """Return the subtotal price (without discount)""" + self.ensure_one() + return self.quantity * self.price_unit + @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_discounted = line._get_discounted_price_subtotal() if line.contract_id.pricelist_id: cur = line.contract_id.pricelist_id.currency_id - line.price_subtotal = cur.round(subtotal) + line.price_subtotal = cur.round(subtotal_discounted) else: - line.price_subtotal = subtotal + line.price_subtotal = subtotal_discounted @api.constrains("discount") def _check_discount(self): diff --git a/contract_fixed_discount/README.rst b/contract_fixed_discount/README.rst new file mode 100644 index 0000000000..e34d850422 --- /dev/null +++ b/contract_fixed_discount/README.rst @@ -0,0 +1,80 @@ +======================= +Contract Fixed Discount +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:248ecda633fa484cca711149034c32d4933d58984763d6ff3a39084ed143afdb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/17.0/contract_fixed_discount + :alt: OCA/contract +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/contract-17-0/contract-17-0-contract_fixed_discount + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/contract&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of contracts to allow you to apply +fixed amount discounts at contract line level. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Foodles + +Contributors +------------ + +- `Foodles `__: + + - Damien Crier + - Pierre Verkest + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/contract `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. 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..777e6491a2 --- /dev/null +++ b/contract_fixed_discount/__manifest__.py @@ -0,0 +1,20 @@ +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +{ + "name": "Contract Fixed Discount", + "version": "17.0.1.0.0", + "category": "Contract Management", + "author": "Foodles, Odoo Community Association (OCA)", + "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..6bcb17261f --- /dev/null +++ b/contract_fixed_discount/models/abstract_contract_line.py @@ -0,0 +1,44 @@ +# 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 _get_discounted_price_subtotal(self): + self.ensure_one() + if self.discount_fixed: + return self._get_price_subtotal() - self.discount_fixed + return super()._get_discounted_price_subtotal() + + @api.depends("quantity", "price_unit", "discount", "discount_fixed") + def _compute_price_subtotal(self): + return 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..5f99cfab4c --- /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): + vals = super()._prepare_invoice_line() + vals["discount_fixed"] = self.discount_fixed + return vals diff --git a/contract_fixed_discount/pyproject.toml b/contract_fixed_discount/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/contract_fixed_discount/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/contract_fixed_discount/readme/CONTRIBUTORS.md b/contract_fixed_discount/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..9835ba764f --- /dev/null +++ b/contract_fixed_discount/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Foodles](https://www.foodles.co): + - Damien Crier \ + - Pierre Verkest \ diff --git a/contract_fixed_discount/readme/DESCRIPTION.md b/contract_fixed_discount/readme/DESCRIPTION.md new file mode 100644 index 0000000000..e3dda6ebf0 --- /dev/null +++ b/contract_fixed_discount/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +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/static/description/index.html b/contract_fixed_discount/static/description/index.html new file mode 100644 index 0000000000..d99d44c234 --- /dev/null +++ b/contract_fixed_discount/static/description/index.html @@ -0,0 +1,428 @@ + + + + + +Contract Fixed Discount + + + +
+

Contract Fixed Discount

+ + +

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

+

This module extends the functionality of contracts to allow you to apply +fixed amount discounts at contract line level.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Foodles
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/contract project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + 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..766a417bc2 --- /dev/null +++ b/contract_fixed_discount/tests/test_contract_discount_fixed.py @@ -0,0 +1,67 @@ +# 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.env.ref("product.group_discount_per_so_line").users = [(4, cls.env.user.id)] + + 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", + }, + ) + ], + } + ) + + cls.contract4.contract_line_ids._compute_price_subtotal() + + 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..e4b712940c --- /dev/null +++ b/contract_fixed_discount/tests/test_contract_invoice_discount_fixed.py @@ -0,0 +1,43 @@ +# 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.env.ref("product.group_discount_per_so_line").users = [(4, cls.env.user.id)] + + 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.assertEqual(invoice.invoice_line_ids.discount_fixed, 48) + self.assertEqual(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 + + + + + + + + + + From e69c2a69ed4e7ee8ce03ec991da9f8d467184271 Mon Sep 17 00:00:00 2001 From: Matthias Barkat Date: Tue, 17 Jun 2025 10:24:31 +0200 Subject: [PATCH 2/6] ADD product_contract_fixed_discount: discount_fixed propagation --- product_contract/tests/common.py | 70 +++ product_contract/tests/test_sale_order.py | 60 +-- product_contract_fixed_discount/README.rst | 93 ++++ product_contract_fixed_discount/__init__.py | 1 + .../__manifest__.py | 21 + .../models/__init__.py | 1 + .../models/sale_order_line.py | 20 + .../pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 3 + .../readme/DESCRIPTION.md | 7 + .../static/description/index.html | 434 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_sale_order.py | 33 ++ test-requirements.txt | 2 + 14 files changed, 692 insertions(+), 57 deletions(-) create mode 100644 product_contract/tests/common.py create mode 100644 product_contract_fixed_discount/README.rst create mode 100644 product_contract_fixed_discount/__init__.py create mode 100644 product_contract_fixed_discount/__manifest__.py create mode 100644 product_contract_fixed_discount/models/__init__.py create mode 100644 product_contract_fixed_discount/models/sale_order_line.py create mode 100644 product_contract_fixed_discount/pyproject.toml create mode 100644 product_contract_fixed_discount/readme/CONTRIBUTORS.md create mode 100644 product_contract_fixed_discount/readme/DESCRIPTION.md create mode 100644 product_contract_fixed_discount/static/description/index.html create mode 100644 product_contract_fixed_discount/tests/__init__.py create mode 100644 product_contract_fixed_discount/tests/test_sale_order.py create mode 100644 test-requirements.txt diff --git a/product_contract/tests/common.py b/product_contract/tests/common.py new file mode 100644 index 0000000000..cd3d62d7bc --- /dev/null +++ b/product_contract/tests/common.py @@ -0,0 +1,70 @@ +# Copyright 2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV +# Copyright 2018 Foodles +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase + + +class CommonProductContractSaleOrderCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + tracking_disable=True, + no_reset_password=True, + ) + ) + cls.partner = cls.env["res.partner"].create({"name": "Test partner"}) + cls.product1 = cls.env.ref("product.product_product_1") + cls.product2 = cls.env.ref("product.product_product_2") + cls.sale = cls.env.ref("sale.sale_order_2") + cls.contract_template1 = cls.env["contract.template"].create( + {"name": "Template 1"} + ) + cls.contract_template2 = cls.env["contract.template"].create( + { + "name": "Template 2", + "contract_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product2.id, + "name": "Services from #START# to #END#", + "quantity": 1, + "uom_id": cls.product2.uom_id.id, + "price_unit": 100, + "discount": 50, + "recurring_rule_type": "yearly", + "recurring_interval": 1, + }, + ) + ], + } + ) + cls.product1.with_company(cls.sale.company_id).write( + { + "is_contract": True, + "default_qty": 12, + "recurring_rule_type": "monthlylastday", + "recurring_invoicing_type": "post-paid", + "property_contract_template_id": cls.contract_template1.id, + } + ) + cls.product2.with_company(cls.sale.company_id).write( + { + "is_contract": True, + "property_contract_template_id": cls.contract_template2.id, + } + ) + cls.order_line1 = cls.sale.order_line.filtered( + lambda line: line.product_id == cls.product1 + ) + cls.order_line1.date_start = "2018-01-01" + cls.order_line1.product_uom_qty = 12 + cls.order_line2 = cls.sale.order_line.filtered( + lambda line: line.product_id == cls.product2 + ) diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py index 6f266b2a6b..71957f088f 100644 --- a/product_contract/tests/test_sale_order.py +++ b/product_contract/tests/test_sale_order.py @@ -8,68 +8,14 @@ from odoo import fields from odoo.exceptions import ValidationError from odoo.fields import Date -from odoo.tests.common import TransactionCase +from .common import CommonProductContractSaleOrderCase -class TestSaleOrder(TransactionCase): + +class TestSaleOrder(CommonProductContractSaleOrderCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.env = cls.env( - context=dict( - cls.env.context, - tracking_disable=True, - no_reset_password=True, - ) - ) - cls.partner = cls.env["res.partner"].create({"name": "Test partner"}) - cls.product1 = cls.env.ref("product.product_product_1") - cls.product2 = cls.env.ref("product.product_product_2") - cls.sale = cls.env.ref("sale.sale_order_2") - cls.contract_template1 = cls.env["contract.template"].create( - {"name": "Template 1"} - ) - cls.contract_template2 = cls.env["contract.template"].create( - { - "name": "Template 2", - "contract_line_ids": [ - ( - 0, - 0, - { - "product_id": cls.product2.id, - "name": "Services from #START# to #END#", - "quantity": 1, - "uom_id": cls.product2.uom_id.id, - "price_unit": 100, - "discount": 50, - "recurring_rule_type": "yearly", - "recurring_interval": 1, - }, - ) - ], - } - ) - cls.product1.with_company(cls.sale.company_id).write( - { - "is_contract": True, - "default_qty": 12, - "recurring_rule_type": "monthlylastday", - "recurring_invoicing_type": "post-paid", - "property_contract_template_id": cls.contract_template1.id, - } - ) - cls.product2.with_company(cls.sale.company_id).write( - { - "is_contract": True, - "property_contract_template_id": cls.contract_template2.id, - } - ) - cls.order_line1 = cls.sale.order_line.filtered( - lambda line: line.product_id == cls.product1 - ) - cls.order_line1.date_start = "2018-01-01" - cls.order_line1.product_uom_qty = 12 pricelist = cls.sale.partner_id.property_product_pricelist.id cls.contract = cls.env["contract.contract"].create( { diff --git a/product_contract_fixed_discount/README.rst b/product_contract_fixed_discount/README.rst new file mode 100644 index 0000000000..03ba8b8b7f --- /dev/null +++ b/product_contract_fixed_discount/README.rst @@ -0,0 +1,93 @@ +=========================================== +Recurring - Product Contract fixed discount +=========================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:34f4982f97b150f5365c171821d166442237ab33192b3ad59fa3afaaeb0e8785 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/17.0/product_contract_fixed_discount + :alt: OCA/contract +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/contract-17-0/contract-17-0-product_contract_fixed_discount + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/contract&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Glue module betweens product_contract that generate contracts from Sale +order, contract_fixed_discount that allows to manage discount with fixed +amount and sale_fixed_discount that allows to manage discount with fixed +amount. + +So this module propagate the fixed discount defined on sale order line +to the fixed discount field on contract line. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Pierre Verkest + +Contributors +------------ + +- `Foodles `__: + + - Damien Crier + - Pierre Verkest + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-petrus-v| image:: https://github.com/petrus-v.png?size=40px + :target: https://github.com/petrus-v + :alt: petrus-v + +Current `maintainer `__: + +|maintainer-petrus-v| + +This module is part of the `OCA/contract `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_contract_fixed_discount/__init__.py b/product_contract_fixed_discount/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/product_contract_fixed_discount/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_contract_fixed_discount/__manifest__.py b/product_contract_fixed_discount/__manifest__.py new file mode 100644 index 0000000000..7aece72c64 --- /dev/null +++ b/product_contract_fixed_discount/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Recurring - Product Contract fixed discount", + "version": "17.0.1.0.0", + "category": "Contract Management", + "license": "AGPL-3", + "author": "Pierre Verkest, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/contract", + "depends": [ + "product_contract", + "contract_fixed_discount", + "sale_fixed_discount", + ], + "data": [], + "installable": True, + "auto_install": True, + "application": False, + "maintainers": ["petrus-v"], +} diff --git a/product_contract_fixed_discount/models/__init__.py b/product_contract_fixed_discount/models/__init__.py new file mode 100644 index 0000000000..8eb9d1d404 --- /dev/null +++ b/product_contract_fixed_discount/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order_line diff --git a/product_contract_fixed_discount/models/sale_order_line.py b/product_contract_fixed_discount/models/sale_order_line.py new file mode 100644 index 0000000000..da40ca2892 --- /dev/null +++ b/product_contract_fixed_discount/models/sale_order_line.py @@ -0,0 +1,20 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _prepare_contract_line_values( + self, contract, predecessor_contract_line_id=False + ): + contract_line_data = super()._prepare_contract_line_values( + contract, predecessor_contract_line_id=predecessor_contract_line_id + ) + if self.discount_fixed: + contract_line_data["discount_fixed"] = self.discount_fixed + del contract_line_data["discount"] + return contract_line_data diff --git a/product_contract_fixed_discount/pyproject.toml b/product_contract_fixed_discount/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/product_contract_fixed_discount/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_contract_fixed_discount/readme/CONTRIBUTORS.md b/product_contract_fixed_discount/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..9835ba764f --- /dev/null +++ b/product_contract_fixed_discount/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Foodles](https://www.foodles.co): + - Damien Crier \ + - Pierre Verkest \ diff --git a/product_contract_fixed_discount/readme/DESCRIPTION.md b/product_contract_fixed_discount/readme/DESCRIPTION.md new file mode 100644 index 0000000000..c790e6b626 --- /dev/null +++ b/product_contract_fixed_discount/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Glue module betweens product_contract that generate contracts from Sale +order, contract_fixed_discount that allows to manage discount with fixed +amount and sale_fixed_discount that allows to manage discount with fixed +amount. + +So this module propagate the fixed discount defined on sale order line +to the fixed discount field on contract line. diff --git a/product_contract_fixed_discount/static/description/index.html b/product_contract_fixed_discount/static/description/index.html new file mode 100644 index 0000000000..feb694979d --- /dev/null +++ b/product_contract_fixed_discount/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +Recurring - Product Contract fixed discount + + + +
+

Recurring - Product Contract fixed discount

+ + +

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

+

Glue module betweens product_contract that generate contracts from Sale +order, contract_fixed_discount that allows to manage discount with fixed +amount and sale_fixed_discount that allows to manage discount with fixed +amount.

+

So this module propagate the fixed discount defined on sale order line +to the fixed discount field on contract line.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Pierre Verkest
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

petrus-v

+

This module is part of the OCA/contract project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_contract_fixed_discount/tests/__init__.py b/product_contract_fixed_discount/tests/__init__.py new file mode 100644 index 0000000000..6f699d0d8b --- /dev/null +++ b/product_contract_fixed_discount/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_order diff --git a/product_contract_fixed_discount/tests/test_sale_order.py b/product_contract_fixed_discount/tests/test_sale_order.py new file mode 100644 index 0000000000..e2b72dae01 --- /dev/null +++ b/product_contract_fixed_discount/tests/test_sale_order.py @@ -0,0 +1,33 @@ +# Copyright 2023 Foodles +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo.addons.product_contract.tests.common import CommonProductContractSaleOrderCase + + +class TestSaleOrder(CommonProductContractSaleOrderCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.order_line1.discount_fixed = 11.0 + cls.contract_template2.contract_line_ids.discount = 0 + cls.contract_template2.contract_line_ids.discount_fixed = 50 + cls.order_line2.discount = 0 + cls.order_line2.discount_fixed = 22.0 + + def test_action_confirm(self): + """It should create a contract for each contract template used in + order_line with propagated fixed_discount""" + self.order_line1._compute_auto_renew() + self.sale.action_confirm() + contracts = self.sale.order_line.mapped("contract_id") + self.assertEqual(len(contracts), 2) + contract_line = self.order_line1.contract_id.contract_line_ids + self.assertEqual(contract_line.discount_fixed, 11) + self.assertEqual(contract_line.discount_fixed, 11) + self.assertEqual( + sorted( + self.order_line2.contract_id.contract_line_ids.mapped("discount_fixed") + ), + sorted([50, 22]), + ) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000..1a8271131a --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo-addon-contract-fixed-discount @ git+https://github.com/OCA/contract.git@refs/pull/1253/head#subdirectory=contract_fixed_discount +odoo-addon-contract @ git+https://github.com/OCA/contract.git@refs/pull/1253/head#subdirectory=contract From d20a00118647d6730d1dbaff1d094bb8ef586914 Mon Sep 17 00:00:00 2001 From: Matthias Barkat Date: Thu, 19 Jun 2025 10:19:43 +0200 Subject: [PATCH 3/6] [IMP] contract: Allow to single invoice a contract by users 17.0 --- contract/__manifest__.py | 1 + contract/models/contract.py | 51 +++++- contract/security/ir.model.access.csv | 1 + contract/tests/test_contract.py | 160 ++++++++++++++---- .../test_contract_manually_create_invoice.py | 25 +++ contract/views/contract.xml | 9 +- contract/wizards/__init__.py | 1 + .../contract_manually_single_invoice.py | 15 ++ .../contract_manually_single_invoice.xml | 40 +++++ 9 files changed, 259 insertions(+), 44 deletions(-) create mode 100644 contract/wizards/contract_manually_single_invoice.py create mode 100644 contract/wizards/contract_manually_single_invoice.xml diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 4314f6d011..a2c6fedf95 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -34,6 +34,7 @@ "data/ir_ui_menu.xml", "wizards/contract_line_wizard.xml", "wizards/contract_manually_create_invoice.xml", + "wizards/contract_manually_single_invoice.xml", "wizards/contract_contract_terminate.xml", "views/contract_tag.xml", "views/abstract_contract_line.xml", diff --git a/contract/models/contract.py b/contract/models/contract.py index 0057e833fe..99cb5eaec4 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -168,6 +168,37 @@ def _compute_group_id(self): if len(set(all_analytic_accounts)) == 1: record.group_id = all_analytic_accounts[0] + def generate_invoices_manually(self, date=None): + if date is None: + date = fields.Date.today() + while ( + self.recurring_next_date + and self.recurring_next_date <= date + and (not self.date_end or self.recurring_next_date <= self.date_end) + ): + _logger.info( + f"next date {self.recurring_next_date} <= {date}, " + f"date end {self.date_end} " + ) + # We create the invoices for the contract lines + result = self.with_company(self.company_id.id)._cron_recurring_create( + self.recurring_next_date, + create_type=self.generation_type, + domain=[("id", "=", self.id)], + ) + for record_list in result: + for record in record_list: + self.message_post( + body=_( + "Contract manually generated: " + f'' + f"{record.display_name}" + "" + ) + ) + return True + def get_formview_id(self, access_uid=None): if self.contract_type == "sale": return self.env.ref("contract.contract_contract_customer_form_view").id @@ -193,6 +224,10 @@ def write(self, vals): @api.model def _set_start_contract_modification(self): subtype_id = self.env.ref("contract.mail_message_subtype_contract_modification") + _logger.warning( + "recurring_create_invoice is deprecated in favor of " + "_recurring_create_invoice instead" + ) for record in self: if record.contract_line_ids: date_start = min(record.contract_line_ids.mapped("date_start")) @@ -669,17 +704,24 @@ def _get_recurring_create_func(self, create_type="invoice"): return self.__class__._recurring_create_invoice @api.model - def _cron_recurring_create(self, date_ref=False, create_type="invoice"): + def _cron_recurring_create( + self, date_ref=False, create_type="invoice", domain=None + ): """ The cron function in order to create recurrent documents from contracts. + The domain is used to add an extra filter """ + if domain is None: + domain = [] _recurring_create_func = self._get_recurring_create_func( create_type=create_type ) if not date_ref: date_ref = fields.Date.context_today(self) - domain = self._get_contracts_to_invoice_domain(date_ref) + domain = expression.AND( + [domain, self._get_contracts_to_invoice_domain(date_ref)] + ) domain = expression.AND( [ domain, @@ -688,6 +730,7 @@ def _cron_recurring_create(self, date_ref=False, create_type="invoice"): ) contracts = self.search(domain) companies = set(contracts.mapped("company_id")) + result = [] # Invoice by companies, so assignation emails get correct context for company in companies: contracts_to_invoice = contracts.filtered( @@ -697,8 +740,8 @@ def _cron_recurring_create(self, date_ref=False, create_type="invoice"): or contract.recurring_next_date <= contract.date_end ) ).with_company(company) - _recurring_create_func(contracts_to_invoice, date_ref) - return True + result.append(_recurring_create_func(contracts_to_invoice, date_ref)) + return result @api.model def cron_recurring_create_invoice(self, date_ref=None): diff --git a/contract/security/ir.model.access.csv b/contract/security/ir.model.access.csv index 63b7beaf76..2707753629 100644 --- a/contract/security/ir.model.access.csv +++ b/contract/security/ir.model.access.csv @@ -14,3 +14,4 @@ "contract_line_wizard","contract_line_wizard","model_contract_line_wizard","account.group_account_manager",1,1,1,1 "contract_manually_create_invoice_wizard","contract_manually_create_invoice_wizard","model_contract_manually_create_invoice","account.group_account_invoice",1,1,1,1 "contract_contract_terminate_wizard","contract_contract_terminate_wizard","model_contract_contract_terminate","contract.can_terminate_contract",1,1,1,1 +"contract_manually_single_invoice_wizard","contract_manually_single_process_wizard","model_contract_manually_single_invoice","account.group_account_invoice",1,1,1,1 diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 0559e30a2b..945c4fb635 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -256,7 +256,9 @@ def test_contract(self): self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) self.acct_line.price_unit = 100.0 self.contract.partner_id = self.partner.id - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() self.invoice_monthly = self.contract._get_related_invoices() self.assertTrue(self.invoice_monthly) self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-02-15")) @@ -266,7 +268,12 @@ def test_contract(self): self.assertEqual(self.contract.user_id, self.invoice_monthly.user_id) def test_contract_level_recurrence(self): - self.contract3.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + { + "date": self.contract3.recurring_next_date, + "contract_id": self.contract3.id, + } + ).create_invoice() self.contract3.flush_recordset() def test_contract_daily(self): @@ -275,7 +282,9 @@ def test_contract_daily(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "daily" self.contract.pricelist_id = False - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoice_daily = self.contract._get_related_invoices() self.assertTrue(invoice_daily) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -294,7 +303,9 @@ def test_contract_invoice_followers(self): self.contract.message_subscribe( partner_ids=self.contract.partner_id.ids, subtype_ids=subtype_ids ) - self.contract._recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoice_daily = self.contract._get_related_invoices() self.assertTrue(invoice_daily) self.assertTrue(self.contract.partner_id in invoice_daily.message_partner_ids) @@ -306,7 +317,9 @@ def test_contract_invoice_salesperson(self): {"name": "Some Salesperson", "login": "salesperson_test"} ) self.contract.user_id = new_salesperson - self.contract._recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoice_daily = self.contract._get_related_invoices() self.assertTrue(invoice_daily) self.assertEqual(self.contract.user_id, invoice_daily.user_id) @@ -318,7 +331,9 @@ def test_contract_weekly_post_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "weekly" self.acct_line.recurring_invoicing_type = "post-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -330,7 +345,9 @@ def test_contract_weekly_pre_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "weekly" self.acct_line.recurring_invoicing_type = "pre-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -342,7 +359,9 @@ def test_contract_yearly_post_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "yearly" self.acct_line.recurring_invoicing_type = "post-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -355,7 +374,9 @@ def test_contract_yearly_pre_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "yearly" self.acct_line.recurring_invoicing_type = "pre-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -367,7 +388,9 @@ def test_contract_monthly_lastday(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.recurring_rule_type = "monthlylastday" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_monthly_lastday = self.contract._get_related_invoices() self.assertTrue(invoices_monthly_lastday) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -380,7 +403,9 @@ def test_contract_quarterly_pre_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "quarterly" self.acct_line.recurring_invoicing_type = "pre-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -393,7 +418,9 @@ def test_contract_quarterly_post_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "quarterly" self.acct_line.recurring_invoicing_type = "post-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -406,7 +433,9 @@ def test_contract_semesterly_pre_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "semesterly" self.acct_line.recurring_invoicing_type = "pre-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -419,7 +448,9 @@ def test_contract_semesterly_post_paid(self): self.acct_line.recurring_next_date = "2018-02-22" self.acct_line.recurring_rule_type = "semesterly" self.acct_line.recurring_invoicing_type = "post-paid" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) @@ -499,7 +530,12 @@ def test_check_journal(self): journal = self.env["account.journal"].search([("type", "=", "sale")]) journal.write({"type": "general"}) with self.assertRaises(ValidationError): - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + { + "date": self.contract.recurring_next_date, + "contract_id": self.contract.id, + } + ).create_invoice() def test_check_date_end(self): with self.assertRaises(ValidationError): @@ -1090,7 +1126,9 @@ def _error_message( def test_recurring_next_date(self): """recurring next date for a contract is the min for all lines""" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() self.assertEqual( self.contract.recurring_next_date, min(self.contract.contract_line_ids.mapped("recurring_next_date")), @@ -1746,14 +1784,18 @@ def test_get_period_to_invoice_monthlylastday_postpaid(self): ) self.assertEqual(first, to_date("2018-01-05")) self.assertEqual(last, to_date("2018-01-31")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) self.assertEqual(first, to_date("2018-02-01")) self.assertEqual(last, to_date("2018-02-28")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1775,7 +1817,9 @@ def test_get_period_to_invoice_monthlylastday_prepaid(self): self.assertEqual(last, to_date("2018-01-31")) self.assertEqual(recurring_next_date, to_date("2018-01-05")) self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-01-05")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1785,7 +1829,9 @@ def test_get_period_to_invoice_monthlylastday_prepaid(self): self.assertEqual(recurring_next_date, to_date("2018-02-01")) self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-02-01")) self.assertEqual(self.acct_line.last_date_invoiced, to_date("2018-01-31")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1795,7 +1841,9 @@ def test_get_period_to_invoice_monthlylastday_prepaid(self): self.assertEqual(recurring_next_date, to_date("2018-03-01")) self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-03-01")) self.assertEqual(self.acct_line.last_date_invoiced, to_date("2018-02-28")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1811,7 +1859,9 @@ def test_get_period_to_invoice_monthly_pre_paid_2(self): self.acct_line.recurring_invoicing_type = "pre-paid" self.acct_line.recurring_rule_type = "monthly" self.acct_line.date_end = "2018-08-15" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1831,7 +1881,9 @@ def test_get_period_to_invoice_monthly_post_paid_2(self): self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.recurring_rule_type = "monthly" self.acct_line.date_end = "2018-08-15" - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1857,14 +1909,18 @@ def test_get_period_to_invoice_monthly_post_paid(self): ) self.assertEqual(first, to_date("2018-01-05")) self.assertEqual(last, to_date("2018-02-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) self.assertEqual(first, to_date("2018-02-05")) self.assertEqual(last, to_date("2018-03-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1883,14 +1939,18 @@ def test_get_period_to_invoice_monthly_pre_paid(self): ) self.assertEqual(first, to_date("2018-01-05")) self.assertEqual(last, to_date("2018-02-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) self.assertEqual(first, to_date("2018-02-05")) self.assertEqual(last, to_date("2018-03-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1909,14 +1969,18 @@ def test_get_period_to_invoice_yearly_post_paid(self): ) self.assertEqual(first, to_date("2018-01-05")) self.assertEqual(last, to_date("2019-01-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) self.assertEqual(first, to_date("2019-01-05")) self.assertEqual(last, to_date("2020-01-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1935,14 +1999,18 @@ def test_get_period_to_invoice_yearly_pre_paid(self): ) self.assertEqual(first, to_date("2018-01-05")) self.assertEqual(last, to_date("2019-01-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) self.assertEqual(first, to_date("2019-01-05")) self.assertEqual(last, to_date("2020-01-04")) - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -2185,9 +2253,15 @@ def test_sale_get_view(self): self.assertEqual(view["id"], sale_form_view.id) def test_contract_count_invoice(self): - self.contract.recurring_create_invoice() - self.contract.recurring_create_invoice() - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() self.contract._compute_invoice_count() self.assertEqual(self.contract.invoice_count, 3) @@ -2224,9 +2298,21 @@ def test_invoice_contract_without_lines(self): self.contract.contract_line_ids.cancel() self.contract.contract_line_ids.unlink() self.assertFalse(self.contract.recurring_create_invoice()) + # self.assertFalse( + # self.env["contract.manually.single.invoice"] + # .create( + # { + # "date": self.contract.recurring_next_date, + # "contract_id": self.contract.id, + # } + # ) + # .create_invoice() + # ) def test_stop_at_last_date_invoiced(self): - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() self.assertTrue(self.acct_line.recurring_next_date) self.acct_line.stop(self.acct_line.last_date_invoiced) self.assertFalse(self.acct_line.recurring_next_date) @@ -2285,7 +2371,9 @@ def test_action_terminate_contract(self): self.assertFalse(self.contract.terminate_comment) def test_terminate_date_before_last_date_invoiced(self): - self.contract.recurring_create_invoice() + self.env["contract.manually.single.invoice"].create( + {"date": self.contract.recurring_next_date, "contract_id": self.contract.id} + ).create_invoice() self.assertEqual(self.acct_line.last_date_invoiced, to_date("2018-02-14")) group_can_terminate_contract = self.env.ref("contract.can_terminate_contract") group_can_terminate_contract.users |= self.env.user diff --git a/contract/tests/test_contract_manually_create_invoice.py b/contract/tests/test_contract_manually_create_invoice.py index b8a5fd537d..1c33e062cc 100644 --- a/contract/tests/test_contract_manually_create_invoice.py +++ b/contract/tests/test_contract_manually_create_invoice.py @@ -45,6 +45,31 @@ def test_contract_manually_create_invoice(self): self.assertFalse(invoice_lines.mapped("move_id") - invoices) self.assertEqual(len(invoices), contract_to_invoice_count) + def test_contract_manually_single_contract(self): + contracts = self.env["contract.contract"] + for _i in range(10): + contracts |= self.contract.copy() + wizard = self.env["contract.manually.single.invoice"].create( + {"date": self.today, "contract_id": self.contract.id} + ) + wizard.create_invoice() + invoice_lines = self.env["account.move.line"].search( + [("contract_line_id", "in", contracts.mapped("contract_line_ids").ids)] + ) + self.assertEqual( + 0, + len(invoice_lines), + ) + invoice_lines = self.env["account.move.line"].search( + [("contract_line_id", "in", self.contract.mapped("contract_line_ids").ids)] + ) + self.assertEqual( + 2, + len(invoice_lines), + ) + # Two invoices are available from to 2018-1-1 2018-3-15. + # We are invoicing at the end of the month + def test_contract_manually_create_invoice_with_usererror(self): contracts = self.contract diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 0cac09f3c6..3366100f2a 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -39,10 +39,11 @@ groups="base.group_user" />