Browse Source

Merge pull request #1022 from Tecnativa/11.0-matrix-tabnavigation

[FIX] web_widget_x2many_2d_matrix: Fix linters  and enable keyboard navigation
pull/1038/head
Pedro M. Baeza 6 years ago
committed by GitHub
parent
commit
6d5751c592
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      web_widget_x2many_2d_matrix/README.rst
  2. 326
      web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js
  3. 196
      web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js

25
web_widget_x2many_2d_matrix/README.rst

@ -39,7 +39,9 @@ Use this widget by saying::
This assumes that my_field refers to a model with the fields `x`, `y` and This assumes that my_field refers to a model with the fields `x`, `y` and
`value`. If your fields are named differently, pass the correct names as `value`. If your fields are named differently, pass the correct names as
attributes::
attributes:
.. code-block:: xml
<field name="my_field" widget="x2many_2d_matrix" field_x_axis="my_field1" field_y_axis="my_field2" field_value="my_field3"> <field name="my_field" widget="x2many_2d_matrix" field_x_axis="my_field1" field_y_axis="my_field2" field_value="my_field3">
<tree> <tree>
@ -80,7 +82,9 @@ You need a data structure already filled with values. Let's assume we want to
use this widget in a wizard that lets the user fill in planned hours for one use this widget in a wizard that lets the user fill in planned hours for one
task per project per user. In this case, we can use ``project.task`` as our task per project per user. In this case, we can use ``project.task`` as our
data model and point to it from our wizard. The crucial part is that we fill data model and point to it from our wizard. The crucial part is that we fill
the field in the default function::
the field in the default function:
.. code-block:: python
from odoo import fields, models from odoo import fields, models
@ -102,7 +106,8 @@ the field in the default function::
'message_needaction': False, 'message_needaction': False,
'date_deadline': fields.Date.today(), 'date_deadline': fields.Date.today(),
}) })
# if the project doesn't have a task for the user, create a new one
# if the project doesn't have a task for the user,
# create a new one
if not p.task_ids.filtered(lambda x: x.user_id == u) else if not p.task_ids.filtered(lambda x: x.user_id == u) else
# otherwise, return the task # otherwise, return the task
(4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id) (4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id)
@ -112,7 +117,9 @@ the field in the default function::
task_ids = fields.Many2many('project.task', default=_default_task_ids) task_ids = fields.Many2many('project.task', default=_default_task_ids)
Now in our wizard, we can use::
Now in our wizard, we can use:
.. code-block:: xml
<field name="task_ids" widget="x2many_2d_matrix" field_x_axis="project_id" field_y_axis="user_id" field_value="planned_hours"> <field name="task_ids" widget="x2many_2d_matrix" field_x_axis="project_id" field_y_axis="user_id" field_value="planned_hours">
<tree> <tree>
@ -123,7 +130,6 @@ Now in our wizard, we can use::
</tree> </tree>
</field> </field>
Known issues / Roadmap Known issues / Roadmap
====================== ======================
@ -134,6 +140,13 @@ Known issues / Roadmap
* Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901 * Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901
* Support cell traversal through keyboard arrows.
* Entering the widget from behind by pressing ``Shift+TAB`` in your keyboard
will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490
is merged.
* Support extra invisible fields inside each cell.
Bug Tracker Bug Tracker
=========== ===========
@ -154,7 +167,7 @@ Contributors
* Artem Kostyuk <a.kostyuk@mobilunity.com> * Artem Kostyuk <a.kostyuk@mobilunity.com>
* Simone Orsi <simone.orsi@camptocamp.com> * Simone Orsi <simone.orsi@camptocamp.com>
* Timon Tschanz <timon.tschanz@camptocamp.com> * Timon Tschanz <timon.tschanz@camptocamp.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
Maintainer Maintainer
---------- ----------

326
web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js

