diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 44cb55c7887..d329b04a429 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -6,7 +6,7 @@
mrp.production.tree
mrp.production
-
+
@@ -22,8 +22,8 @@
-
-
+ !lazyColumns.includes(column));
+ },
+
+ _makeDataPoint: function () {
+ // Should put the lazy column to work with decoration
+ const dataPoint = this._super.apply(this, arguments);
+ if (dataPoint.type === "record" && dataPoint.fieldsInfo) {
+ const fieldsInfo = dataPoint.fieldsInfo[dataPoint.viewType];
+ const lazyColumn = _getLazyColumns(fieldsInfo);
+ _bindLazyColumnToState(dataPoint, lazyColumn);
+ }
+ return dataPoint;
+ },
+});
+
+const LazyColumnListRenderer = ListRenderer.extend({
+ init: function () {
+ this.currentNbLazyRpc = 0;
+ return this._super.apply(this, arguments);
+ },
+
+ _lazyLoad: async function (state, lazyColumns) {
+ const res = await this._rpc(
+ {
+ model: state.model,
+ method: "read",
+ args: [state.res_ids, lazyColumns],
+ },
+ { shadow: true }
+ );
+ const resById = {};
+ for (const element of res.values()) {
+ resById[element.id] = {};
+ for (const lazyCol of lazyColumns) {
+ resById[element.id][lazyCol] = element[lazyCol];
+ }
+ }
+ _bindLazyColumnToState(this.state, lazyColumns, resById);
+ this.currentNbLazyRpc -= 1;
+ // Render only thr
+ for (const lazyCol of lazyColumns) {
+ const node = this.columns.find((colAttribute) => colAttribute.attrs.name === lazyCol);
+ if (!node) {
+ continue;
+ }
+ const indexNode = this.columns.findIndex(
+ (colAttribute) => colAttribute.attrs.name === lazyCol
+ );
+ const $columnHeader = this.$el.find(`th[data-name='${lazyCol}']`);
+ $columnHeader.replaceWith(this._renderHeaderCell(node));
+ const $cellsLazy = this.$el.find(`.o_data_cell[name='${lazyCol}']`);
+ for (let i = 0; i <= $cellsLazy.length; i++) {
+ const $cell = $($cellsLazy[i]);
+ const idRecord = $cell.parent().data("id");
+ const record = _findRecordsInState(this.state).find((rec) => rec.id === idRecord);
+ if (record) {
+ $cell.replaceWith(
+ this._renderBodyCell(record, node, indexNode, { mode: "readonly" })
+ );
+ }
+ }
+ }
+ },
+
+ async _render() {
+ const lazyColumns = _getLazyColumns(this.state.fieldsInfo[this.state.type]);
+ const lazyVisible = lazyColumns.some((col) => {
+ return !!this.columns.find((colAttribute) => colAttribute.attrs.name === col);
+ });
+ const lazyLoad = lazyVisible && this.state.res_ids.length > 0;
+ if (lazyLoad) {
+ this.currentNbLazyRpc += 1;
+ }
+ const res = await this._super(...arguments);
+ if (lazyLoad) {
+ this._lazyLoad(this.state, lazyColumns);
+ }
+ return res;
+ },
+
+ _isLazyNode: function (node) {
+ try {
+ if (JSON.parse(node.attrs.options).lazy) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ return false;
+ },
+
+ _renderBodyCell: function (record, node) {
+ const $res = this._super.apply(this, arguments);
+ const lazyColumns = _getLazyColumns(this.state.fieldsInfo[this.state.type]);
+ if (this._isLazyNode(node)) {
+ // Decorations doesn't work for lazy column because the evalContext isn't updated and it is a readonly attribute
+ // Then a lazy fields can be only used in a other lazy fields decoration.
+ const extraData = {};
+ for (const col of lazyColumns) {
+ extraData[col] = record.data[col];
+ }
+ const evalContext = Object.assign({}, record.evalContext, extraData);
+ for (const [cssClass, expr] of Object.entries(this.fieldDecorations[node.attrs.name])) {
+ $res.toggleClass(cssClass, py.PY_isTrue(py.evaluate(expr, evalContext)));
+ }
+ }
+ return $res;
+ },
+
+ _renderHeaderCell: function (node) {
+ const $res = this._super.apply(this, arguments);
+ if (this._isLazyNode(node)) {
+ const $spin = $('');
+ if (this.currentNbLazyRpc == 0) {
+ $spin.addClass("invisible");
+ }
+ $res.find(":last").before($spin);
+ }
+ return $res;
+ },
+});
+
+const LazyColumnList = ListView.extend({
+ config: Object.assign({}, ListView.prototype.config, {
+ Renderer: LazyColumnListRenderer,
+ Model: LazyColumnListModel,
+ }),
+});
+
+viewRegistry.add("lazy_column_list", LazyColumnList);
+
+export default LazyColumnList;
diff --git a/addons/stock/static/tests/lazy_column_list_tests.js b/addons/stock/static/tests/lazy_column_list_tests.js
new file mode 100644
index 00000000000..6844a787451
--- /dev/null
+++ b/addons/stock/static/tests/lazy_column_list_tests.js
@@ -0,0 +1,91 @@
+/** @odoo-module **/
+
+import testUtils from "web.test_utils";
+import LazyColumnList from "../src/js/lazy_column_list";
+import { makeDeferred, nextTick } from "../../../web/static/tests/helpers/utils";
+
+const createView = testUtils.createView;
+
+QUnit.module(
+ "Views",
+ {
+ beforeEach: function () {
+ this.data = {
+ person: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ slowField: { string: "Hard to Compute", type: "char" },
+ },
+ records: [
+ { id: 1, name: "Daniel Fortesque" },
+ { id: 2, name: "Samuel Oak" },
+ ],
+ },
+ };
+ },
+ },
+ function () {
+ QUnit.module("LazyColumnList");
+
+ QUnit.test("Loading correctly", async function (assert) {
+ assert.expect(12);
+ const promiseRead = makeDeferred();
+ const list = await createView({
+ View: LazyColumnList,
+ model: "person",
+ data: this.data,
+ arch:
+ '' +
+ '' +
+ '' +
+ "",
+ mockRPC: async function (route, args) {
+ console.log(route);
+ if (route === "/web/dataset/call_kw/person/read") {
+ const fields = args.args[1];
+ const records = [];
+ for (const d of this.data.person.records) {
+ let reco = { id: d.id };
+ for (const f of fields) {
+ if (f === "slowField") {
+ reco[f] = `${d.name} ${d.id}`;
+ } else {
+ reco[f] = d[f];
+ }
+ }
+ records.push(reco);
+ }
+ await promiseRead;
+ return Promise.resolve(records);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ // Checks we have initially 2 records
+ assert.containsN(list, ".o_data_row", 2, "should have 2 records");
+
+ let $spin = list.$el.find("th[data-name='slowField'] .fa.fa-spin");
+ let $results = list.$el.find("td[name='slowField']");
+ assert.equal($spin.length, 1);
+ assert.equal($spin.hasClass("invisible"), false);
+ assert.equal($results.length, 2);
+ assert.equal($results.first().text(), "");
+ assert.equal($results.last().text(), "");
+
+ promiseRead.resolve();
+ await nextTick();
+
+ $spin = list.$el.find("th[data-name='slowField'] .fa.fa-spin");
+ $results = list.$el.find("td[name='slowField']");
+ assert.equal($spin.length, 1);
+ assert.equal($spin.hasClass("invisible"), true);
+ assert.equal($results.length, 2);
+ assert.equal($results.first().text(), "Daniel Fortesque 1");
+ assert.equal($results.last().text(), "Samuel Oak 2");
+
+ assert.containsN(list, ".o_data_row", 2, "should have 2 records");
+
+ list.destroy();
+ });
+ }
+);
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 05267039e7e..ab9e2cb9c8b 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -189,7 +189,7 @@
stock.picking.tree
stock.picking
-
+
@@ -203,8 +203,8 @@
-
-
+