From 4a5dc95fb3380e96b61cd9ed906a1973deb0c2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 1 Jun 2023 11:33:45 +0000 Subject: [PATCH] [FIX] sale: sol_product_many2one should not crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Signed-off-by: Géry Debongnie --- addons/sale/__manifest__.py | 3 +- .../sale/static/src/js/sale_product_field.js | 51 +++---- .../static/tests/sale_product_field_tests.js | 128 ++++++++++++++++++ .../static/tests/tours/sale_timesheet_tour.js | 3 + 4 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 addons/sale/static/tests/sale_product_field_tests.js diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index cc198bd4956..ef8885f8ae4 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -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', diff --git a/addons/sale/static/src/js/sale_product_field.js b/addons/sale/static/src/js/sale_product_field.js index dbe910960d6..255ae56b339 100644 --- a/addons/sale/static/src/js/sale_product_field.js +++ b/addons/sale/static/src/js/sale_product_field.js @@ -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() { diff --git a/addons/sale/static/tests/sale_product_field_tests.js b/addons/sale/static/tests/sale_product_field_tests.js new file mode 100644 index 00000000000..deae3805b72 --- /dev/null +++ b/addons/sale/static/tests/sale_product_field_tests.js @@ -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: ` +
+ + + + + + + +
`, + 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", + ]); + + }); + +}); diff --git a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js index f402185ad4c..25cc02e0d8a 100644 --- a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js +++ b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js @@ -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.',