From 8897fc0234abf6a80176922c62f2ba9ea8014c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Mangin?= Date: Tue, 27 Oct 2026 10:31:50 +0100 Subject: [PATCH 1/2] [REF] contract_sale_generation: increase extendability - add a hook to prepare the sale values of one contract --- contract_sale_generation/README.rst | 1 + contract_sale_generation/models/contract.py | 34 +++++++++++-------- .../readme/CONTRIBUTORS.md | 1 + .../static/description/index.html | 1 + .../tests/test_contract_sale.py | 11 ++++++ contract_sale_generation/views/contract.xml | 5 ++- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/contract_sale_generation/README.rst b/contract_sale_generation/README.rst index 3a4f7c8aeb..9bb75e20f5 100644 --- a/contract_sale_generation/README.rst +++ b/contract_sale_generation/README.rst @@ -78,6 +78,7 @@ Contributors - Florent THOMAS - Serpent Consulting Services Pvt. Ltd. - Denis Roussel +- Stéphane Mangin Maintainers ----------- diff --git a/contract_sale_generation/models/contract.py b/contract_sale_generation/models/contract.py index 334cd50229..1987bd357b 100644 --- a/contract_sale_generation/models/contract.py +++ b/contract_sale_generation/models/contract.py @@ -92,23 +92,27 @@ def _prepare_recurring_sales_values(self, date_ref=False): # this use case is possible when recurring_create_invoice is # called for a finished contract continue - contract_lines = contract._get_lines_to_invoice(date_ref) - if not contract_lines: - continue - sale_values = contract._prepare_sale(date_ref) - for line in contract_lines: - sale_values.setdefault("order_line", []) - invoice_line_values = line._prepare_sale_line( - sale_values=sale_values, - ) - if invoice_line_values: - sale_values["order_line"].append( - Command.create(invoice_line_values) - ) - sales_values.append(sale_values) - contract_lines._update_last_date_invoiced() + sale_values = contract._prepare_recurring_sale_values(date_ref) + if sale_values: + sales_values.append(sale_values) return sales_values + def _prepare_recurring_sale_values(self, date_ref): + self.ensure_one() + contract_lines = self._get_lines_to_invoice(date_ref) + if not contract_lines: + return {} + sale_values = self._prepare_sale(date_ref) + for line in contract_lines: + sale_values.setdefault("order_line", []) + invoice_line_values = line._prepare_sale_line( + sale_values=sale_values, + ) + if invoice_line_values: + sale_values["order_line"].append(Command.create(invoice_line_values)) + contract_lines._update_last_date_invoiced() + return sale_values + def _recurring_create_sale(self, date_ref=False): sales_values = self._prepare_recurring_sales_values(date_ref) sale_orders = self.env["sale.order"].create(sales_values) diff --git a/contract_sale_generation/readme/CONTRIBUTORS.md b/contract_sale_generation/readme/CONTRIBUTORS.md index c3d885fe39..4281858638 100644 --- a/contract_sale_generation/readme/CONTRIBUTORS.md +++ b/contract_sale_generation/readme/CONTRIBUTORS.md @@ -2,3 +2,4 @@ - Florent THOMAS \ - Serpent Consulting Services Pvt. Ltd. \ - Denis Roussel \ +- Stéphane Mangin \ diff --git a/contract_sale_generation/static/description/index.html b/contract_sale_generation/static/description/index.html index 634b6cc8a9..c7c1f36a1b 100644 --- a/contract_sale_generation/static/description/index.html +++ b/contract_sale_generation/static/description/index.html @@ -427,6 +427,7 @@

