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"
/>
+
@@ -112,6 +113,7 @@
domain="[('contract_id','=',contract_id)]"
optional="hide"
/>
+
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 @@
-
+
+