@ -4,15 +4,14 @@
odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) { odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) {
"use strict"; "use strict";
// heavily inspired by Odoo's `ListRenderer`
// Heavily inspired by Odoo's `ListRenderer`
var BasicRenderer = require('web.BasicRenderer'); var BasicRenderer = require('web.BasicRenderer');
var config = require('web.config'); var config = require('web.config');
var field_utils = require('web.field_utils');
var utils = require('web.utils');
var core = require('web.core'); var core = require('web.core');
var _t = core._t
var field_utils = require('web.field_utils');
var _t = core._t;
var FIELD_CLASSES = { var FIELD_CLASSES = {
// copied from ListRenderer
// Copied from ListRenderer
float: 'o_list_number', float: 'o_list_number',
integer: 'o_list_number', integer: 'o_list_number',
monetary: 'o_list_number', monetary: 'o_list_number',
@ -21,34 +20,51 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
var X2Many2dMatrixRenderer = BasicRenderer.extend({ var X2Many2dMatrixRenderer = BasicRenderer.extend({
/**
* @override
*/
init: function (parent, state, params) { init: function (parent, state, params) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.editable = params.editable; this.editable = params.editable;
this.columns = params.matrix_data.columns;
this.rows = params.matrix_data.rows;
this.matrix_data = params.matrix_data;
this._saveMatrixData(params.matrix_data);
}, },
/** /**
* Main render function for the matrix widget. It is rendered as a table. For now,
* 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. * this method does not wait for the field widgets to be ready.
* *
* @override * @override
* @private * @private
* returns {Deferred} this deferred is resolved immediately
* @returns {Deferred} this deferred is resolved immediately
*/ */
_renderView: function () { _renderView: function () {
var self = this; var self = this;
// Display a nice message if there's no data to display // Display a nice message if there's no data to display
this.$el.empty(); this.$el.empty();
if (!self.rows.length){
if (!self.rows.length) {
var $alert = $('<div>', {'class': 'alert alert-info'}); var $alert = $('<div>', {'class': 'alert alert-info'});
$alert.text(_t('Sorry no matrix data to display.')); $alert.text(_t('Sorry no matrix data to display.'));
this.$el.append($alert); this.$el.append($alert);
return this._super(); return this._super();
} }
var $table = $('<table>').addClass('o_list_view table table-condensed table-striped');
var $table = $('<table>').addClass(
'o_list_view table table-condensed table-striped'
);
this.$el this.$el
.addClass('table-responsive') .addClass('table-responsive')
.append($table); .append($table);
@ -64,12 +80,15 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
} }
return this._super(); return this._super();
}, },
/** /**
* Render the table body. Looks for the table body and renders the rows in it.
* Render the table body.
*
* Looks for the table body and renders the rows in it.
* Also it sets the tabindex on every input element. * Also it sets the tabindex on every input element.
* *
* @private * @private
* return {jQueryElement} The table body element that was just filled.
* @returns {jQueryElement} The table body element just filled.
*/ */
_renderBody: function () { _renderBody: function () {
var $body = $('<tbody>').append(this._renderRows()); var $body = $('<tbody>').append(this._renderRows());
@ -78,23 +97,30 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
}); });
return $body; return $body;
}, },
/** /**
* Render the table head of our matrix. Looks for the first table head * Render the table head of our matrix. Looks for the first table head
* and inserts the header into it. * and inserts the header into it.
* *
* @private * @private
* @return {jQueryElement} The thead element that was inserted into.
* @returns {jQueryElement} The thead element that was inserted into.
*/ */
_renderHeader: function () { _renderHeader: function () {
var $tr = $('<tr>').append('<th/>'); var $tr = $('<tr>').append('<th/>');
$tr= $tr.append(_.map(this.columns, this._renderHeaderCell.bind(this)));
$tr = $tr.append(_.map(
this.columns,
this._renderHeaderCell.bind(this)
));
if (this.matrix_data.show_row_totals) { if (this.matrix_data.show_row_totals) {
$tr.append($('<th/>', {class: 'total'})); $tr.append($('<th/>', {class: 'total'}));
} }
return $('<thead>').append($tr); return $('<thead>').append($tr);
}, },
/** /**
* Render a single header cell. Creates a th and adds the description as text.
* Render a single header cell.
*
* Creates a th and adds the description as text.
* *
* @private * @private
* @param {jQueryElement} node * @param {jQueryElement} node
@ -107,16 +133,20 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
if (!field) { if (!field) {
return $th; return $th;
} }
var description;
var description = null;
if (node.attrs.widget) { if (node.attrs.widget) {
description = this.state.fieldsInfo.list[name].Widget.prototype.description;
description = this.state.fieldsInfo.list[name]
.Widget.prototype.description;
} }
if (description === undefined) {
if (_.isNull(description)) {
description = node.attrs.string || field.string; description = node.attrs.string || field.string;
} }
$th.text(description).data('name', name); $th.text(description).data('name', name);
if (field.type === 'float' || field.type === 'integer' || field.type === 'monetary') {
if (
field.type === 'float' || field.type === 'integer' ||
field.type === 'monetary'
) {
$th.addClass('text-right'); $th.addClass('text-right');
} }
@ -132,64 +162,68 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
} }
return $th; return $th;
}, },
/** /**
* Proxy call to function rendering single row. * Proxy call to function rendering single row.
* *
* @private * @private
* @returns {String} a string with the generated html. * @returns {String} a string with the generated html.
*
*/ */
_renderRows: function () { _renderRows: function () {
return _.map(this.rows, this._renderRow.bind(this)); return _.map(this.rows, this._renderRow.bind(this));
}, },
/** /**
* Render a single row with all its columns. Renders all the cells and then wraps them with a <tr>.
* If aggregate is set on the row it also will generate the aggregate cell.
* Render a single row with all its columns.
* Renders all the cells and then wraps them with a <tr>.
* If aggregate is set on the row it also will generate
* the aggregate cell.
* *
* @private * @private
* @param {Object} row: The row that will be rendered.
* @param {Object} row The row that will be rendered.
* @returns {jQueryElement} the <tr> element that has been rendered. * @returns {jQueryElement} the <tr> element that has been rendered.
*/ */
_renderRow: function (row) { _renderRow: function (row) {
var self = this;
var $tr = $('<tr/>', {class: 'o_data_row'});
$tr = $tr.append(self._renderLabelCell(row.data[0]));
var $tr = $('<tr/>', {class: 'o_data_row'}),
_data = _.without(row.data, undefined);
$tr = $tr.append(this._renderLabelCell(_data[0]));
var $cells = _.map(this.columns, function (node, index) { var $cells = _.map(this.columns, function (node, index) {
var record = row.data[index]; var record = row.data[index];
// make the widget use our field value for each cell
node.attrs.name = self.matrix_data.field_value;
return self._renderBodyCell(record, node, index, {mode:''});
});
// Make the widget use our field value for each cell
node.attrs.name = this.matrix_data.field_value;
return this._renderBodyCell(record, node, index, {mode:''});
}.bind(this));
$tr = $tr.append($cells); $tr = $tr.append($cells);
if (row.aggregate) { if (row.aggregate) {
$tr.append(self._renderAggregateRowCell(row));
$tr.append(this._renderAggregateRowCell(row));
} }
return $tr; return $tr;
}, },
/** /**
* Renders the label for a specific row. * Renders the label for a specific row.
* *
* @private * @private
* @params {Object} record: Contains the information about the record.
* @params {jQueryElement} the cell that was rendered.
* @param {Object} record Contains the information about the record.
* @returns {jQueryElement} the cell that was rendered.
*/ */
_renderLabelCell: function(record) {
_renderLabelCell: function (record) {
var $td = $('<td>'); var $td = $('<td>');
var value = record.data[this.matrix_data.field_y_axis]; var value = record.data[this.matrix_data.field_y_axis];
if (value.type == 'record') {
// we have a related record
if (value.type === 'record') {
// We have a related record
value = value.data.display_name; value = value.data.display_name;
} }
// get 1st column filled w/ Y label
// Get 1st column filled w/ Y label
$td.text(value); $td.text(value);
return $td; return $td;
}, },
/** /**
* Create a cell and fill it with the aggregate value. * Create a cell and fill it with the aggregate value.
* *
* @private * @private
* @param {Object} row: the row object to aggregate.
* @param {Object} row the row object to aggregate.
* @returns {jQueryElement} The rendered cell. * @returns {jQueryElement} The rendered cell.
*/ */
_renderAggregateRowCell: function (row) { _renderAggregateRowCell: function (row) {
@ -197,16 +231,17 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
this._apply_aggregate_value($cell, row.aggregate); this._apply_aggregate_value($cell, row.aggregate);
return $cell; return $cell;
}, },
/** /**
* Render a single body Cell. * Render a single body Cell.
* Gets the field and renders the widget. We force the edit mode, since * Gets the field and renders the widget. We force the edit mode, since
* we always want the widget to be editable. * we always want the widget to be editable.
* *
* @private * @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
* @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. * @returns {jQueryElement} the rendered cell.
*/ */
_renderBodyCell: function (record, node, colIndex, options) { _renderBodyCell: function (record, node, colIndex, options) {
@ -214,35 +249,52 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
if (node.tag === 'button') { if (node.tag === 'button') {
tdClassName += ' o_list_button'; tdClassName += ' o_list_button';
} else if (node.tag === 'field') { } else if (node.tag === 'field') {
var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type];
var typeClass = FIELD_CLASSES[
this.state.fields[node.attrs.name].type
];
if (typeClass) { if (typeClass) {
tdClassName += (' ' + typeClass);
tdClassName += ' ' + typeClass;
} }
if (node.attrs.widget) { if (node.attrs.widget) {
tdClassName += (' o_' + node.attrs.widget + '_cell');
tdClassName += ' o_' + node.attrs.widget + '_cell';
} }
} }
// TODO roadmap: here we should collect possible extra params // TODO roadmap: here we should collect possible extra params
// the user might want to attach to each single cell. // the user might want to attach to each single cell.
var $td = $('<td>', { var $td = $('<td>', {
'class': tdClassName, 'class': tdClassName,
});
if (_.isUndefined(record)) {
// Without record, nothing elese to do
return $td;
}
$td.attr({
'data-form-id': record.id, 'data-form-id': record.id,
'data-id': record.data.id, 'data-id': record.data.id,
}); });
// We register modifiers on the <td> 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 <td> element is left empty.
// Indeed, if the modifiers was to change the whole cell would be
// rerendered anyway.
// We register modifiers on the <td> 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 <td> element is
// left empty. Indeed, if the modifiers was to change the
// whole cell would be rerendered anyway.
if (modifiers.invisible && !(options && options.renderInvisible)) { if (modifiers.invisible && !(options && options.renderInvisible)) {
return $td; return $td;
} }
options.mode = this.getParent().mode; // enforce mode of the parent
var widget = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
// Enforce mode of the parent
options.mode = this.getParent().mode;
var widget = this._renderFieldWidget(
node, record, _.pick(options, 'mode')
);
this._handleAttributes(widget.$el, node); this._handleAttributes(widget.$el, node);
return $td.append(widget.$el); return $td.append(widget.$el);
}, },
/** /**
* Wraps the column aggregate with a tfoot element * Wraps the column aggregate with a tfoot element
* *
@ -252,10 +304,12 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
_renderFooter: function () { _renderFooter: function () {
var $cells = this._renderAggregateColCells(); var $cells = this._renderAggregateColCells();
if ($cells) { if ($cells) {
return $('<tfoot>').append($('<tr>').append('<td/>').append($cells));
return $('<tfoot>').append(
$('<tr>').append('<td/>').append($cells)
);
} }
return;
}, },
/** /**
* Render the Aggregate cells for the column. * Render the Aggregate cells for the column.
* *
@ -264,7 +318,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
*/ */
_renderAggregateColCells: function () { _renderAggregateColCells: function () {
var self = this; var self = this;
return _.map(this.columns, function (column, index) {
return _.map(this.columns, function (column) {
var $cell = $('<td>', {class: 'col-total text-right'}); var $cell = $('<td>', {class: 'col-total text-right'});
if (column.aggregate) { if (column.aggregate) {
self._apply_aggregate_value($cell, column.aggregate); self._apply_aggregate_value($cell, column.aggregate);
@ -272,6 +326,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
return $cell; return $cell;
}); });
}, },
/** /**
* Compute the column aggregates. * Compute the column aggregates.
* This function is called everytime the value is changed. * This function is called everytime the value is changed.
@ -282,30 +337,74 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
if (!this.matrix_data.show_column_totals) { if (!this.matrix_data.show_column_totals) {
return; return;
} }
var self = this,
fname = this.matrix_data.field_value,
var fname = this.matrix_data.field_value,
field = this.state.fields[fname]; field = this.state.fields[fname];
if (!field) { return; }
if (!field) {
return;
}
var type = field.type; var type = field.type;
if (type !== 'integer' && type !== 'float' && type !== 'monetary') {
if (!~['integer', 'float', 'monetary'].indexOf(type)) {
return; return;
} }
_.each(self.columns, function (column, index) {
_.each(this.columns, function (column, index) {
column.aggregate = { column.aggregate = {
fname: fname, fname: fname,
ftype: type, ftype: type,
// TODO: translate
help: 'Sum',
value: 0
help: _t('Sum'),
value: 0,
}; };
_.each(self.rows, function (row) {
// var record = _.findWhere(self.state.data, {id: col.data.id});
_.each(this.rows, function (row) {
// TODO Use only one _.propertyOf in underscore 1.9.0+
try {
column.aggregate.value += row.data[index].data[fname]; column.aggregate.value += row.data[index].data[fname];
} catch (error) {
// Nothing to do
}
}); });
});
}.bind(this));
}, },
/**
* @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. * Compute the row aggregates.
*
* This function is called everytime the value is changed. * This function is called everytime the value is changed.
* *
* @private * @private
@ -314,49 +413,72 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
if (!this.matrix_data.show_row_totals) { if (!this.matrix_data.show_row_totals) {
return; return;
} }
var self = this,
fname = this.matrix_data.field_value,
var fname = this.matrix_data.field_value,
field = this.state.fields[fname]; field = this.state.fields[fname];
if (!field) { return; }
if (!field) {
return;
}
var type = field.type; var type = field.type;
if (type !== 'integer' && type !== 'float' && type !== 'monetary') {
if (!~['integer', 'float', 'monetary'].indexOf(type)) {
return; return;
} }
_.each(self.rows, function (row) {
_.each(this.rows, function (row) {
row.aggregate = { row.aggregate = {
fname: fname, fname: fname,
ftype: type, ftype: type,
// TODO: translate
help: 'Sum',
value: 0
help: _t('Sum'),
value: 0,
}; };
_.each(row.data, function (col) { _.each(row.data, function (col) {
// TODO Use _.property in underscore 1.9+
try {
row.aggregate.value += col.data[fname]; row.aggregate.value += col.data[fname];
} catch (error) {
// Nothing to do
}
}); });
}); });
}, },
/** /**
* Takes the given Value, formats it and adds it to the given cell. * Takes the given Value, formats it and adds it to the given cell.
* *
* @private * @private
* @param {jQueryElement} $cell: The Cell where the aggregate should be added.
* @param {Object} aggregate: The object which contains the information about the aggregate value
*
* @param {jQueryElement} $cell
* The Cell where the aggregate should be added.
*
* @param {Object} aggregate
* The object which contains the information about the aggregate value
*/ */
_apply_aggregate_value: function ($cell, aggregate) { _apply_aggregate_value: function ($cell, aggregate) {
var field = this.state.fields[aggregate.fname], var field = this.state.fields[aggregate.fname],
formatter = field_utils.format[field.type]; formatter = field_utils.format[field.type];
var formattedValue = formatter(aggregate.value, field, {escape: true, });
$cell.addClass('total').attr('title', aggregate.help).html(formattedValue);
var formattedValue = formatter(
aggregate.value, field, {escape: true}
);
$cell.addClass('total').attr('title', aggregate.help)
.html(formattedValue);
}, },
/** /**
* Check if the change was successful and then update the grid. * Check if the change was successful and then update the grid.
* This function is required on relational fields. * This function is required on relational fields.
* *
* @params {Object} state: Contains the current state of the field & all the data
* @params {String} id: the id of the updated object.
* @params {Array} fields: The fields we have in the view.
* @params {Object} ev: The event object.
* @returns {Deferred} The deferred object thats gonna be resolved when the change is made.
* @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) { confirmUpdate: function (state, id, fields, ev) {
var self = this; var self = this;
@ -365,32 +487,36 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
self._refresh(id); self._refresh(id);
}); });
}, },
/** /**
* Refresh our grid. * Refresh our grid.
* *
* @private * @private
* @param {String} id Datapoint ID
*/ */
_refresh: function (id) { _refresh: function (id) {
this._updateRow(id); this._updateRow(id);
this._refreshColTotals(); this._refreshColTotals();
this._refreshRowTotals(); this._refreshRowTotals();
}, },
/** /**
*Update row data in our internal rows. *Update row data in our internal rows.
* *
* @params {String} id: The id of the row that needs to be updated.
* @param {String} id: The id of the row that needs to be updated.
*/ */
_updateRow: function (id) { _updateRow: function (id) {
var self = this,
record = _.findWhere(self.state.data, {id: id});
_.each(self.rows, function(row) {
_.each(row.data, function(col, i) {
if (col.id == 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; row.data[i] = record;
} }
}); });
}); });
}, },
/** /**
* Update the row total. * Update the row total.
*/ */
@ -398,6 +524,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
this._computeColumnAggregates(); this._computeColumnAggregates();
this.$('tfoot').replaceWith(this._renderFooter()); this.$('tfoot').replaceWith(this._renderFooter());
}, },
/** /**
* Update the column total. * Update the column total.
*/ */
@ -405,17 +532,22 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ
var self = this; var self = this;
this._computeRowAggregates(); this._computeRowAggregates();
var $rows = self.$el.find('tr.o_data_row'); var $rows = self.$el.find('tr.o_data_row');
_.each(self.rows, function(row, i) {
_.each(self.rows, function (row, i) {
if (row.aggregate) { if (row.aggregate) {
$($rows[i]).find('.row-total') $($rows[i]).find('.row-total')
.replaceWith(self._renderAggregateRowCell(row)); .replaceWith(self._renderAggregateRowCell(row));
} }
}); });
}, },
/*
x2m fields expect this
/**
* X2many fields expect this
*
* @returns {null}
*/ */
getEditableRecordID: function (){ return false;}
getEditableRecordID: function () {
return null;
},
}); });

