From 0ae4fab56de50ee05f787a34f35aca704cc7bac6 Mon Sep 17 00:00:00 2001 From: sergio-teruel Date: Fri, 24 Jan 2025 22:26:10 +0100 Subject: [PATCH 1/4] [FIX] product_contract: Compute date_start and date_end correctly. Make fields computed --- product_contract/README.rst | 5 +- product_contract/models/sale_order_line.py | 108 +++++++++++------- product_contract/readme/CONTRIBUTORS.md | 1 + .../static/description/index.html | 1 + .../contract_configurator_controller.esm.js | 2 + .../static/src/js/sale_product_field.esm.js | 5 +- product_contract/tests/test_sale_order.py | 8 +- product_contract/views/sale_order.xml | 41 ++++++- .../wizards/product_contract_configurator.py | 39 +++---- .../product_contract_configurator_views.xml | 39 +++++-- 10 files changed, 162 insertions(+), 87 deletions(-) diff --git a/product_contract/README.rst b/product_contract/README.rst index 01e4adee33..f9608b94cb 100644 --- a/product_contract/README.rst +++ b/product_contract/README.rst @@ -2,7 +2,7 @@ Recurring - Product Contract ============================ -.. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! @@ -97,6 +97,7 @@ Contributors - Ernesto Tejeda - Pedro M. Baeza - Carlos Roca + - Sergio Teruel - David Jaen @@ -119,7 +120,7 @@ promote its widespread use. Current `maintainer `__: -|maintainer-sbejaoui| +|maintainer-sbejaoui| This module is part of the `OCA/contract `_ project on GitHub. diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py index 1bf5cf84c1..ad3bc1acd1 100644 --- a/product_contract/models/sale_order_line.py +++ b/product_contract/models/sale_order_line.py @@ -30,23 +30,25 @@ class SaleOrderLine(models.Model): string="Contract Template", compute="_compute_contract_template_id", ) + recurring_interval = fields.Integer( + default=1, + string="Invoice Every", + help="Invoice every (Days/Week/Month/Year)", + ) 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() - + date_start = fields.Date(compute="_compute_date_start", readonly=False, store=True) + date_end = fields.Date(compute="_compute_date_end", readonly=False, store=True) 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, ) @@ -97,47 +99,75 @@ def _compute_contract_template_id(self): rec.order_id.company_id ).property_contract_template_id + @api.depends("product_id") + def _compute_date_start(self): + for sol in self: + if sol.contract_start_date_method == "start_this": + sol.date_start = sol.order_id.date_order.replace(day=1) + elif sol.contract_start_date_method == "end_this": + sol.date_start = ( + sol.order_id.date_order + + self.get_relative_delta( + sol.recurring_rule_type, sol.product_id.default_qty + ) + ).replace(day=1) - relativedelta(days=1) + elif sol.contract_start_date_method == "start_next": + # Dia 1 del siguiente recurring_rule_type + sol.date_start = ( + sol.order_id.date_order + + self.get_relative_delta( + sol.recurring_rule_type, sol.product_id.default_qty + ) + ).replace(day=1) + elif sol.contract_start_date_method == "end_next": + # Last day of next recurring period + sol.date_start = ( + sol.order_id.date_order + + self.get_relative_delta( + sol.recurring_rule_type, sol.product_id.default_qty + 1 + ) + ).replace(day=1) - relativedelta(days=1) + else: + # Manual method + sol.date_start = False + + @api.depends( + "is_auto_renew", + "date_start", + "auto_renew_interval", + "auto_renew_rule_type", + ) + def _compute_date_end(self): + for sol in self: + if sol.is_auto_renew and sol.date_start: + sol.date_end = self.env["contract.line"]._get_first_date_end( + sol.date_start, + sol._get_auto_renew_rule_type(), + sol.auto_renew_interval, + ) + else: + sol.date_end = False + + @api.model + def get_relative_delta(self, recurring_rule_type, interval): + return self.env["contract.recurrency.mixin"].get_relative_delta( + recurring_rule_type, interval + ) + 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": + if self.auto_renew_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 + return self.auto_renew_rule_type @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 + rec.product_uom_qty = rec.product_id.default_qty + rec.is_auto_renew = rec.product_id.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_contract_line_qty(self): """Returns the amount that will be placed in new contract lines.""" @@ -178,7 +208,7 @@ def _prepare_contract_line_values( "date_end": self.date_end, "date_start": self.date_start or fields.Date.today(), "recurring_next_date": recurring_next_date, - "recurring_interval": 1, + "recurring_interval": self.recurring_interval or 1, "recurring_invoicing_type": self.recurring_invoicing_type, "recurring_rule_type": self.recurring_rule_type, "is_auto_renew": self.is_auto_renew, diff --git a/product_contract/readme/CONTRIBUTORS.md b/product_contract/readme/CONTRIBUTORS.md index c80ad3d118..f310ec169a 100644 --- a/product_contract/readme/CONTRIBUTORS.md +++ b/product_contract/readme/CONTRIBUTORS.md @@ -4,4 +4,5 @@ - Ernesto Tejeda - Pedro M. Baeza - Carlos Roca + - Sergio Teruel - David Jaen \<\> diff --git a/product_contract/static/description/index.html b/product_contract/static/description/index.html index 45bb45befe..8749e091ff 100644 --- a/product_contract/static/description/index.html +++ b/product_contract/static/description/index.html @@ -444,6 +444,7 @@

