diff --git a/web_widget_x2many_2d_matrix/__manifest__.py b/web_widget_x2many_2d_matrix/__manifest__.py index d50a0d521150..eb38ecb28586 100644 --- a/web_widget_x2many_2d_matrix/__manifest__.py +++ b/web_widget_x2many_2d_matrix/__manifest__.py @@ -5,7 +5,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "2D matrix for x2many fields", - "version": "15.0.1.0.2", + "version": "16.0.1.0.0", "maintainers": ["ChrisOForgeFlow"], "development_status": "Production/Stable", "author": ( @@ -13,6 +13,7 @@ "Tecnativa, " "Camptocamp, " "CorporateHub, " + "Onestein, " "Odoo Community Association (OCA)" ), "website": "https://github.com/OCA/web", @@ -24,11 +25,16 @@ "installable": True, "assets": { "web.assets_backend": [ - "web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss", - "web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js", - "web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js", - "web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js", - "web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js", + "web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/" + "x2many_2d_matrix_renderer.esm.js", + "web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/" + "x2many_2d_matrix_renderer.xml", + "web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/" + "x2many_2d_matrix_field.esm.js", + "web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/" + "x2many_2d_matrix_field.xml", + "web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/" + "x2many_2d_matrix_field.scss", ], }, } diff --git a/web_widget_x2many_2d_matrix/readme/USAGE.rst b/web_widget_x2many_2d_matrix/readme/USAGE.rst index 514257d0065b..bab81c79b3c3 100644 --- a/web_widget_x2many_2d_matrix/readme/USAGE.rst +++ b/web_widget_x2many_2d_matrix/readme/USAGE.rst @@ -23,10 +23,6 @@ field_x_axis The field that indicates the x value of a point field_y_axis The field that indicates the y value of a point -field_label_x_axis - Use another field to display in the table header -field_label_y_axis - Use another field to display in the table header field_value Show this field as value show_row_totals diff --git a/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.esm.js b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.esm.js new file mode 100644 index 000000000000..65cdc17d03f8 --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.esm.js @@ -0,0 +1,80 @@ +/** @odoo-module **/ + +import {Component} from "@odoo/owl"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {registry} from "@web/core/registry"; +import {archParseBoolean} from "@web/views/utils"; +import {X2Many2DMatrixRenderer} from "@web_widget_x2many_2d_matrix/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm"; +import { _lt } from "@web/core/l10n/translation"; + +export class X2Many2DMatrixField extends Component { + + setup() { + this.activeField = this.props.record.activeFields[this.props.name]; + } + + _getDefaultRecordValues() { + return {}; + } + + async commitChange(x, y, value) { + const fields = this.props.matrixFields; + + const values = this._getDefaultRecordValues(); + values[fields.x] = x; + values[fields.y] = y; + + const matchingRecords = this.props.value.records.filter( + (record) => record.data[fields.x] === x && record.data[fields.y] === y + ); + if (matchingRecords.length === 1) { + values[fields.value] = value; + await matchingRecords[0].update(values); + } else { + let total = 0; + if (matchingRecords.length) { + total = matchingRecords + .map((r) => r.data[fields.value]) + .reduce((aggr, v) => aggr + v); + } + const diff = value - total; + values[fields.value] = diff; + const record = await this.list.addNew({ + mode: "edit", + }); + await record.update(values); + } + this.props.setDirty(false); + } +} + +X2Many2DMatrixField.components = {X2Many2DMatrixRenderer}; +X2Many2DMatrixField.displayName = _lt("X2Many2DMatrixField Table"); +X2Many2DMatrixField.template = "web_widget_x2many_2d_matrix.X2Many2DMatrixField"; +X2Many2DMatrixField.props = { + ...standardFieldProps, + matrixFields: Object, + isXClickable: Boolean, + isYClickable: Boolean, + showRowTotals: Boolean, + showColumnTotals: Boolean, +}; +X2Many2DMatrixField.extractProps = ({attrs}) => { + return { + matrixFields: { + value: attrs.field_value, + x: attrs.field_x_axis, + y: attrs.field_y_axis, + }, + isXClickable: archParseBoolean(attrs.x_axis_clickable), + isYClickable: archParseBoolean(attrs.y_axis_clickable), + showRowTotals: + "show_row_totals" in attrs ? archParseBoolean(attrs.show_row_totals) : false, + showColumnTotals: + "show_column_totals" in attrs + ? archParseBoolean(attrs.show_column_totals) + : false, + }; +}; + +registry.category("fields").add("x2many_2d_matrix", X2Many2DMatrixField); diff --git a/web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.scss similarity index 96% rename from web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss rename to web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.scss index 8596f01ef967..424527189acd 100644 --- a/web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss +++ b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.scss @@ -6,7 +6,7 @@ $x2many_2d_matrix_max_height: 450px; overflow-y: auto; } - .o_x2many_2d_matrix.o_list_view { + .table { > thead > tr > th { white-space: pre-line; position: sticky; @@ -42,6 +42,7 @@ $x2many_2d_matrix_max_height: 450px; box-shadow: -1px 5px 10px $gray-300; } &.row-total { + padding: 0.75rem; font-weight: bold; position: sticky; right: 0; @@ -54,7 +55,7 @@ $x2many_2d_matrix_max_height: 450px; } } - > tfoot > tr > td { + > tfoot > tr > th { padding: 0.75rem; text-align: left; background-color: $o-list-footer-bg-color; diff --git a/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.xml b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.xml new file mode 100644 index 000000000000..c97372eb455c --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_field/x2many_2d_matrix_field.xml @@ -0,0 +1,16 @@ + + + +
+ +
+
+
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 new file mode 100644 index 000000000000..cdc0f23ec5bb --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm.js @@ -0,0 +1,155 @@ +/** @odoo-module **/ + +import {Component, onWillUpdateProps} from "@odoo/owl"; +import {registry} from "@web/core/registry"; + +export class X2Many2DMatrixRenderer extends Component { + + setup() { + this.ValueFieldComponent = this._getValueFieldComponent(); + this.AggregateFieldComponent = this._getAggregateFieldComponent(); + this.columns = this._getColumns(); + this.rows = this._getRows(); + this.matrix = this._getMatrix(); + + onWillUpdateProps((newProps) => { + this.columns = this._getColumns(newProps.matrixRows.records); + this.rows = this._getRows(newProps.matrixRows.records); + this.matrix = this._getMatrix(newProps.matrixRows.records); + }); + } + + _getColumns(records = this.matrixRows.records) { + const columns = []; + records.forEach((record) => { + const column = { + value: record.data[this.matrixFields.x], + text: record.data[this.matrixFields.x], + }; + if (record.fields[this.matrixFields.x].type === "many2one") { + column.value = column.value[0]; + column.text = column.value[1]; + } + if (columns.findIndex((c) => c.value === column.value) !== -1) return; + columns.push(column); + }); + return columns; + } + + _getRows(records = this.matrixRows.records) { + const rows = []; + records.forEach((record) => { + const row = { + value: record.data[this.matrixFields.y], + text: record.data[this.matrixFields.y], + }; + if (record.fields[this.matrixFields.y].type === "many2one") { + row.value = row.value[0]; + row.text = row.value[1]; + } + if (rows.findIndex((r) => r.value === row.value) !== -1) return; + rows.push(row); + }); + return rows; + } + + _getPointOfRecord(record) { + let xValue = record.data[this.matrixFields.x]; + if (record.fields[this.matrixFields.x].type === "many2one") { + xValue = xValue[0]; + } + let yValue = record.data[this.matrixFields.y]; + if (record.fields[this.matrixFields.y].type === "many2one") { + yValue = yValue[0]; + } + + const x = this.columns.findIndex((c) => c.value === xValue); + const y = this.rows.findIndex((r) => r.value === yValue); + return {x, y}; + } + + _getMatrix(records = this.matrixRows.records) { + const matrix = this.rows.map(() => + new Array(this.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); + matrix[y][x].value += value; + matrix[y][x].records.push(record); + }); + return matrix; + } + + get matrixRows() { + return this.props.matrixRows; + } + + get matrixFields() { + return this.props.matrixFields; + } + + _getValueFieldComponent() { + return this.matrixRows.activeFields[this.matrixFields.value].FieldComponent; + } + + _getAggregateFieldComponent() { + return registry.category("fields").get("char") + } + + _aggregateRow(row) { + const y = this.rows.findIndex((r) => r.value === row); + return this.matrix[y].map((r) => r.value).reduce((aggr, x) => aggr + x); + } + + _aggregateColumn(column) { + const x = this.columns.findIndex((c) => c.value === column); + return this.matrix + .map((r) => r[x]) + .map((r) => r.value) + .reduce((aggr, y) => aggr + y); + } + + _aggregateAll() { + return this.matrix + .map((r) => r.map((x) => x.value).reduce((aggr, x) => aggr + x)) + .reduce((aggr, y) => aggr + y); + } + + update(x, y, value) { + this.matrix[y][x].value = value; + const xFieldValue = this.columns[x].value; + const yFieldValue = this.rows[y].value; + + this.props.onUpdate(xFieldValue, yFieldValue, value); + } + + getValueFieldProps(column, row) { + const x = this.columns.findIndex((c) => c.value === column); + const y = this.rows.findIndex((r) => r.value === row); + const props = this.matrixRows.activeFields[this.matrixFields.value].props; + const propsFromAttrs = this.matrixRows.activeFields[this.matrixFields.value].propsFromAttrs; + const record = this.matrix[y][x].records[0]; + return { + ...props, + ...propsFromAttrs, + value: record.data[this.matrixFields.value], + update: (value) => this.update(x, y, value), + readonly: this.props.readonly, + record: record, + }; + } +} + +X2Many2DMatrixRenderer.template = "web_widget_x2many_2d_matrix.X2Many2DMatrixRenderer"; +X2Many2DMatrixRenderer.props = { + matrixRows: Object, + matrixFields: Object, + setDirty: Function, + onUpdate: Function, + readonly: Boolean, + showRowTotals: Boolean, + showColumnTotals: Boolean, +}; diff --git a/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.xml b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.xml new file mode 100644 index 000000000000..64201a05f637 --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ + + + +
+
+ Nothing to display. +
+
+
diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js deleted file mode 100644 index 85374a5fc953..000000000000 --- a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js +++ /dev/null @@ -1,617 +0,0 @@ -/* Copyright 2018 Simone Orsi - * Copyright 2018 Brainbean Apps - * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ - -odoo.define("web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer", function (require) { - "use strict"; - - var BasicRenderer = require("web.BasicRenderer"); - var config = require("web.config"); - var core = require("web.core"); - var field_utils = require("web.field_utils"); - var utils = require("web.utils"); - var _t = core._t; - - var FIELD_CLASSES = { - float: "o_list_number", - integer: "o_list_number", - monetary: "o_list_number", - text: "o_list_text", - }; - - // X2Many2dMatrixRenderer is heavily inspired by Odoo's ListRenderer - // and is reusing portions of code from list_renderer.js - var X2Many2dMatrixRenderer = BasicRenderer.extend({ - /** - * @override - */ - init: function (parent, state, params) { - this._super.apply(this, arguments); - this.editable = params.editable; - this._saveMatrixData(params.matrix_data); - }, - - /** - * Update matrix data in current renderer instance. - * - * @param {Object} matrixData Contains the matrix data - */ - _saveMatrixData: function (matrixData) { - this.columns = matrixData.columns; - this.rows = matrixData.rows; - this.matrix_data = matrixData; - }, - - /** - * Main render function for the matrix widget. - * - * It is rendered as a table. For now, - * this method does not wait for the field widgets to be ready. - * - * @override - * @private - * @returns {Deferred} this deferred is resolved immediately - */ - _renderView: function () { - var self = this; - - this.$el.removeClass("table-responsive").empty(); - - // Display a nice message if there's no data to display - if (!self.rows.length) { - var $alert = $("
", {class: "alert alert-info"}); - $alert.text(_t("Sorry no matrix data to display.")); - this.$el.append($alert); - return this._super(); - } - - var $table = $("").addClass( - "o_list_view table table-condensed table-striped " + - "o_x2many_2d_matrix " - ); - this.$el.addClass("table-responsive").append($table); - - this._computeColumnAggregates(); - this._computeRowAggregates(); - - // We need to initialize the deferred list object for inherited functions that use this.defs even if it - // is empty at the moment. - var defs = []; - this.defs = defs; - - $table.append(this._renderHeader()).append(this._renderBody()); - if (self.matrix_data.show_column_totals) { - $table.append(this._renderFooter()); - } - delete this.defs; - return this._super(); - }, - - /** - * Render the table body. - * - * Looks for the table body and renders the rows in it. - * Also it sets the tabindex on every input element. - * - * @private - * @returns {jQueryElement} The table body element just filled. - */ - _renderBody: function () { - var $body = $("").append(this._renderRows()); - _.each($body.find("input"), function (td, i) { - $(td).attr("tabindex", i); - }); - return $body; - }, - - /** - * Render the table head of our matrix. Looks for the first table head - * and inserts the header into it. - * - * @private - * @returns {jQueryElement} The thead element that was inserted into. - */ - _renderHeader: function () { - var $tr = $("").append("").append($tr); - }, - - /** - * Render a single header cell. - * - * Creates a th and adds the description as text. - * - * @private - * @param {jQueryElement} node - * @returns {jQueryElement} the created . - * If aggregate is set on the row it also will generate - * the aggregate cell. - * - * @private - * @param {Object} row The row that will be rendered. - * @returns {jQueryElement} the element that has been rendered. - */ - _renderRow: function (row) { - var $tr = $("", {class: "o_data_row"}), - _data = _.without(row.data, undefined); - $tr = $tr.append(this._renderLabelCell(_data[0])); - var $cells = this.columns.map( - function (column, index) { - var record = row.data[index]; - // Make the widget use our field value for each cell - column.attrs.name = this.matrix_data.field_value; - return this._renderBodyCell(record, column, index, {mode: ""}); - }.bind(this) - ); - $tr = $tr.append($cells); - if (row.aggregate) { - $tr.append(this._renderAggregateRowCell(row)); - } - return $tr; - }, - - /** - * Renders the label for a specific row. - * - * @private - * @param {Object} record Contains the information about the record. - * @returns {jQueryElement} the cell that was rendered. - */ - _renderLabelCell: function (record) { - var $td = $("").append("").append($tr); - } - }, - - /** - * Renders the total cell (of all rows / columns) - * - * @private - * @returns {jQueryElement} The td element with the total in it. - */ - _renderTotalCell: function () { - if ( - !this.matrix_data.show_column_totals || - !this.matrix_data.show_row_totals - ) { - return; - } - - var $cell = $("
"); - $tr = $tr.append(_.map(this.columns, this._renderHeaderCell.bind(this))); - if (this.matrix_data.show_row_totals) { - $tr.append($("", {class: "total"})); - } - return $("
node. - */ - _renderHeaderCell: function (node) { - var name = node.attrs.name; - var field = this.state.fields[name]; - var $th = $(""); - if (!field) { - return $th; - } - var description = null; - if (node.attrs.widget) { - description = - this.state.fieldsInfo.list[name].Widget.prototype.description; - } - if (_.isNull(description)) { - description = node.attrs.string || field.string; - } - $th.text(description).data("name", name); - - if ( - field.type === "float" || - field.type === "integer" || - field.type === "monetary" - ) { - $th.addClass("text-right"); - } else { - $th.addClass("text-center"); - } - - if (config.isDebug()) { - var fieldDescr = { - field: field, - name: name, - string: description || name, - record: this.state, - attrs: node.attrs, - }; - this._addFieldTooltip(fieldDescr, $th); - } - return $th; - }, - - /** - * Proxy call to function rendering single row. - * - * @private - * @returns {String} a string with the generated html. - */ - _renderRows: function () { - return _.map( - this.rows, - function (row) { - row.attrs.name = this.matrix_data.field_value; - return this._renderRow(row); - }.bind(this) - ); - }, - - /** - * Render a single row with all its columns. - * Renders all the cells and then wraps them with a
"); - var value = record.data[this.matrix_data.field_label_y_axis]; - if (value.type === "record") { - // We have a related record - value = value.data.display_name; - } - // Get 1st column filled w/ Y label - $td.text(value); - return $td; - }, - - /** - * Create a cell and fill it with the aggregate value. - * - * @private - * @param {Object} row the row object to aggregate. - * @returns {jQueryElement} The rendered cell. - */ - _renderAggregateRowCell: function (row) { - var $cell = $("", {class: "row-total"}); - this.applyAggregateValue($cell, row); - return $cell; - }, - - /** - * Render a single body Cell. - * Gets the field and renders the widget. We force the edit mode, since - * we always want the widget to be editable. - * - * @private - * @param {Object} record Contains the data for this cell - * @param {jQueryElement} node The HTML of the field. - * @param {int} colIndex The index of the current column. - * @param {Object} options The obtions used for the widget - * @returns {jQueryElement} the rendered cell. - */ - _renderBodyCell: function (record, node, colIndex, options) { - var tdClassName = "o_data_cell"; - if (node.tag === "field") { - var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type]; - if (typeClass) { - tdClassName += " " + typeClass; - } - if (node.attrs.widget) { - tdClassName += " o_" + node.attrs.widget + "_cell"; - } - } - - // TODO roadmap: here we should collect possible extra params - // the user might want to attach to each single cell. - - var $td = $("", { - class: tdClassName, - }); - - if (_.isUndefined(record)) { - // Without record, nothing elese to do - return $td; - } - $td.attr({ - "data-form-id": record.id, - "data-id": record.data.id, - }); - - // We register modifiers on the element so that it gets - // the correct modifiers classes (for styling) - var modifiers = this._registerModifiers( - node, - record, - $td, - _.pick(options, "mode") - ); - // If the invisible modifiers is true, the element is - // left empty. Indeed, if the modifiers was to change the - // whole cell would be rerendered anyway. - if (modifiers.invisible && !(options && options.renderInvisible)) { - return $td; - } - - // Enforce mode of the parent - options.mode = this.getParent().mode; - - if (node.tag === "widget") { - return $td.append(this._renderWidget(record, node)); - } - var $el = this._renderFieldWidget(node, record, _.pick(options, "mode")); - return $td.append($el); - }, - - /** - * Wraps the column aggregate with a tfoot element - * - * @private - * @returns {jQueryElement} The footer element with the cells in it. - */ - _renderFooter: function () { - var $cells = this._renderAggregateColCells(); - if ($cells) { - var $tr = $("
").append($cells); - var $total_cell = this._renderTotalCell(); - if ($total_cell) { - $tr.append($total_cell); - } - return $("
", {class: "col-total"}); - this.applyAggregateValue($cell, this.total); - return $cell; - }, - - /** - * Render the Aggregate cells for the column. - * - * @private - * @returns {List} the rendered cells - */ - _renderAggregateColCells: function () { - var self = this; - - return _.map(this.columns, function (column) { - var $cell = $(""); - if (config.isDebug()) { - $cell.addClass(column.attrs.name); - } - if (column.aggregate) { - self.applyAggregateValue($cell, column); - } - return $cell; - }); - }, - - /** - * Compute the column aggregates. - * This function is called everytime the value is changed. - * - * @private - */ - _computeColumnAggregates: function () { - if (!this.matrix_data.show_column_totals) { - return; - } - var fname = this.matrix_data.field_value, - field = this.state.fields[fname]; - if (!field) { - return; - } - var type = field.type; - if (!~["integer", "float", "monetary"].indexOf(type)) { - return; - } - this.total = { - attrs: { - name: fname, - }, - aggregate: { - help: _t("Sum Total"), - value: 0, - }, - }; - _.each( - this.columns, - function (column, index) { - column.aggregate = { - help: _t("Sum"), - value: 0, - }; - _.each(this.rows, function (row) { - // TODO Use only one _.propertyOf in underscore 1.9.0+ - try { - column.aggregate.value += row.data[index].data[fname]; - } catch (error) { - // Nothing to do - } - }); - this.total.aggregate.value += column.aggregate.value; - }.bind(this) - ); - }, - - _getRecord: function (recordId) { - var record = null; - utils.traverse_records(this.state, function (r) { - if (r.id === recordId) { - record = r; - } - }); - return record; - }, - - /** - * @override - */ - updateState: function (state, params) { - if (params.matrix_data) { - this._saveMatrixData(params.matrix_data); - } - return this._super.apply(this, arguments); - }, - - /** - * Traverse the fields matrix with the keyboard - * - * @override - * @private - * @param {OdooEvent} event "navigation_move" event - */ - _onNavigationMove: function (event) { - var widgets = this.__parentedChildren, - index = widgets.indexOf(event.target), - first = index === 0, - last = index === widgets.length - 1, - move = 0; - // Guess if we have to move the focus - if (event.data.direction === "next" && !last) { - move = 1; - } else if (event.data.direction === "previous" && !first) { - move = -1; - } - // Move focus - if (move) { - var target = widgets[index + move]; - index = this.allFieldWidgets[target.record.id].indexOf(target); - this._activateFieldWidget(target.record, index, {inc: 0}); - event.stopPropagation(); - } - }, - - /** - * Compute the row aggregates. - * - * This function is called everytime the value is changed. - * - * @private - */ - _computeRowAggregates: function () { - if (!this.matrix_data.show_row_totals) { - return; - } - var fname = this.matrix_data.field_value, - field = this.state.fields[fname]; - if (!field) { - return; - } - var type = field.type; - if (!~["integer", "float", "monetary"].indexOf(type)) { - return; - } - _.each(this.rows, function (row) { - row.aggregate = { - help: _t("Sum"), - value: 0, - }; - _.each(row.data, function (col) { - // TODO Use _.property in underscore 1.9+ - try { - row.aggregate.value += col.data[fname]; - } catch (error) { - // Nothing to do - } - }); - }); - }, - - /** - * Takes the given Value, formats it and adds it to the given cell. - * - * @private - * - * @param {jQueryElement} $cell - * The Cell where the aggregate should be added. - * - * @param {Object} axis - * The object which contains the information about the aggregate value axis - */ - applyAggregateValue: function ($cell, axis) { - var field = this.state.fields[axis.attrs.name]; - var value = axis.aggregate.value; - var help = axis.aggregate.help; - var fieldInfo = this.state.fieldsInfo.list[axis.attrs.name]; - var formatFunc = - field_utils.format[fieldInfo.widget ? fieldInfo.widget : field.type]; - var formattedValue = formatFunc(value, field, {escape: true}); - $cell.addClass("o_list_number").attr("title", help).html(formattedValue); - }, - - /** - * Check if the change was successful and then update the grid. - * This function is required on relational fields. - * - * @param {Object} state - * Contains the current state of the field & all the data - * - * @param {String} id - * the id of the updated object. - * - * @param {Array} fields - * The fields we have in the view. - * - * @param {Object} ev - * The event object. - * - * @returns {Deferred} - * The deferred object thats gonna be resolved when the change is made. - */ - confirmUpdate: function (state, id, fields, ev) { - var self = this; - this.state = state; - return this.confirmChange(state, id, fields, ev).then(function () { - self._refresh(id); - }); - }, - - /** - * Refresh our grid. - * - * @private - * @param {String} id Datapoint ID - */ - _refresh: function (id) { - this._updateRow(id); - this._refreshColTotals(); - this._refreshRowTotals(); - }, - - /** - *Update row data in our internal rows. - * - * @param {String} id: The id of the row that needs to be updated. - */ - _updateRow: function (id) { - var record = _.findWhere(this.state.data, {id: id}), - _id = _.property("id"); - _.each(this.rows, function (row) { - _.each(row.data, function (col, i) { - if (_id(col) === id) { - row.data[i] = record; - } - }); - }); - }, - - /** - * Update the row total. - */ - _refreshColTotals: function () { - this._computeColumnAggregates(); - this.$("tfoot").replaceWith(this._renderFooter()); - }, - - /** - * Update the column total. - */ - _refreshRowTotals: function () { - var self = this; - this._computeRowAggregates(); - var $rows = self.$el.find("tr.o_data_row"); - _.each(self.rows, function (row, i) { - if (row.aggregate) { - $($rows[i]) - .find(".row-total") - .replaceWith(self._renderAggregateRowCell(row)); - } - }); - }, - - /** - * X2many fields expect this - * - * @returns {null} - */ - getEditableRecordID: function () { - return null; - }, - }); - - return X2Many2dMatrixRenderer; -}); diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js deleted file mode 100644 index a223cf804ade..000000000000 --- a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright 2019 Alexandre Díaz - * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ - -odoo.define("web_widget_x2many_2d_matrix.X2Many2dMatrixView", function (require) { - "use strict"; - - var BasicView = require("web.BasicView"); - - BasicView.include({ - _processField: function (viewType, field, attrs) { - // Workaround for kanban mode rendering. - // Source of the issue: https://github.com/OCA/OCB/blob/12.0/addons/web/static/src/js/views/basic/basic_view.js#L303 . - // See https://github.com/OCA/web/pull/1404#pullrequestreview-305813206 . - // In the long term we should a way to handle kanban mode - // better (eg: a specific renderer). - if (attrs.widget === "x2many_2d_matrix") { - attrs.mode = "tree"; - } - return this._super(viewType, field, attrs); - }, - }); -}); diff --git a/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js b/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js deleted file mode 100644 index 3303f86cb4d6..000000000000 --- a/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js +++ /dev/null @@ -1,16 +0,0 @@ -odoo.define("web_widget_x2many_2d_matrix.matrix_limit_extend", function (require) { - "use strict"; - - var FormView = require("web.FormView"); - - FormView.include({ - // We extend this method so that the view is not limited to - // just 40 cells when the 'x2many_2d_matrix' widget is used. - _setSubViewLimit: function (attrs) { - this._super(attrs); - if (attrs.widget === "x2many_2d_matrix") { - attrs.limit = Infinity; - } - }, - }); -}); diff --git a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js deleted file mode 100644 index 0f374060333a..000000000000 --- a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js +++ /dev/null @@ -1,262 +0,0 @@ -/* Copyright 2015 Holger Brunn - * Copyright 2016 Pedro M. Baeza - * Copyright 2018 Simone Orsi - * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ - -odoo.define("web_widget_x2many_2d_matrix.widget", function (require) { - "use strict"; - - var field_registry = require("web.field_registry"); - var relational_fields = require("web.relational_fields"); - var X2Many2dMatrixRenderer = require("web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer"); - - var WidgetX2Many2dMatrix = relational_fields.FieldOne2Many.extend({ - widget_class: "o_form_field_x2many_2d_matrix", - - /** - * Initialize the widget & parameters. - * - * @param {Object} parent contains the form view. - * @param {String} name the name of the field. - * @param {Object} record information about the database records. - * @param {Object} options view options. - */ - init: function (parent, name, record, options) { - this._super(parent, name, record, options); - this.init_params(); - }, - - /** - * Initialize the widget specific parameters. - * Sets the axis and the values. - */ - init_params: function () { - var node = this.attrs; - this.by_y_axis = {}; - this.x_axis = []; - this.y_axis = []; - this.field_x_axis = node.field_x_axis || this.field_x_axis; - this.field_y_axis = node.field_y_axis || this.field_y_axis; - this.field_label_x_axis = node.field_label_x_axis || this.field_x_axis; - this.field_label_y_axis = node.field_label_y_axis || this.field_y_axis; - this.x_axis_clickable = this.parse_boolean(node.x_axis_clickable || "1"); - this.y_axis_clickable = this.parse_boolean(node.y_axis_clickable || "1"); - this.field_value = node.field_value || this.field_value; - // TODO: is this really needed? Holger? - for (var property in node) { - if (property.startsWith("field_att_")) { - this.fields_att[property.substring(10)] = node[property]; - } - } - var field_defs = this.recordData[this.name].fields; - // TODO: raise when any of the fields above don't exist with a - // helpful error message - if (!field_defs[this.field_value]) { - throw new Error( - _.str.sprintf( - "You need to include %s in your view definition", - this.field_value - ) - ); - } - this.show_row_totals = this.parse_boolean( - node.show_row_totals || - this.is_aggregatable(field_defs[this.field_value]) - ? "1" - : "" - ); - this.show_column_totals = this.parse_boolean( - node.show_column_totals || - this.is_aggregatable(field_defs[this.field_value]) - ? "1" - : "" - ); - }, - - /** - * Initializes the Value matrix. - * - * Puts the values in the grid. - * If we have related items we use the display name. - */ - init_matrix: function () { - var records = this.recordData[this.name].data; - // Wipe the content if something still exists - this.by_y_axis = {}; - this.x_axis = []; - this.y_axis = []; - _.each( - records, - function (record) { - var x = record.data[this.field_label_x_axis], - y = record.data[this.field_label_y_axis]; - if (x.type === "record") { - // We have a related record - x = x.data.display_name; - } - if (y.type === "record") { - // We have a related record - y = y.data.display_name; - } - this.by_y_axis[y] = this.by_y_axis[y] || {}; - this.by_y_axis[y][x] = record; - if (this.y_axis.indexOf(y) === -1) { - this.y_axis.push(y); - } - if (this.x_axis.indexOf(x) === -1) { - this.x_axis.push(x); - } - }.bind(this) - ); - // Init columns - this.columns = []; - _.each( - this.x_axis, - function (x) { - this.columns.push(this._make_column(x)); - }.bind(this) - ); - this.rows = []; - _.each( - this.y_axis, - function (y) { - this.rows.push(this._make_row(y)); - }.bind(this) - ); - this.matrix_data = { - field_value: this.field_value, - field_x_axis: this.field_x_axis, - field_y_axis: this.field_y_axis, - field_label_x_axis: this.field_label_x_axis, - field_label_y_axis: this.field_label_y_axis, - columns: this.columns, - rows: this.rows, - show_row_totals: this.show_row_totals, - show_column_totals: this.show_column_totals, - }; - }, - - /** - * Create scaffold for a column. - * - * @param {String} x The string used as a column title - * @returns {Object} - */ - _make_column: function (x) { - return { - // Simulate node parsed on xml arch - tag: "field", - attrs: { - name: this.field_x_axis, - string: x, - }, - }; - }, - - /** - * Create scaffold for a row. - * - * @param {String} y The string used as a row title - * @returns {Object} - */ - _make_row: function (y) { - var self = this; - // Use object so that we can attach more data if needed - var row = { - tag: "field", - attrs: { - name: this.field_y_axis, - string: y, - }, - data: [], - }; - _.each(self.x_axis, function (x) { - row.data.push(self.by_y_axis[y][x]); - }); - return row; - }, - - /** - * Determine if a field represented by field_def can be aggregated - */ - is_aggregatable: function (field_def) { - return field_def.type in {float: 1, monetary: 1, integer: 1}; - }, - - /** - * Parse a String containing a bool and convert it to a JS bool. - * - * @param {String} val: the string to be parsed. - * @returns {Boolean} The parsed boolean. - */ - parse_boolean: function (val) { - if (val.toLowerCase() === "true" || val === "1") { - return true; - } - return false; - }, - - /** - * Create the matrix renderer and add its output to our element - * - * @returns {Deferred} - * A deferred object to be completed when it finished rendering. - */ - _render: function () { - if (!this.view) { - return this._super(); - } - // Ensure widget is re initiated when rendering - this.init_matrix(); - var arch = this.view.arch; - // Update existing renderer - if (!_.isUndefined(this.renderer)) { - return this.renderer.updateState(this.value, { - matrix_data: this.matrix_data, - }); - } - // Create a new matrix renderer - this.renderer = new X2Many2dMatrixRenderer(this, this.value, { - arch: arch, - editable: this.mode === "edit" && arch.attrs.editable, - viewType: "list", - matrix_data: this.matrix_data, - }); - this.$el.addClass("o_field_x2many o_field_x2many_2d_matrix"); - return this.renderer.appendTo(this.$el); - }, - - /** - * Activate the widget. - * - * @override - */ - activate: function (options) { - // Won't work fine without https://github.com/odoo/odoo/pull/26490 - // TODO Use _.propertyOf in underscore 1.9+ - try { - this._backwards = options.event.data.direction === "previous"; - } catch (error) { - this._backwards = false; - } - var result = this._super.apply(this, arguments); - delete this._backwards; - return result; - }, - - /** - * Get first element to focus. - * - * @override - */ - getFocusableElement: function () { - return this.$(".o_input:" + (this._backwards ? "last" : "first")); - }, - }); - - field_registry.add("x2many_2d_matrix", WidgetX2Many2dMatrix); - - return { - WidgetX2Many2dMatrix: WidgetX2Many2dMatrix, - }; -});