196
web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js

@ -6,24 +6,22 @@
odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
"use strict"; "use strict";
var core = require('web.core');
// var FieldManagerMixin = require('web.FieldManagerMixin');
var field_registry = require('web.field_registry'); var field_registry = require('web.field_registry');
var relational_fields = require('web.relational_fields'); var relational_fields = require('web.relational_fields');
var weContext = require('web_editor.context');
// var Helpers = require('web_widget_x2many_2d_matrix.helpers');
var AbstractField = require('web.AbstractField');
var X2Many2dMatrixRenderer = require('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer');
var X2Many2dMatrixRenderer = require(
'web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer'
);
var WidgetX2Many2dMatrix = relational_fields.FieldOne2Many.extend({ var WidgetX2Many2dMatrix = relational_fields.FieldOne2Many.extend({
widget_class: 'o_form_field_x2many_2d_matrix', widget_class: 'o_form_field_x2many_2d_matrix',
/** /**
* Initialize the widget & parameters. * Initialize the widget & parameters.
* *
* @param {Object} parent: contains the form view.
* @param {String} name: the name of the field.
* @param {Object} record: Contains the information about the database records.
* @param {Object} options: Contains the view options.
* @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) { init: function (parent, name, record, options) {
this._super(parent, name, record, options); this._super(parent, name, record, options);
@ -40,114 +38,131 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
this.by_y_axis = {}; this.by_y_axis = {};
this.field_x_axis = node.field_x_axis || this.field_x_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_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_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; this.field_value = node.field_value || this.field_value;
// TODO: is this really needed? Holger? // TODO: is this really needed? Holger?
for (var property in node) { for (var property in node) {
if (property.startsWith("field_att_")) { if (property.startsWith("field_att_")) {
this.fields_att[property.substring(10)] = node[property];
this.fields_att[property.substring(10)] =
node[property];
} }
} }
// and this?
this.field_editability = node.field_editability || this.field_editability;
this.show_row_totals = this.parse_boolean(node.show_row_totals || '1');
this.show_column_totals = this.parse_boolean(node.show_column_totals || '1');
// And this?
this.field_editability =
node.field_editability || this.field_editability;
this.show_row_totals =
this.parse_boolean(node.show_row_totals || '1');
this.show_column_totals =
this.parse_boolean(node.show_column_totals || '1');
}, },
/** /**
* Initializes the Value matrix. * Initializes the Value matrix.
* Puts the values in the grid. If we have related items we use the display name.
*
* Puts the values in the grid.
* If we have related items we use the display name.
*/ */
init_matrix: function(){
var self = this,
records = self.recordData[this.name].data;
init_matrix: function () {
var records = this.recordData[this.name].data;
// Wipe the content if something still exists // Wipe the content if something still exists
this.by_x_axis = {}; this.by_x_axis = {};
this.by_y_axis = {}; this.by_y_axis = {};
_.each(records, function(record) {
var x = record.data[self.field_x_axis],
y = record.data[self.field_y_axis];
if (x.type == 'record') {
// we have a related record
_.each(records, function (record) {
var x = record.data[this.field_x_axis],
y = record.data[this.field_y_axis];
if (x.type === 'record') {
// We have a related record
x = x.data.display_name; x = x.data.display_name;
} }
if (y.type == 'record') {
// we have a related record
if (y.type === 'record') {
// We have a related record
y = y.data.display_name; y = y.data.display_name;
} }
self.by_x_axis[x] = self.by_x_axis[x] || {};
self.by_y_axis[y] = self.by_y_axis[y] || {};
self.by_x_axis[x][y] = record;
self.by_y_axis[y][x] = record;
});
// init columns
self.columns = [];
$.each(self.by_x_axis, function(x){
self.columns.push(self._make_column(x));
});
self.rows = [];
$.each(self.by_y_axis, function(y){
self.rows.push(self._make_row(y));
});
self.matrix_data = {
'field_value': self.field_value,
'field_x_axis': self.field_x_axis,
'field_y_axis': self.field_y_axis,
'columns': self.columns,
'rows': self.rows,
'show_row_totals': self.show_row_totals,
'show_column_totals': self.show_column_totals
this.by_x_axis[x] = this.by_x_axis[x] || {};
this.by_y_axis[y] = this.by_y_axis[y] || {};
this.by_x_axis[x][y] = record;
this.by_y_axis[y][x] = record;
}.bind(this));
// Init columns
this.columns = [];
$.each(this.by_x_axis, function (x) {
this.columns.push(this._make_column(x));
}.bind(this));
this.rows = [];
$.each(this.by_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,
'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. * Create scaffold for a column.
* *
* @params {String} x: The string used as a column title
* @param {String} x The string used as a column title
* @returns {Object}
*/ */
_make_column: function(x){
_make_column: function (x) {
return { return {
// simulate node parsed on xml arch
// Simulate node parsed on xml arch
'tag': 'field', 'tag': 'field',
'attrs': { 'attrs': {
'name': this.field_x_axis, 'name': this.field_x_axis,
'string': x
}
'string': x,
},
}; };
}, },
/** /**
* Create scaffold for a row. * Create scaffold for a row.
* *
* @params {String} x: The string used as a row title
* @param {String} y The string used as a row title
* @returns {Object}
*/ */
_make_row: function(y){
_make_row: function (y) {
var self = this; var self = this;
// use object so that we can attach more data if needed
// Use object so that we can attach more data if needed
var row = {'data': []}; var row = {'data': []};
$.each(self.by_x_axis, function(x) {
$.each(self.by_x_axis, function (x) {
row.data.push(self.by_y_axis[y][x]); row.data.push(self.by_y_axis[y][x]);
}); });
return row; return row;
}, },
/** /**
*Parse a String containing a Python bool or 1 and convert it to a proper bool.
* Parse a String containing a bool and convert it to a JS bool.
* *
* @params {String} val: the string to be parsed.
* @param {String} val: the string to be parsed.
* @returns {Boolean} The parsed boolean. * @returns {Boolean} The parsed boolean.
*/ */
parse_boolean: function(val) {
parse_boolean: function (val) {
if (val.toLowerCase() === 'true' || val === '1') { if (val.toLowerCase() === 'true' || val === '1') {
return true; return true;
} }
return false; return false;
}, },
/** /**
*Create the matrix renderer and add its output to our element
* Create the matrix renderer and add its output to our element
* *
* @returns {Deferred} A deferred object to be completed when it finished rendering.
* @returns {Deferred}
* A deferred object to be completed when it finished rendering.
*/ */
_render: function () { _render: function () {
if (!this.view) { if (!this.view) {
@ -155,26 +170,55 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
} }
// Ensure widget is re initiated when rendering // Ensure widget is re initiated when rendering
this.init_matrix(); this.init_matrix();
var arch = this.view.arch,
viewType = 'list';
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, { this.renderer = new X2Many2dMatrixRenderer(this, this.value, {
arch: arch, arch: arch,
editable: true,
viewType: viewType,
matrix_data: this.matrix_data
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'); this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix');
// Remove previous rendered and add the newly created one
this.$el.find('div:not(.o_x2m_control_panel)').remove();
return this.renderer.appendTo(this.$el); 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); field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix);
return { return {
WidgetX2Many2dMatrix: WidgetX2Many2dMatrix
WidgetX2Many2dMatrix: WidgetX2Many2dMatrix,
}; };
}); });
Loading…
Cancel
Save