Contributors

  • Florent THOMAS <florent.thomas@mind-and-go.com>
  • Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>
  • Denis Roussel <denis.roussel@acsone.eu>
  • +
  • Stéphane Mangin <stephane.mangin@acsone.eu>
  • diff --git a/contract_sale_generation/tests/test_contract_sale.py b/contract_sale_generation/tests/test_contract_sale.py index befd325e1f..c91b171cb1 100644 --- a/contract_sale_generation/tests/test_contract_sale.py +++ b/contract_sale_generation/tests/test_contract_sale.py @@ -3,6 +3,8 @@ # Copyright 2017 Angel Moya # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from unittest.mock import patch + from odoo import Command from odoo.exceptions import ValidationError @@ -136,6 +138,15 @@ def test__prepare_recurring_sales_values_no_contract_lines(self): ) self.assertEqual(a_contract_with_no_lines._prepare_recurring_sales_values(), []) + def test__prepare_recurring_sale_values_no_sale_line(self): + with patch.object( + type(self.contract_line), "_prepare_sale_line", return_value={} + ): + sale_values = self.contract._prepare_recurring_sale_values( + self.contract.recurring_next_date + ) + self.assertEqual(sale_values["order_line"], []) + def test__prepare_sale_line_vals_with_order_id(self): order = self.contract.recurring_create_sale()[0] recurring_next_date = self.contract.recurring_next_date diff --git a/contract_sale_generation/views/contract.xml b/contract_sale_generation/views/contract.xml index 6afd794a0b..e4094c1556 100644 --- a/contract_sale_generation/views/contract.xml +++ b/contract_sale_generation/views/contract.xml @@ -4,7 +4,10 @@ contract.contract - + From 75faef55c84743cf1b6c1d751d113b4ff2419a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Mangin?= Date: Tue, 27 Oct 2026 12:46:24 +0100 Subject: [PATCH 2/2] [IMP] contract_sale_generation: link sale orders to their contract - add the contract on the generated sale orders - find related sales and auto confirmation from it --- contract_sale_generation/__manifest__.py | 2 +- .../migrations/19.0.1.1.0/post-migration.py | 14 ++++++++++++++ contract_sale_generation/models/contract.py | 7 ++----- contract_sale_generation/models/sale_order.py | 6 ++---- .../tests/test_contract_sale.py | 15 +++++++++++++++ 5 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 contract_sale_generation/migrations/19.0.1.1.0/post-migration.py diff --git a/contract_sale_generation/__manifest__.py b/contract_sale_generation/__manifest__.py index 616e548064..3c80bd140a 100644 --- a/contract_sale_generation/__manifest__.py +++ b/contract_sale_generation/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Contracts Management - Recurring Sales", - "version": "19.0.1.0.0", + "version": "19.0.1.1.0", "category": "Contract Management", "license": "AGPL-3", "author": "ACSONE SA/NV, PESOL, Odoo Community Association (OCA)", diff --git a/contract_sale_generation/migrations/19.0.1.1.0/post-migration.py b/contract_sale_generation/migrations/19.0.1.1.0/post-migration.py new file mode 100644 index 0000000000..a1c4c510fb --- /dev/null +++ b/contract_sale_generation/migrations/19.0.1.1.0/post-migration.py @@ -0,0 +1,14 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def migrate(cr, version): + cr.execute( + """ + UPDATE sale_order sale + SET contract_id = contract_line.contract_id + FROM sale_order_line sale_line + JOIN contract_line ON contract_line.id = sale_line.contract_line_id + WHERE sale_line.order_id = sale.id + """ + ) diff --git a/contract_sale_generation/models/contract.py b/contract_sale_generation/models/contract.py index 1987bd357b..e8e7d7b42e 100644 --- a/contract_sale_generation/models/contract.py +++ b/contract_sale_generation/models/contract.py @@ -25,6 +25,7 @@ def _prepare_sale(self, date_ref): "partner_id": self.partner_id, "date_order": date_ref, "origin": self.name, + "contract_id": self.id, "company_id": self.company_id.id, "user_id": self.partner_id.user_id.id, } @@ -37,11 +38,7 @@ def _prepare_sale(self, date_ref): def _get_related_sales(self): self.ensure_one() - sales = ( - self.env["sale.order.line"] - .search([("contract_line_id", "in", self.contract_line_ids.ids)]) - .mapped("order_id") - ) + sales = self.env["sale.order"].search([("contract_id", "in", self.ids)]) return sales def _compute_sale_count(self): diff --git a/contract_sale_generation/models/sale_order.py b/contract_sale_generation/models/sale_order.py index fb94846778..a6944f1e77 100644 --- a/contract_sale_generation/models/sale_order.py +++ b/contract_sale_generation/models/sale_order.py @@ -7,6 +7,7 @@ class SaleOrder(models.Model): _inherit = "sale.order" + contract_id = fields.Many2one("contract.contract", index=True) contract_auto_confirm = fields.Boolean( compute="_compute_contract_auto_confirm", help="This is a technical field in order to know if the order should" @@ -15,10 +16,7 @@ class SaleOrder(models.Model): def _compute_contract_auto_confirm(self): sale_auto_confirm = self.filtered( - lambda sale: any( - line.contract_line_id.contract_id.sale_autoconfirm - for line in sale.order_line - ) + lambda sale: sale.contract_id.sale_autoconfirm ) sale_auto_confirm.contract_auto_confirm = True (self - sale_auto_confirm).contract_auto_confirm = False diff --git a/contract_sale_generation/tests/test_contract_sale.py b/contract_sale_generation/tests/test_contract_sale.py index c91b171cb1..b9c3c2776d 100644 --- a/contract_sale_generation/tests/test_contract_sale.py +++ b/contract_sale_generation/tests/test_contract_sale.py @@ -147,6 +147,21 @@ def test__prepare_recurring_sale_values_no_sale_line(self): ) self.assertEqual(sale_values["order_line"], []) + def test_related_sales_without_contract_line(self): + with patch.object( + type(self.contract_line), "_prepare_sale_line", return_value={} + ): + sales = self.contract._recurring_create_sale() + self.assertEqual(self.contract._get_related_sales(), sales) + + def test_autoconfirm_without_contract_line(self): + self.contract.sale_autoconfirm = True + with patch.object( + type(self.contract_line), "_prepare_sale_line", return_value={} + ): + sales = self.contract._recurring_create_sale() + self.assertEqual(sales.state, "sale") + def test__prepare_sale_line_vals_with_order_id(self): order = self.contract.recurring_create_sale()[0] recurring_next_date = self.contract.recurring_next_date