From 7baddd8f420d7de61912a4cc6dacd2ffbc99d3c4 Mon Sep 17 00:00:00 2001
From: sbejaoui <souheil.bejaoui@acsone.eu>
Date: Fri, 15 Nov 2024 21:18:39 +0100
Subject: [PATCH] [IMP] product_contract: add Recurrence Number to compute
 start and end dates

---
 product_contract/models/__init__.py           |   5 +-
 product_contract/models/product_template.py   |   2 +-
 product_contract/models/sale_order_line.py    | 109 +---------
 .../models/sale_order_line_contract_mixin.py  | 202 ++++++++++++++++++
 .../contract_configurator_controller.esm.js   |   4 +
 .../static/src/js/sale_product_field.esm.js   |   4 +
 product_contract/tests/test_sale_order.py     |   6 +-
 product_contract/views/sale_order.xml         |   2 +
 .../wizards/product_contract_configurator.py  | 104 +--------
 .../product_contract_configurator_views.xml   |   3 +-
 10 files changed, 223 insertions(+), 218 deletions(-)
 create mode 100644 product_contract/models/sale_order_line_contract_mixin.py

diff --git a/product_contract/models/__init__.py b/product_contract/models/__init__.py
index c16c95bb2c..93f0b22ea2 100644
--- a/product_contract/models/__init__.py
+++ b/product_contract/models/__init__.py
@@ -1,11 +1,8 @@
-# Copyright 2017 LasLabs Inc.
-# Copyright 2018 ACSONE SA/NV
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-
 from . import contract
 from . import contract_line
 from . import product_template
 from . import sale_order
+from . import sale_order_line_contract_mixin
 from . import sale_order_line
 from . import res_company
 from . import res_config_settings
diff --git a/product_contract/models/product_template.py b/product_contract/models/product_template.py
index 12a0bd9169..7a4be429d4 100644
--- a/product_contract/models/product_template.py
+++ b/product_contract/models/product_template.py
@@ -15,7 +15,7 @@ class ProductTemplate(models.Model):
         string="Contract Template",
         company_dependent=True,
     )
