From 64dce27c80603921de45292b31a84e041cfcc431 Mon Sep 17 00:00:00 2001 From: Ricard Calvo Date: Thu, 15 Jan 2026 11:40:34 +0100 Subject: [PATCH] [18.0][FIX] web_widget_x2many_2d_matrix: refresh totals on cell edit Totals were not consistently refreshing immediately on cell edits because the renderer logic relied on expensive getters that caused redundant computations per render cycle and lacked proper reactivity. - Refactor X2Many2DMatrixRenderer to use the `onWillRender` hook, computing all matrix data (rows, columns, and cells) once per render cycle. This ensures totals update immediately when a cell value changes and significantly improves performance. - Expand the JS test suite with a robust case verifying totals after multiple consecutive edits. - Adapt tests to the Odoo 18 Hoot framework using `contains` and `queryAll`. --- .../x2many_2d_matrix_renderer.esm.js | 29 ++-- .../tests/web_widget_x2many_2d_matrix.test.js | 157 ++++++++++++------ .../tests/test_web_widget_x2many_2d_matrix.py | 2 +- 3 files changed, 119 insertions(+), 69 deletions(-) diff --git a/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm.js b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm.js index 20016e5d3e8a..aee17d915cd7 100644 --- a/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm.js +++ b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm.js @@ -1,19 +1,16 @@ -import {Component, onWillUpdateProps} from "@odoo/owl"; import {evaluateBooleanExpr, evaluateExpr} from "@web/core/py_js/py"; +import {Component, onWillRender} from "@odoo/owl"; import {Domain} from "@web/core/domain"; import {Record} from "@web/model/relational_model/record"; import {getFieldContext} from "@web/model/relational_model/utils"; export class X2Many2DMatrixRenderer extends Component { setup() { - this.columns = this._getColumns(); - this.rows = this._getRows(); - this.matrix = this._getMatrix(); - - onWillUpdateProps((newProps) => { - this.columns = this._getColumns(newProps.list.records); - this.rows = this._getRows(newProps.list.records); - this.matrix = this._getMatrix(newProps.list.records); + onWillRender(() => { + const records = this.list.records; + this.columns = this._getColumns(records); + this.rows = this._getRows(records); + this.matrix = this._getMatrix(records, this.rows, this.columns); }); } @@ -53,7 +50,7 @@ export class X2Many2DMatrixRenderer extends Component { return rows; } - _getPointOfRecord(record) { + _getPointOfRecord(record, rows, columns) { let xValue = record.data[this.matrixFields.x]; if (record.fields[this.matrixFields.x].type === "many2one") { xValue = xValue[0]; @@ -63,20 +60,20 @@ export class X2Many2DMatrixRenderer extends Component { yValue = yValue[0]; } - const x = this.columns.findIndex((c) => c.value === xValue); - const y = this.rows.findIndex((r) => r.value === yValue); + const x = columns.findIndex((c) => c.value === xValue); + const y = rows.findIndex((r) => r.value === yValue); return {x, y}; } - _getMatrix(records = this.list.records) { - const matrix = this.rows.map(() => - new Array(this.columns.length).fill(null).map(() => { + _getMatrix(records = this.list.records, rows, columns) { + const matrix = rows.map(() => + new Array(columns.length).fill(null).map(() => { return {value: 0, records: []}; }) ); records.forEach((record) => { const value = record.data[this.matrixFields.value]; - const {x, y} = this._getPointOfRecord(record); + const {x, y} = this._getPointOfRecord(record, rows, columns); matrix[y][x].value += value; matrix[y][x].records.push(record); }); diff --git a/web_widget_x2many_2d_matrix/static/tests/web_widget_x2many_2d_matrix.test.js b/web_widget_x2many_2d_matrix/static/tests/web_widget_x2many_2d_matrix.test.js index 942132b12d0c..c3f8575a7dda 100644 --- a/web_widget_x2many_2d_matrix/static/tests/web_widget_x2many_2d_matrix.test.js +++ b/web_widget_x2many_2d_matrix/static/tests/web_widget_x2many_2d_matrix.test.js @@ -1,5 +1,12 @@ -import {defineModels, fields, models, mountView} from "@web/../tests/web_test_helpers"; +import { + contains, + defineModels, + fields, + models, + mountView, +} from "@web/../tests/web_test_helpers"; import {describe, expect, test} from "@odoo/hoot"; +import {queryAll} from "@odoo/hoot-dom"; describe.current.tags("desktop"); @@ -63,12 +70,13 @@ class Line extends models.Model { } defineModels([Line, Main]); -test("matrix displaying float fields are rendered correctly", async () => { - await mountView({ - type: "form", - resModel: "main", - resId: 1, - arch: ` +describe("X2Many2DMatrixRenderer", () => { + test("matrix displaying float fields are rendered correctly", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: `
@@ -78,17 +86,17 @@ test("matrix displaying float fields are rendered correctly", async () => {
`, + }); + expect(".o_field_widget input").toHaveCount(4); + expect(".col-total").toHaveText("168.00"); }); - expect(".o_field_widget input").toHaveCount(4); - expect(".col-total").toHaveText("168.00"); -}); -test("matrix displaying float fields can be configured", async () => { - await mountView({ - type: "form", - resModel: "main", - resId: 1, - arch: ` + test("matrix displaying float fields can be configured", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: `
@@ -98,16 +106,16 @@ test("matrix displaying float fields can be configured", async () => {
`, + }); + expect(".o_field_widget input").toHaveValue("42.000"); }); - expect(".o_field_widget input").toHaveValue("42.000"); -}); -test("matrix displaying char fields are rendered correctly", async () => { - await mountView({ - type: "form", - resModel: "main", - resId: 1, - arch: ` + test("matrix displaying char fields are rendered correctly", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: `
@@ -117,17 +125,17 @@ test("matrix displaying char fields are rendered correctly", async () => {
`, + }); + expect(".o_field_widget input").toHaveCount(4); + expect(".col-total").toHaveCount(0); }); - expect(".o_field_widget input").toHaveCount(4); - expect(".col-total").toHaveCount(0); -}); -test("matrix displaying many2one fields are rendered correctly", async () => { - await mountView({ - type: "form", - resModel: "main", - resId: 1, - arch: ` + test("matrix displaying many2one fields are rendered correctly", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: `
@@ -137,16 +145,16 @@ test("matrix displaying many2one fields are rendered correctly", async () => {
`, + }); + expect(".o_field_many2one_selection").toHaveCount(4); + expect(".o_form_uri").toHaveCount(0); }); - expect(".o_field_many2one_selection").toHaveCount(4); - expect(".o_form_uri").toHaveCount(0); -}); -test("matrix displaying many2one fields can be configured", async () => { - await mountView({ - type: "form", - resModel: "main", - resId: 1, - arch: ` + test("matrix displaying many2one fields can be configured", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: `
@@ -156,16 +164,16 @@ test("matrix displaying many2one fields can be configured", async () => {
`, + }); + expect(".o_field_many2one_selection").toHaveCount(2); + expect(".o_form_uri").toHaveCount(2); }); - expect(".o_field_many2one_selection").toHaveCount(2); - expect(".o_form_uri").toHaveCount(2); -}); -test("matrix axis can be clickable", async () => { - await mountView({ - type: "form", - resModel: "main", - resId: 1, - arch: ` + test("matrix axis can be clickable", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: `
@@ -174,6 +182,51 @@ test("matrix axis can be clickable", async () => {
`, + }); + expect(".o_form_uri").toHaveCount(2); + }); + + test("matrix totals are updated correctly after several edits", async () => { + await mountView({ + type: "form", + resModel: "main", + resId: 1, + arch: ` +
+ + + + + + + +
`, + }); + + expect(".col-total").toHaveText("168.00"); + + const inputs = queryAll(".o_field_widget input"); + // Initial state: [42, 42, 42, 42] -> Total 168 + + await contains(inputs[0]).edit("100"); + // State: [100, 42, 42, 42] -> Total 226 + expect(".col-total").toHaveText("226.00"); + + await contains(inputs[1]).edit("200"); + // State: [100, 200, 42, 42] -> Total 384 + expect(".col-total").toHaveText("384.00"); + + await contains(inputs[2]).edit("50"); + // State: [100, 200, 50, 42] -> Total 392 + expect(".col-total").toHaveText("392.00"); + + await contains(inputs[3]).edit("0"); + // State: [100, 200, 50, 0] -> Total 350 + expect(".col-total").toHaveText("350.00"); + + // Edit the first one again + await contains(inputs[0]).edit("10"); + // State: [10, 200, 50, 0] -> Total 260 + expect(".col-total").toHaveText("260.00"); }); - expect(".o_form_uri").toHaveCount(2); }); diff --git a/web_widget_x2many_2d_matrix/tests/test_web_widget_x2many_2d_matrix.py b/web_widget_x2many_2d_matrix/tests/test_web_widget_x2many_2d_matrix.py index 590738a1e63b..d5ae574625f7 100644 --- a/web_widget_x2many_2d_matrix/tests/test_web_widget_x2many_2d_matrix.py +++ b/web_widget_x2many_2d_matrix/tests/test_web_widget_x2many_2d_matrix.py @@ -5,7 +5,7 @@ class TestWebWidgetX2Many2DMatrix(HttpCase): def test_js(self): self.browser_js( - "/web/tests?headless&loglevel=2&preset=desktop&timeout=15000&id=b42fa5f8", + "/web/tests?headless&loglevel=2&preset=desktop&timeout=15000&filter=X2Many2DMatrixRenderer", "", "", login="admin",