[FIX] sale: sol_product_many2one should not crash
The `sol_product_many2one` field is a sub many2one field that should open the proper configuration system, such as the product configurator. Before this commit, when inputting some data, then clicking on another notebook tab, we would often see a crash, because the field has been destroyed (since it is no longer in the current tab) and it tries to open the configurator, which requires rpcs. It is usually a problem when a destroyed component tries to perform some work. Note that it happens at creation and update. Also, the field overrides some many2one methods that display confirmation dialog. To fix this, this commit simplifies the field by only calling the configuration methods when its value has changed. This is a different strategy that does not interfere with the many2one basic features. Also, it does not crash since it will only call the configuration methods on patched, so it is guaranteed to be alive. It is certainly not a complete solution, since we should probably move the code somewhere else not in a field widget, but this would require a redesign of the code, so it is not appropriate in a bug fix anyway. Note that the tour had to be adapted, since now the methods _onProductUpdate/_onProductTemplateUpdate are now called in an effect (so after a render), which is slightly later than before. But the tour would click on confirm even before the product update code was executed, which means that the product was not properly configurated. This is clearly a concurrency problem: the product configuration code should be properly coordinated with the basic model, but doing so is a significative change. Also, in practice, it is not worse than before this commit. opw 3178297 closes odoo/odoo#127048 X-original-commit: de7a6f5f3d83356eb02f8fc25b4a0b8aabb946b3 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
@@ -83,7 +83,8 @@ This module contains all the common features of Sales Management and eCommerce.
|
||||
'sale/static/tests/tours/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'sale/static/tests/sales_team_dashboard_tests.js',
|
||||
'sale/static/tests/**/*',
|
||||
('remove', 'sale/static/tests/tours/**/*')
|
||||
],
|
||||
'web.report_assets_common': [
|
||||
'sale/static/src/scss/sale_report.scss',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEffect } from '@odoo/owl';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneField, many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
@@ -11,47 +12,27 @@ export class SaleOrderLineProductField extends Many2OneField {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
const { update } = this;
|
||||
this.update = async (value) => {
|
||||
await update(value);
|
||||
let newValue = false;
|
||||
// NB: quick creation doesn't go through here, but through quickCreate
|
||||
// below
|
||||
if (value) {
|
||||
if (
|
||||
Array.isArray(value[0]) &&
|
||||
this.props.record.data[this.props.name] != value[0]
|
||||
) {
|
||||
// product (existing)
|
||||
newValue = true;
|
||||
} else {
|
||||
// new product (Create & edit)
|
||||
// value[0] is a dict of creation values
|
||||
newValue = true;
|
||||
}
|
||||
}
|
||||
if (newValue) {
|
||||
if (this.relation === "product.template") {
|
||||
this._onProductTemplateUpdate();
|
||||
} else {
|
||||
this._onProductUpdate();
|
||||
}
|
||||
}
|
||||
let isMounted = false;
|
||||
let isInternalUpdate = false;
|
||||
const { updateRecord } = this;
|
||||
this.updateRecord = (value) => {
|
||||
isInternalUpdate = true;
|
||||
return updateRecord.call(this, value);
|
||||
};
|
||||
|
||||
if (this.props.canQuickCreate) {
|
||||
// HACK to make quick creation also open
|
||||
// configurators if needed
|
||||
this.quickCreate = async (name, params = {}) => {
|
||||
await this.props.record.update({ [this.props.name]: [false, name] });
|
||||
|
||||
useEffect(value => {
|
||||
if (!isMounted) {
|
||||
isMounted = true;
|
||||
} else if (value && isInternalUpdate) {
|
||||
// we don't want to trigger product update when update comes from an external sources,
|
||||
// such as an onchange, or the product configuration dialog itself
|
||||
if (this.relation === "product.template") {
|
||||
this._onProductTemplateUpdate();
|
||||
} else {
|
||||
this._onProductUpdate();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
isInternalUpdate = false;
|
||||
}, () => [Array.isArray(this.value) && this.value[0]]);
|
||||
}
|
||||
|
||||
get isProductClickable() {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
getFixture, patchWithCleanup, addRow, editInput, triggerEvent, click } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
'sale.order': {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
order_line: {
|
||||
string: "order lines",
|
||||
type: "one2many",
|
||||
relation: "sale.order.line",
|
||||
relation_field: "order_id",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
order_line: [],
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
'sale.order.line': {
|
||||
fields: {
|
||||
product_template_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product.template",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
],
|
||||
},
|
||||
'product.template': {
|
||||
fields: {
|
||||
display_name: { string: "Partner Type", type: "char" },
|
||||
name: { string: "Partner Type", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 12, display_name: "desk" },
|
||||
],
|
||||
methods: {
|
||||
get_single_product_variant() {
|
||||
return Promise.resolve({product_id: 12});
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
partner_ids: {
|
||||
string: "one2many partners field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "user_id",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Sale product field");
|
||||
|
||||
QUnit.test("blurring input with incomplete text will propose to create product", async function (assert) {
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="order_line">
|
||||
<tree editable="bottom" >
|
||||
<field name="product_template_id" widget="sol_product_many2one" />
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
}
|
||||
});
|
||||
|
||||
// add a line and enter new product name
|
||||
await addRow(target, ".o_field_x2many_list");
|
||||
await editInput(target, "[name='product_template_id'] input", "new product");
|
||||
|
||||
// blur input => should ask for confirmation if we want to create product
|
||||
await triggerEvent(target, "[name='product_template_id'] input", "blur");
|
||||
assert.containsOnce(target, ".modal:contains(Create new product as a new Product)")
|
||||
assert.verifySteps([
|
||||
"get_views",
|
||||
"onchange",
|
||||
"onchange",
|
||||
"name_search",
|
||||
]);
|
||||
|
||||
await click(target, ".modal button.btn-primary");
|
||||
assert.verifySteps([
|
||||
"name_create",
|
||||
"get_single_product_variant",
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -36,6 +36,9 @@ registry.category("web_tour.tours").add('sale_timesheet_tour', {
|
||||
}, {
|
||||
trigger: '.o_field_widget[name=price_subtotal]:contains(2,500.00)',
|
||||
run() {},
|
||||
}, {
|
||||
trigger: 'div[name="name"] textarea:propValueContains(Service Product)',
|
||||
run: () => {}
|
||||
}, {
|
||||
trigger: 'button[name="action_confirm"]',
|
||||
content: 'Click on Confirm button to create a sale order with this quotation.',
|
||||
|
||||
Reference in New Issue
Block a user