Contributors

  • Ernesto Tejeda
  • Pedro M. Baeza
  • Carlos Roca
  • +
  • Sergio Teruel
  • David Jaen <david.jaen.revert@gmail.com>
  • 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..3be3a58a50 100644 --- a/product_contract/static/src/js/contract_configurator_controller.esm.js +++ b/product_contract/static/src/js/contract_configurator_controller.esm.js @@ -21,6 +21,7 @@ export class ProductContractConfiguratorController extends formView.Controller { is_auto_renew, auto_renew_interval, auto_renew_rule_type, + recurring_interval, } = record.data; return this.action.doAction({ type: "ir.actions.act_window_close", @@ -34,6 +35,7 @@ export class ProductContractConfiguratorController extends formView.Controller { is_auto_renew, auto_renew_interval, auto_renew_rule_type, + recurring_interval, }, }, }); 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 716e18f4b1..4ca2f763c8 100644 --- a/product_contract/static/src/js/sale_product_field.esm.js +++ b/product_contract/static/src/js/sale_product_field.esm.js @@ -44,6 +44,7 @@ patch(SaleOrderLineProductField.prototype, { default_company_id: this.props.record.model.root.data.company_id[0], default_product_uom_qty: this.props.record.data.product_uom_qty, default_contract_id: this.props.record.data.contract_id[0], + default_recurring_interval: this.props.record.data.recurring_interval, default_date_start: this.props.record.data.date_start, default_date_end: this.props.record.data.date_end, default_is_auto_renew: this.props.record.data.is_auto_renew, @@ -54,7 +55,9 @@ patch(SaleOrderLineProductField.prototype, { additionalContext: actionContext, onClose: async (closeInfo) => { if (closeInfo && !closeInfo.special) { - this.props.record.update(closeInfo.productContractConfiguration); + this.props.record._update(closeInfo.productContractConfiguration, { + withoutOnchange: true, + }); } else if (isNew) { this.props.record.update({ [this.props.name]: undefined, diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py index 434f54a5c2..65df98ecb9 100644 --- a/product_contract/tests/test_sale_order.py +++ b/product_contract/tests/test_sale_order.py @@ -109,7 +109,7 @@ def test_compute_is_contract(self): def test_action_confirm(self): """It should create a contract for each contract template used in order_line""" - self.order_line1._compute_auto_renew() + self.order_line1.is_auto_renew = True self.sale.action_confirm() contracts = self.sale.order_line.mapped("contract_id") self.assertEqual(len(contracts), 2) @@ -155,7 +155,7 @@ def test_action_confirm_without_contract_creation(self): """It should create a contract for each contract template used in order_line""" self.sale.company_id.create_contract_at_sale_order_confirmation = False - self.order_line1._compute_auto_renew() + self.order_line1.is_auto_renew = True self.sale.action_confirm() self.assertEqual(len(self.sale.order_line.mapped("contract_id")), 0) self.assertTrue(self.sale.need_contract_creation) @@ -174,14 +174,14 @@ def test_action_confirm_without_contract_creation(self): def test_sale_contract_count(self): """It should count contracts as many different contract template used in order_line""" - self.order_line1._compute_auto_renew() + self.order_line1.is_auto_renew = True self.sale.action_confirm() self.assertEqual(self.sale.contract_count, 2) def test_onchange_product(self): """It should get recurrence invoicing info to the sale line from its product""" - self.order_line1._compute_auto_renew() + self.order_line1.is_auto_renew = True self.assertEqual( self.order_line1.recurring_rule_type, self.product1.recurring_rule_type, diff --git a/product_contract/views/sale_order.xml b/product_contract/views/sale_order.xml index b3187fd6f8..4abf7ca252 100644 --- a/product_contract/views/sale_order.xml +++ b/product_contract/views/sale_order.xml @@ -62,19 +62,30 @@ invisible="not is_contract" /> - + - + - + - + @@ -113,6 +126,11 @@ domain="[('contract_id','=',contract_id)]" optional="hide" /> + @@ -120,15 +138,25 @@ name="date_start" optional="hide" required="is_contract and contract_start_date_method == 'manual'" + readonly="product_uom_readonly" + /> + + - - diff --git a/product_contract/wizards/product_contract_configurator.py b/product_contract/wizards/product_contract_configurator.py index 4c6a63252f..fd1253454e 100644 --- a/product_contract/wizards/product_contract_configurator.py +++ b/product_contract/wizards/product_contract_configurator.py @@ -1,7 +1,6 @@ # 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 @@ -20,12 +19,17 @@ class ProductContractConfigurator(models.TransientModel): string="Contract Template", compute="_compute_contract_template_id", ) + recurring_interval = fields.Integer( + default=1, + string="Invoice Every", + help="Invoice every (Days/Week/Month/Year)", + ) 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() + date_end = fields.Date(compute="_compute_date_end", readonly=False, store=True) contract_line_id = fields.Many2one( comodel_name="contract.line", string="Contract Line to replace", @@ -79,33 +83,22 @@ def _compute_auto_renew(self): 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.depends("date_start", "recurring_interval") + def _compute_date_end(self): + self.update({"date_end": False}) + for rec in self.filtered(lambda ln: ln.is_auto_renew and ln.date_start): + rec.date_end = self.env["contract.line"]._get_first_date_end( + rec.date_start, rec._get_auto_renew_rule_type(), rec.auto_renew_interval + ) + 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": + if self.auto_renew_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 + return self.auto_renew_rule_type diff --git a/product_contract/wizards/product_contract_configurator_views.xml b/product_contract/wizards/product_contract_configurator_views.xml index dbcc60050e..e87418bce3 100644 --- a/product_contract/wizards/product_contract_configurator_views.xml +++ b/product_contract/wizards/product_contract_configurator_views.xml @@ -13,21 +13,21 @@ - - - - + - + + + + + + + + + + + + + Date: Mon, 27 Jan 2025 12:39:35 +0100 Subject: [PATCH 2/4] [IMP] product_contract: Make computed fields as precompute to be able to use them on _compute_name method --- product_contract/models/sale_order_line.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py index ad3bc1acd1..402e3bb503 100644 --- a/product_contract/models/sale_order_line.py +++ b/product_contract/models/sale_order_line.py @@ -39,8 +39,12 @@ class SaleOrderLine(models.Model): recurring_invoicing_type = fields.Selection( related="product_id.recurring_invoicing_type" ) - date_start = fields.Date(compute="_compute_date_start", readonly=False, store=True) - date_end = fields.Date(compute="_compute_date_end", readonly=False, store=True) + date_start = fields.Date( + compute="_compute_date_start", readonly=False, store=True, precompute=True + ) + date_end = fields.Date( + compute="_compute_date_end", readonly=False, store=True, precompute=True + ) contract_line_id = fields.Many2one( comodel_name="contract.line", string="Contract Line to replace", @@ -51,6 +55,7 @@ class SaleOrderLine(models.Model): compute="_compute_auto_renew", store=True, readonly=False, + precompute=True, ) auto_renew_interval = fields.Integer( default=1, @@ -59,6 +64,7 @@ class SaleOrderLine(models.Model): store=True, readonly=False, help="Renew every (Days/Week/Month/Year)", + precompute=True, ) auto_renew_rule_type = fields.Selection( [ @@ -73,6 +79,7 @@ class SaleOrderLine(models.Model): readonly=False, string="Renewal type", help="Specify Interval for automatic renewal.", + precompute=True, ) contract_start_date_method = fields.Selection( related="product_id.contract_start_date_method" From 76d8c2f4720b98d28c3b6e16bce9940c393842eb Mon Sep 17 00:00:00 2001 From: Carlos Roca Date: Tue, 28 Jan 2025 08:46:57 +0100 Subject: [PATCH 3/4] [IMP] product_contract: Make configurator contract context heredable --- product_contract/README.rst | 24 +++++++++---------- .../static/src/js/sale_product_field.esm.js | 8 +++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/product_contract/README.rst b/product_contract/README.rst index f9608b94cb..99c575b7c8 100644 --- a/product_contract/README.rst +++ b/product_contract/README.rst @@ -2,7 +2,7 @@ Recurring - Product Contract ============================ -.. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! @@ -65,8 +65,8 @@ To use this module, you need to: Known issues / Roadmap ====================== -- There's no support right now for computing the start date for the - following recurrent types: daily, weekly and monthlylastday. +- There's no support right now for computing the start date for the + following recurrent types: daily, weekly and monthlylastday. Bug Tracker =========== @@ -90,16 +90,16 @@ Authors Contributors ------------ -- Ted Salmon -- Souheil Bejaoui -- `Tecnativa `__: +- Ted Salmon +- Souheil Bejaoui +- `Tecnativa `__: - - Ernesto Tejeda - - Pedro M. Baeza - - Carlos Roca - - Sergio Teruel + - Ernesto Tejeda + - Pedro M. Baeza + - Carlos Roca + - Sergio Teruel -- David Jaen +- David Jaen Maintainers ----------- @@ -120,7 +120,7 @@ promote its widespread use. Current `maintainer `__: -|maintainer-sbejaoui| +|maintainer-sbejaoui| This module is part of the `OCA/contract `_ project on GitHub. 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 4ca2f763c8..4e60db39cb 100644 --- a/product_contract/static/src/js/sale_product_field.esm.js +++ b/product_contract/static/src/js/sale_product_field.esm.js @@ -37,8 +37,8 @@ patch(SaleOrderLineProductField.prototype, { return super.isConfigurableLine || this.props.record.data.is_contract; }, - async _openContractConfigurator(isNew = false) { - const actionContext = { + get contractContext() { + return { 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], @@ -51,6 +51,10 @@ patch(SaleOrderLineProductField.prototype, { default_auto_renew_interval: this.props.record.data.auto_renew_interval, default_auto_renew_rule_type: this.props.record.data.auto_renew_rule_type, }; + }, + + async _openContractConfigurator(isNew = false) { + const actionContext = this.contractContext; this.action.doAction("product_contract.product_contract_configurator_action", { additionalContext: actionContext, onClose: async (closeInfo) => { From 6629826f9223487ac35ee13d04f15239be67e14c Mon Sep 17 00:00:00 2001 From: sergio-teruel Date: Tue, 28 Jan 2025 10:43:22 +0100 Subject: [PATCH 4/4] [FIX] product_contract: Remove unnecessary methods --- product_contract/models/sale_order_line.py | 9 +-------- .../wizards/product_contract_configurator.py | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py index 402e3bb503..c841099c49 100644 --- a/product_contract/models/sale_order_line.py +++ b/product_contract/models/sale_order_line.py @@ -149,7 +149,7 @@ def _compute_date_end(self): if sol.is_auto_renew and sol.date_start: sol.date_end = self.env["contract.line"]._get_first_date_end( sol.date_start, - sol._get_auto_renew_rule_type(), + sol.auto_renew_rule_type, sol.auto_renew_interval, ) else: @@ -161,13 +161,6 @@ def get_relative_delta(self, recurring_rule_type, interval): recurring_rule_type, interval ) - def _get_auto_renew_rule_type(self): - """monthly last day don't make sense for auto_renew_rule_type""" - self.ensure_one() - if self.auto_renew_rule_type == "monthlylastday": - return "monthly" - return self.auto_renew_rule_type - @api.depends("product_id") def _compute_auto_renew(self): for rec in self.filtered("product_id.is_contract"): diff --git a/product_contract/wizards/product_contract_configurator.py b/product_contract/wizards/product_contract_configurator.py index fd1253454e..ac13112b4b 100644 --- a/product_contract/wizards/product_contract_configurator.py +++ b/product_contract/wizards/product_contract_configurator.py @@ -93,12 +93,5 @@ def _compute_date_end(self): self.update({"date_end": False}) for rec in self.filtered(lambda ln: ln.is_auto_renew and ln.date_start): rec.date_end = self.env["contract.line"]._get_first_date_end( - rec.date_start, rec._get_auto_renew_rule_type(), rec.auto_renew_interval + rec.date_start, rec.auto_renew_rule_type, rec.auto_renew_interval ) - - def _get_auto_renew_rule_type(self): - """monthly last day don't make sense for auto_renew_rule_type""" - self.ensure_one() - if self.auto_renew_rule_type == "monthlylastday": - return "monthly" - return self.auto_renew_rule_type