[IMP] product_contract: Add contract configurator instead of making tree not editable

Before this changes, when trying to edit a line of sale order, it was
opening the form of the line. But following the way to work of odoo
with sale event, we have make a new contract configurator that will
be opened when selecting a product of type contract.
This commit is contained in:
Carlos Roca
2024-08-26 11:29:09 +02:00
parent 7f7d947172
commit 88d2cb9a2f
10 changed files with 367 additions and 22 deletions

View File

@@ -2,3 +2,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models
from . import wizards

View File

@@ -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/*"]},
}

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_product_contract_configurator access.product.contract.configurator model_product_contract_configurator sales_team.group_sale_salesman 1 1 1 0

View File

@@ -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 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
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.option {
span.pre {
white-space: pre }
span.problematic {
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
@@ -431,7 +432,9 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>

View File

@@ -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,
});

View File

@@ -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,
});
}
},
});
},
});

View File

@@ -41,13 +41,13 @@
<field
name="contract_id"
options='{"no_create": True}'
invisible="is_contract == False"
invisible="not is_contract"
domain="['|',('contract_template_id','=',contract_template_id), ('contract_template_id','=',False), ('partner_id','=',parent.partner_id), ('is_terminated','=',False),
]"
/>
<field
name="contract_line_id"
invible="is_contract == False"
invisible="not is_contract"
domain="[('contract_id','=',contract_id)]"
/>
</xpath>
@@ -59,37 +59,37 @@
<separator
colspan="4"
string="Recurrence Invoicing"
invisible="is_contract == False"
invisible="not is_contract"
/>
<group invisible="is_contract == False">
<group invisible="not is_contract">
<field name="recurring_rule_type" />
</group>
<group invisible="is_contract == False">
<group invisible="not is_contract">
<field name="recurring_invoicing_type" />
</group>
<group invisible="is_contract == False">
<field name="date_start" required="is_contract == True" />
<group invisible="not is_contract">
<field name="date_start" required="is_contract" />
</group>
<group invisible="is_contract == False">
<field name="date_end" required="is_contract == True" />
<group invisible="not is_contract">
<field name="date_end" required="is_contract" />
</group>
<group invisible="is_contract == False">
<group invisible="not is_contract">
<field name="is_auto_renew" />
</group>
<group invisible="is_auto_renew == False">
<group invisible="not is_auto_renew">
<label for="auto_renew_interval" />
<div>
<field
name="auto_renew_interval"
class="oe_inline"
nolabel="1"
required="is_auto_renew == True"
required="is_auto_renew"
/>
<field
name="auto_renew_rule_type"
class="oe_inline"
nolabel="1"
required="is_auto_renew == True"
required="is_auto_renew"
/>
</div>
</group>
@@ -98,15 +98,41 @@
expr="//field[@name='order_line']/tree//field[@name='price_total']"
position="after"
>
<field name="contract_template_id" column_invisible="1" />
<field name="is_contract" column_invisible="1" />
<field
name="date_start"
column_invisible="parent.is_contract == False"
name="contract_id"
options='{"no_create": True}'
domain="['|',('contract_template_id','=',contract_template_id), ('contract_template_id','=',False), ('partner_id','=',parent.partner_id), ('is_terminated','=',False),
]"
optional="hide"
/>
<field
name="contract_line_id"
domain="[('contract_id','=',contract_id)]"
optional="hide"
/>
<field name="recurring_rule_type" optional="hide" />
<field name="recurring_invoicing_type" optional="hide" />
<field name="date_start" optional="hide" required="is_contract" />
<field name="date_end" required="is_contract" optional="hide" />
<field name="is_auto_renew" optional="hide" />
<field
name="auto_renew_interval"
class="oe_inline"
nolabel="1"
required="is_auto_renew"
optional="hide"
/>
<field
name="auto_renew_rule_type"
class="oe_inline"
nolabel="1"
required="is_auto_renew"
optional="hide"
/>
<field name="date_end" column_invisible="parent.is_contract == False" />
</xpath>
<xpath expr="//field[@name='order_line']/tree" position="attributes">
<attribute name="editable" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
from . import product_contract_configurator

View File

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

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="product_contract_configurator_form" model="ir.ui.view">
<field name="model">product.contract.configurator</field>
<field name="arch" type="xml">
<form js_class="product_contract_configurator_form">
<group>
<field name="product_id" invisible="1" />
<field name="partner_id" invisible="1" />
<field name="contract_template_id" invisible="1" />
<group colspan="2">
<field name="product_uom_qty" />
</group>
<separator colspan="4" string="Recurrence Invoicing" />
<group>
<field name="recurring_rule_type" />
<field name="date_start" required="1" />
<field name="is_auto_renew" />
</group>
<group>
<field name="recurring_invoicing_type" />
<field name="date_end" required="1" />
<label
for="auto_renew_interval"
invisible="not is_auto_renew"
/>
<div invisible="not is_auto_renew">
<field
name="auto_renew_interval"
class="oe_inline"
nolabel="1"
required="is_auto_renew"
/>
<field
name="auto_renew_rule_type"
class="oe_inline"
nolabel="1"
required="is_auto_renew"
/>
</div>
</group>
<separator colspan="4" string="Contract" />
<group colspan="2">
<field
name="contract_id"
options='{"no_create": True}'
domain="['|',('contract_template_id','=',contract_template_id), ('contract_template_id','=',False), ('partner_id','=',partner_id), ('is_terminated','=',False),
]"
/>
<field
name="contract_line_id"
domain="[('contract_id','=',contract_id)]"
/>
</group>
</group>
<footer>
<button
string="Ok"
class="btn-primary"
special="save"
data-hotkey="q"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
data-hotkey="x"
/>
</footer>
</form>
</field>
</record>
<record id="product_contract_configurator_action" model="ir.actions.act_window">
<field name="name">Configure a contract</field>
<field name="res_model">product.contract.configurator</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="product_contract_configurator_form" />
</record>
</odoo>