diff --git a/product_contract/__init__.py b/product_contract/__init__.py index c6339a004a..9911bbdebe 100644 --- a/product_contract/__init__.py +++ b/product_contract/__init__.py @@ -2,3 +2,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models +from . import wizards diff --git a/product_contract/__manifest__.py b/product_contract/__manifest__.py index a2ae0c24c3..4b697e41d3 100644 --- a/product_contract/__manifest__.py +++ b/product_contract/__manifest__.py @@ -11,13 +11,16 @@ "website": "https://github.com/OCA/contract", "depends": ["product", "contract", "sale"], "data": [ + "security/ir.model.access.csv", "wizards/res_config_settings.xml", "views/contract.xml", "views/product_template.xml", "views/sale_order.xml", + "wizards/product_contract_configurator_views.xml", ], "installable": True, "application": False, "external_dependencies": {"python": ["dateutil"]}, "maintainers": ["sbejaoui"], + "assets": {"web.assets_backend": ["product_contract/static/src/js/*"]}, } diff --git a/product_contract/security/ir.model.access.csv b/product_contract/security/ir.model.access.csv new file mode 100644 index 0000000000..87a6bfbd6e --- /dev/null +++ b/product_contract/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_contract_configurator,access.product.contract.configurator,model_product_contract_configurator,sales_team.group_sale_salesman,1,1,1,0 diff --git a/product_contract/static/description/index.html b/product_contract/static/description/index.html index 5e89e11afa..49185289e4 100644 --- a/product_contract/static/description/index.html +++ b/product_contract/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -431,7 +432,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/product_contract/static/src/js/contract_configurator_controller.esm.js b/product_contract/static/src/js/contract_configurator_controller.esm.js new file mode 100644 index 0000000000..3b8fe7468b --- /dev/null +++ b/product_contract/static/src/js/contract_configurator_controller.esm.js @@ -0,0 +1,50 @@ +/** @odoo-module **/ + +import {formView} from "@web/views/form/form_view"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +export class ProductContractConfiguratorController extends formView.Controller { + setup() { + super.setup(); + this.action = useService("action"); + } + + async onRecordSaved(record) { + await super.onRecordSaved(...arguments); + const { + product_uom_qty, + contract_id, + recurring_rule_type, + recurring_invoicing_type, + date_start, + date_end, + contract_line_id, + is_auto_renew, + auto_renew_interval, + auto_renew_rule_type, + } = record.data; + return this.action.doAction({ + type: "ir.actions.act_window_close", + infos: { + productContractConfiguration: { + product_uom_qty, + contract_id, + recurring_rule_type, + recurring_invoicing_type, + date_start, + date_end, + contract_line_id, + is_auto_renew, + auto_renew_interval, + auto_renew_rule_type, + }, + }, + }); + } +} + +registry.category("views").add("product_contract_configurator_form", { + ...formView, + Controller: ProductContractConfiguratorController, +}); diff --git a/product_contract/static/src/js/sale_product_field.esm.js b/product_contract/static/src/js/sale_product_field.esm.js new file mode 100644 index 0000000000..483dd57f88 --- /dev/null +++ b/product_contract/static/src/js/sale_product_field.esm.js @@ -0,0 +1,54 @@ +/** @odoo-module **/ + +import {SaleOrderLineProductField} from "@sale/js/sale_product_field"; +import {patch} from "@web/core/utils/patch"; + +patch(SaleOrderLineProductField.prototype, { + async _onProductUpdate() { + super._onProductUpdate(...arguments); + if (this.props.record.data.is_contract) { + this._openContractConfigurator(true); + } + }, + + _editLineConfiguration() { + super._editLineConfiguration(...arguments); + if (this.props.record.data.is_contract) { + this._openContractConfigurator(); + } + }, + + get isConfigurableLine() { + return super.isConfigurableLine || this.props.record.data.is_contract; + }, + + async _openContractConfigurator(isNew = false) { + const actionContext = { + 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_product_uom_qty: this.props.record.data.product_uom_qty, + default_contract_id: this.props.record.data.contract_id[0], + default_recurring_rule_type: this.props.record.data.recurring_rule_type, + default_recurring_invoicing_type: + this.props.record.data.recurring_invoicing_type, + 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, + default_auto_renew_interval: this.props.record.data.auto_renew_interval, + default_auto_renew_rule_type: this.props.record.data.auto_renew_rule_type, + }; + this.action.doAction("product_contract.product_contract_configurator_action", { + additionalContext: actionContext, + onClose: async (closeInfo) => { + if (closeInfo && !closeInfo.special) { + this.props.record.update(closeInfo.productContractConfiguration); + } else if (isNew) { + this.props.record.update({ + [this.props.name]: undefined, + }); + } + }, + }); + }, +}); diff --git a/product_contract/views/sale_order.xml b/product_contract/views/sale_order.xml index c6b5130bfe..8c8f7178dd 100644 --- a/product_contract/views/sale_order.xml +++ b/product_contract/views/sale_order.xml @@ -41,13 +41,13 @@ @@ -59,37 +59,37 @@ - + - + - - + + - - + + - + - + @@ -98,15 +98,41 @@ expr="//field[@name='order_line']/tree//field[@name='price_total']" position="after" > + + + + + + + + + + - - - diff --git a/product_contract/wizards/__init__.py b/product_contract/wizards/__init__.py new file mode 100644 index 0000000000..a04bb80feb --- /dev/null +++ b/product_contract/wizards/__init__.py @@ -0,0 +1 @@ +from . import product_contract_configurator diff --git a/product_contract/wizards/product_contract_configurator.py b/product_contract/wizards/product_contract_configurator.py new file mode 100644 index 0000000000..4916d33d0b --- /dev/null +++ b/product_contract/wizards/product_contract_configurator.py @@ -0,0 +1,124 @@ +# 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 + + +class ProductContractConfigurator(models.TransientModel): + _name = "product.contract.configurator" + _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( + [ + ("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="Invoice Every", + ) + recurring_invoicing_type = fields.Selection( + [("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")], + default="pre-paid", + string="Invoicing type", + help="Specify if process date is 'from' or 'to' invoicing date", + ) + 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.", + ) + + @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 + rec.recurring_rule_type = rec.product_id.recurring_rule_type + rec.recurring_invoicing_type = rec.product_id.recurring_invoicing_type + 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", "recurring_rule_type") + 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 new file mode 100644 index 0000000000..d08f524997 --- /dev/null +++ b/product_contract/wizards/product_contract_configurator_views.xml @@ -0,0 +1,81 @@ + + + + product.contract.configurator + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Configure a contract + product.contract.configurator + form + new + + +