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 @@ - + +