-    default_qty = fields.Integer(string="Default Quantity", default=1)
+    default_qty = fields.Integer(string="Recurrence Number", default=1)
     recurring_rule_type = fields.Selection(
         [
             ("daily", "Day(s)"),
diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py
index 96d5ebdaa8..7cd316c79c 100644
--- a/product_contract/models/sale_order_line.py
+++ b/product_contract/models/sale_order_line.py
@@ -16,64 +16,8 @@
 
 
 class SaleOrderLine(models.Model):
-    _inherit = "sale.order.line"
-
-    is_contract = fields.Boolean(
-        string="Is a contract", related="product_id.is_contract"
-    )
-    contract_id = fields.Many2one(
-        comodel_name="contract.contract", string="Contract", copy=False
-    )
-    contract_template_id = fields.Many2one(
-        comodel_name="contract.template",
-        string="Contract Template",
-        compute="_compute_contract_template_id",
-    )
-    recurring_rule_type = fields.Selection(related="product_id.recurring_rule_type")
-    recurring_invoicing_type = fields.Selection(
-        related="product_id.recurring_invoicing_type"
-    )
-    date_start = fields.Date()
-    date_end = fields.Date()
-
-    contract_line_id = fields.Many2one(
-        comodel_name="contract.line",
-        string="Contract Line to replace",
-        required=False,
-        copy=False,
-    )
-    is_auto_renew = fields.Boolean(
-        string="Auto Renew",
-        compute="_compute_auto_renew",
-        default=False,
-        store=True,
-        readonly=False,
-    )
-    auto_renew_interval = fields.Integer(
-        default=1,
-        string="Renew Every",
-        compute="_compute_auto_renew",
-        store=True,
-        readonly=False,
-        help="Renew every (Days/Week/Month/Year)",
-    )
-    auto_renew_rule_type = fields.Selection(
-        [
-            ("daily", "Day(s)"),
-            ("weekly", "Week(s)"),
-            ("monthly", "Month(s)"),
-            ("yearly", "Year(s)"),
-        ],
-        default="yearly",
-        compute="_compute_auto_renew",
-        store=True,
-        readonly=False,
-        string="Renewal type",
-        help="Specify Interval for automatic renewal.",
-    )
-    contract_start_date_method = fields.Selection(
-        related="product_id.contract_start_date_method"
-    )
+    _name = "sale.order.line"
+    _inherit = ["sale.order.line", "sale.order.line.contract.mixin"]
 
     @api.constrains("contract_id")
     def _check_contact_is_not_terminated(self):
@@ -86,55 +30,6 @@ def _check_contact_is_not_terminated(self):
                     _("You can't upsell or downsell a terminated contract")
                 )
 
-    @api.depends("product_id", "order_id.company_id")
-    def _compute_contract_template_id(self):
-        for rec in self:
-            rec.contract_template_id = rec.product_id.with_company(
-                rec.order_id.company_id
-            ).property_contract_template_id
-
-    def _get_auto_renew_rule_type(self):
-        """monthly last day don't make sense for auto_renew_rule_type"""
-        self.ensure_one()
-        if self.recurring_rule_type == "monthlylastday":
-            return "monthly"
-        return self.recurring_rule_type
-
-    def _get_date_end(self):
-        self.ensure_one()
-        contract_start_date_method = self.product_id.contract_start_date_method
-        date_end = False
-        if contract_start_date_method == "manual":
-            contract_line_model = self.env["contract.line"]
-            date_end = (
-                self.date_start
-                + contract_line_model.get_relative_delta(
-                    self._get_auto_renew_rule_type(),
-                    int(self.product_uom_qty),
-                )
-                - relativedelta(days=1)
-            )
-        return date_end
-
-    @api.depends("product_id")
-    def _compute_auto_renew(self):
-        for rec in self:
-            if rec.product_id.is_contract:
-                rec.product_uom_qty = rec.product_id.default_qty
-                contract_start_date_method = rec.product_id.contract_start_date_method
-                if contract_start_date_method == "manual":
-                    rec.date_start = rec.date_start or fields.Date.today()
-                rec.date_end = rec._get_date_end()
-                rec.is_auto_renew = rec.product_id.is_auto_renew
-                if rec.is_auto_renew:
-                    rec.auto_renew_interval = rec.product_id.auto_renew_interval
-                    rec.auto_renew_rule_type = rec.product_id.auto_renew_rule_type
-
-    @api.onchange("date_start", "product_uom_qty")
-    def onchange_date_start(self):
-        for rec in self.filtered("product_id.is_contract"):
-            rec.date_end = rec._get_date_end() if rec.date_start else False
-
     def _get_contract_line_qty(self):
         """Returns the amount that will be placed in new contract lines."""
         self.ensure_one()
diff --git a/product_contract/models/sale_order_line_contract_mixin.py b/product_contract/models/sale_order_line_contract_mixin.py
new file mode 100644
index 0000000000..4333c9d7c5
--- /dev/null
+++ b/product_contract/models/sale_order_line_contract_mixin.py
@@ -0,0 +1,202 @@
+# Copyright 2024 ACSONE SA/NV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from dateutil.relativedelta import relativedelta
+
+from odoo import api, fields, models
+
+
+class SaleOrderLineContractMixin(models.AbstractModel):
+    _name = "sale.order.line.contract.mixin"
+    _description = "Sale Order Line Contract Mixin"
+
+    is_contract = fields.Boolean(
+        string="Is a contract", related="product_id.is_contract"
+    )
+    product_id = fields.Many2one("product.product")
+    partner_id = fields.Many2one("res.partner")
+    company_id = fields.Many2one("res.company")
+    product_uom_qty = fields.Float("Quantity")
+    contract_id = fields.Many2one(comodel_name="contract.contract", string="Contract")
+    contract_template_id = fields.Many2one(
+        comodel_name="contract.template",
+        string="Contract Template",
+        compute="_compute_contract_template_id",
+    )
+    recurrence_number = fields.Integer(
+        compute="_compute_product_contract_data",
+        precompute=True,
+        store=True,
+        readonly=False,
+    )
+    recurring_rule_type = fields.Selection(
+        [
+            ("daily", "Day(s)"),
+            ("weekly", "Week(s)"),
+            ("monthly", "Month(s)"),
+            ("monthlylastday", "Month(s) last day"),
+            ("quarterly", "Quarter(s)"),
+            ("semesterly", "Semester(s)"),
+            ("yearly", "Year(s)"),
+        ],
+        default="monthly",
+        string="Recurrence",
+        help="Specify Interval for automatic invoice generation.",
+        compute="_compute_product_contract_data",
+        precompute=True,
+        store=True,
+        readonly=False,
+    )
+    recurring_invoicing_type = fields.Selection(
+        [("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")],
+        default="pre-paid",
+        string="Invoicing type",
+        help=(
+            "Specify if the invoice must be generated at the beginning "
+            "(pre-paid) or end (post-paid) of the period."
+        ),
+        compute="_compute_product_contract_data",
+        precompute=True,
+        store=True,
+        readonly=False,
+    )
+    date_start = fields.Date(
+        compute="_compute_contract_line_date",
+        store=True,
+        readonly=False,
+    )
+    date_end = fields.Date(
+        compute="_compute_contract_line_date",
+        store=True,
+        readonly=False,
+    )
+    contract_line_id = fields.Many2one(
+        comodel_name="contract.line",
+        string="Contract Line to replace",
+        required=False,
+    )
+    is_auto_renew = fields.Boolean(
+        string="Auto Renew",
+        compute="_compute_product_contract_data",
+        precompute=True,
+        default=False,
+        store=True,
+        readonly=False,
+    )
+    auto_renew_interval = fields.Integer(
+        default=1,
+        string="Renew Every",
+        compute="_compute_product_contract_data",
+        precompute=True,
+        store=True,
+        readonly=False,
+        help="Renew every (Days/Week/Month/Year)",
+    )
+    auto_renew_rule_type = fields.Selection(
+        [
+            ("daily", "Day(s)"),
+            ("weekly", "Week(s)"),
+            ("monthly", "Month(s)"),
+            ("yearly", "Year(s)"),
+        ],
+        default="yearly",
+        compute="_compute_product_contract_data",
+        precompute=True,
+        store=True,
+        readonly=False,
+        string="Renewal type",
+        help="Specify Interval for automatic renewal.",
+    )
+    contract_start_date_method = fields.Selection(
+        [
+            ("manual", "Manual"),
+            ("start_this", "Start of current period"),
+            ("end_this", "End of current period"),
+            ("start_next", "Start of next period"),
+            ("end_next", "End of next period"),
+        ],
+        "Start Date Method",
+        default="manual",
+        help="""This field allows to define how the start date of the contract will
+        be calculated:
+
+        - Manual: The start date will be selected by the user, by default will be the
+        date of sale confirmation.
+        - Start of current period: The start date will be the first day of the actual
+        period selected on 'Invoicing Every' field. Example: If we are on 2024/08/27
+        and the period selected is 'Year(s)' the start date will be 2024/01/01.
+        - End of current period: The start date will be the last day of the actual
+        period selected on 'Invoicing Every' field. Example: If we are on 2024/08/27
+        and the period selected is 'Year(s)' the start date will be 2024/12/31.
+        - Start of next period: The start date will be the first day of the next
+        period selected on 'Invoicing Every' field. Example: If we are on 2024/08/27
+        and the period selected is 'Year(s)' the start date will be 2025/01/01.
+        - End of next period: The start date will be the last day of the actual
+        period selected on 'Invoicing Every' field. Example: If we are on 2024/08/27
+        and the period selected is 'Year(s)' the start date will be 2025/12/31.
+            """,
+        compute="_compute_product_contract_data",
+        precompute=True,
+        store=True,
+        readonly=False,
+    )
+
+    @api.depends("product_id", "company_id")
+    def _compute_contract_template_id(self):
+        for rec in self:
+            rec.contract_template_id = rec.product_id.with_company(
+                rec.company_id
+            ).property_contract_template_id
+
+    @api.depends("product_id")
+    def _compute_product_contract_data(self):
+        for rec in self:
+            vals = {
+                "recurrence_number": 0,
+                "recurring_rule_type": False,
+                "recurring_invoicing_type": False,
+                "is_auto_renew": False,
+                "auto_renew_interval": False,
+                "auto_renew_rule_type": False,
+                "contract_start_date_method": False,
+            }
+            if rec.product_id.is_contract:
+                p = rec.product_id
+                vals = {
+                    "recurrence_number": p.default_qty,
+                    "recurring_rule_type": p.recurring_rule_type,
+                    "recurring_invoicing_type": p.recurring_invoicing_type,
+                    "is_auto_renew": p.is_auto_renew,
+                    "auto_renew_interval": p.auto_renew_interval,
+                    "auto_renew_rule_type": p.auto_renew_rule_type,
+                    "contract_start_date_method": p.contract_start_date_method,
+                }
+            rec.update(vals)
+
+    @api.depends(
+        "recurring_rule_type", "recurrence_number", "contract_start_date_method"
+    )
+    def _compute_contract_line_date(self):
+        for rec in self:
+            if rec.contract_start_date_method == "manual":
+                rec.date_start = rec.date_start or fields.Date.today()
+            rec.date_end = rec._get_date_end() if rec.date_start else False
+
+    def _get_auto_renew_rule_type(self):
+        """monthly last day don't make sense for auto_renew_rule_type"""
+        self.ensure_one()
+        if self.recurring_rule_type == "monthlylastday":
+            return "monthly"
+        return self.recurring_rule_type
+
+    def _get_date_end(self):
+        self.ensure_one()
+        contract_line_model = self.env["contract.line"]
+        date_end = (
+            self.date_start
+            + contract_line_model.get_relative_delta(
+                self._get_auto_renew_rule_type(), self.recurrence_number
+            )
+            - relativedelta(days=1)
+        )
+        return date_end
diff --git a/product_contract/static/src/js/contract_configurator_controller.esm.js b/product_contract/static/src/js/contract_configurator_controller.esm.js
index 77e6e56e3d..c742a94df5 100644
--- a/product_contract/static/src/js/contract_configurator_controller.esm.js
+++ b/product_contract/static/src/js/contract_configurator_controller.esm.js
@@ -14,6 +14,8 @@ export class ProductContractConfiguratorController extends formView.Controller {
         await super.onRecordSaved(...arguments);
         const {
             product_uom_qty,
+            recurrence_number,
+            recurring_rule_type,
             contract_id,
             date_start,
             date_end,
@@ -27,6 +29,8 @@ export class ProductContractConfiguratorController extends formView.Controller {
             infos: {
                 productContractConfiguration: {
                     product_uom_qty,
+                    recurrence_number,
+                    recurring_rule_type,
                     contract_id,
                     date_start,
                     date_end,
diff --git a/product_contract/static/src/js/sale_product_field.esm.js b/product_contract/static/src/js/sale_product_field.esm.js
index 856489414d..49f34041d4 100644
--- a/product_contract/static/src/js/sale_product_field.esm.js
+++ b/product_contract/static/src/js/sale_product_field.esm.js
@@ -24,9 +24,13 @@ patch(SaleOrderLineProductField.prototype, {
 
     async _openContractConfigurator(isNew = false) {
         const actionContext = {
+            active_model: this.props.record.resModel,
+            active_id: this.props.record.resId,
             default_product_id: this.props.record.data.product_id[0],
             default_partner_id: this.props.record.model.root.data.partner_id[0],
             default_company_id: this.props.record.model.root.data.company_id[0],
+            default_recurrence_number: this.props.record.data.recurrence_number,
+            default_recurring_rule_type: this.props.record.data.recurring_rule_type,
             default_product_uom_qty: this.props.record.data.product_uom_qty,
             default_contract_id: this.props.record.data.contract_id[0],
             default_date_start: this.props.record.data.date_start,
diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py
index 434f54a5c2..9a0177de91 100644
--- a/product_contract/tests/test_sale_order.py
+++ b/product_contract/tests/test_sale_order.py
@@ -69,7 +69,7 @@ def setUpClass(cls):
             lambda line: line.product_id == cls.product1
         )
         cls.order_line1.date_start = "2018-01-01"
-        cls.order_line1.product_uom_qty = 12
+        cls.order_line1.recurrence_number = 12
         pricelist = cls.sale.partner_id.property_product_pricelist.id
         cls.contract = cls.env["contract.contract"].create(
             {
@@ -384,11 +384,11 @@ def _create_contract_product(
                 "is_contract": True,
                 "recurring_rule_type": recurring_rule_type,
                 "contract_start_date_method": contract_start_date_method,
-                "property_contract_template_id": self.contract_template1,
+                "property_contract_template_id": self.contract_template1.id,
             }
         )
         if recurring_rule_type != "monthly":
-            product["force_month_%s" % recurring_rule_type] = force_month
+            product[f"force_month_{recurring_rule_type}"] = force_month
         return product
 
     def _create_and_confirm_sale(self, product):
diff --git a/product_contract/views/sale_order.xml b/product_contract/views/sale_order.xml
index e21ee65a9d..e9874831e8 100644
--- a/product_contract/views/sale_order.xml
+++ b/product_contract/views/sale_order.xml
@@ -62,6 +62,7 @@
                     invisible="not is_contract"
                 />
                 <group invisible="not is_contract">
+                    <field name="recurrence_number" />
                     <field name="recurring_rule_type" />
                 </group>
                 <group invisible="not is_contract">
@@ -112,6 +113,7 @@
                     domain="[('contract_id','=',contract_id)]"
                     optional="hide"
                 />
+                <field name="recurrence_number" optional="hide" />
                 <field name="recurring_rule_type" optional="hide" />
                 <field name="recurring_invoicing_type" optional="hide" />
                 <field name="contract_start_date_method" column_invisible="1" />
diff --git a/product_contract/wizards/product_contract_configurator.py b/product_contract/wizards/product_contract_configurator.py
index 4c6a63252f..aec9465fea 100644
--- a/product_contract/wizards/product_contract_configurator.py
+++ b/product_contract/wizards/product_contract_configurator.py
@@ -1,111 +1,11 @@
 # Copyright 2024 Tecnativa - Carlos Roca
 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
 
-from dateutil.relativedelta import relativedelta
 
-from odoo import api, fields, models
+from odoo import models
 
 
 class ProductContractConfigurator(models.TransientModel):
     _name = "product.contract.configurator"
+    _inherit = "sale.order.line.contract.mixin"
     _description = "Product Contract Configurator Wizard"
-
-    product_id = fields.Many2one("product.product")
-    partner_id = fields.Many2one("res.partner")
-    company_id = fields.Many2one("res.company")
-    product_uom_qty = fields.Float("Quantity")
-    contract_id = fields.Many2one(comodel_name="contract.contract", string="Contract")
-    contract_template_id = fields.Many2one(
-        comodel_name="contract.template",
-        string="Contract Template",
-        compute="_compute_contract_template_id",
-    )
-    recurring_rule_type = fields.Selection(related="product_id.recurring_rule_type")
-    recurring_invoicing_type = fields.Selection(
-        related="product_id.recurring_invoicing_type"
-    )
-    date_start = fields.Date()
-    date_end = fields.Date()
-    contract_line_id = fields.Many2one(
-        comodel_name="contract.line",
-        string="Contract Line to replace",
-        required=False,
-    )
-    is_auto_renew = fields.Boolean(
-        string="Auto Renew",
-        compute="_compute_auto_renew",
-        default=False,
-        store=True,
-        readonly=False,
-    )
-    auto_renew_interval = fields.Integer(
-        default=1,
-        string="Renew Every",
-        compute="_compute_auto_renew",
-        store=True,
-        readonly=False,
-        help="Renew every (Days/Week/Month/Year)",
-    )
-    auto_renew_rule_type = fields.Selection(
-        [
-            ("daily", "Day(s)"),
-            ("weekly", "Week(s)"),
-            ("monthly", "Month(s)"),
-            ("yearly", "Year(s)"),
-        ],
-        default="yearly",
-        compute="_compute_auto_renew",
-        store=True,
-        readonly=False,
-        string="Renewal type",
-        help="Specify Interval for automatic renewal.",
-    )
-    contract_start_date_method = fields.Selection(
-        related="product_id.contract_start_date_method"
-    )
-
-    @api.depends("product_id", "company_id")
-    def _compute_contract_template_id(self):
-        for rec in self:
-            rec.contract_template_id = rec.product_id.with_company(
-                rec.company_id
-            ).property_contract_template_id
-
-    @api.depends("product_id")
-    def _compute_auto_renew(self):
-        for rec in self:
-            if rec.product_id.is_contract:
-                rec.product_uom_qty = rec.product_id.default_qty
-                contract_start_date_method = rec.product_id.contract_start_date_method
-                if contract_start_date_method == "manual":
-                    rec.date_start = rec.date_start or fields.Date.today()
-                rec.date_end = rec._get_date_end()
-                rec.is_auto_renew = rec.product_id.is_auto_renew
-                if rec.is_auto_renew:
-                    rec.auto_renew_interval = rec.product_id.auto_renew_interval
-                    rec.auto_renew_rule_type = rec.product_id.auto_renew_rule_type
-
-    def _get_auto_renew_rule_type(self):
-        """monthly last day don't make sense for auto_renew_rule_type"""
-        self.ensure_one()
-        if self.recurring_rule_type == "monthlylastday":
-            return "monthly"
-        return self.recurring_rule_type
-
-    def _get_date_end(self):
-        self.ensure_one()
-        contract_line_model = self.env["contract.line"]
-        date_end = (
-            self.date_start
-            + contract_line_model.get_relative_delta(
-                self._get_auto_renew_rule_type(),
-                int(self.product_uom_qty),
-            )
-            - relativedelta(days=1)
-        )
-        return date_end
-
-    @api.onchange("date_start", "product_uom_qty")
-    def _onchange_date_start(self):
-        for rec in self.filtered("product_id.is_contract"):
-            rec.date_end = rec._get_date_end() if rec.date_start else False
diff --git a/product_contract/wizards/product_contract_configurator_views.xml b/product_contract/wizards/product_contract_configurator_views.xml
index dbcc60050e..72eb0f279b 100644
--- a/product_contract/wizards/product_contract_configurator_views.xml
+++ b/product_contract/wizards/product_contract_configurator_views.xml
@@ -13,7 +13,7 @@
                     </group>
                     <separator colspan="4" string="Recurrence Invoicing" />
                     <group>
-                        <field name="recurring_rule_type" />
+                        <field name="recurrence_number" />
                         <field name="contract_start_date_method" />
                         <field
                             name="date_start"
@@ -23,6 +23,7 @@
                         <field name="is_auto_renew" invisible="not date_end" />
                     </group>
                     <group>
+                        <field name="recurring_rule_type" />
                         <field name="recurring_invoicing_type" />
                         <field
                             name="date_end"