[MIG+REF][11] web_widget_x2many_2d_matrix
The widget has been completely refactored to benefit from the new MVC paradigm introduced in v11.pull/871/head
-
1setup/web_widget_x2many_2d_matrix/odoo/addons/web_widget_x2many_2d_matrix
-
2setup/web_widget_x2many_2d_matrix/setup.cfg
-
6setup/web_widget_x2many_2d_matrix/setup.py
-
53web_widget_x2many_2d_matrix/README.rst
-
7web_widget_x2many_2d_matrix/__manifest__.py
-
BINweb_widget_x2many_2d_matrix/static/description/icon.png
-
BINweb_widget_x2many_2d_matrix/static/description/screenshot.png
-
9web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css
-
416web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js
-
433web_widget_x2many_2d_matrix/static/src/js/web_widget_x2many_2d_matrix.js
-
172web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js
-
36web_widget_x2many_2d_matrix/static/src/xml/web_widget_x2many_2d_matrix.xml
-
3web_widget_x2many_2d_matrix/views/assets.xml
@ -0,0 +1 @@ |
|||||
|
../../../../web_widget_x2many_2d_matrix |
@ -0,0 +1,2 @@ |
|||||
|
[bdist_wheel] |
||||
|
universal=1 |
@ -0,0 +1,6 @@ |
|||||
|
import setuptools |
||||
|
|
||||
|
setuptools.setup( |
||||
|
setup_requires=['setuptools-odoo'], |
||||
|
odoo_addon=True, |
||||
|
) |
Before Width: 80 | Height: 80 | Size: 5.0 KiB After Width: 90 | Height: 90 | Size: 2.4 KiB |
Before Width: 914 | Height: 349 | Size: 19 KiB After Width: 1240 | Height: 471 | Size: 22 KiB |
@ -1,8 +1,3 @@ |
|||||
.oe_form_field_x2many_2d_matrix th.oe_link |
|
||||
{ |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
.oe_form_field_x2many_2d_matrix .oe_list_content > tbody > tr > td.oe_list_field_cell |
|
||||
{ |
|
||||
white-space: normal; |
|
||||
|
.o_field_x2many_2d_matrix .row-total { |
||||
|
font-weight: bold; |
||||
} |
} |
@ -0,0 +1,416 @@ |
|||||
|
/* Copyright 2018 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
|
||||
|
|
||||
|
odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
// heavily inspired by Odoo's `ListRenderer`
|
||||
|
var BasicRenderer = require('web.BasicRenderer'); |
||||
|
var config = require('web.config'); |
||||
|
var field_utils = require('web.field_utils'); |
||||
|
var utils = require('web.utils'); |
||||
|
var FIELD_CLASSES = { |
||||
|
// copied from ListRenderer
|
||||
|
float: 'o_list_number', |
||||
|
integer: 'o_list_number', |
||||
|
monetary: 'o_list_number', |
||||
|
text: 'o_list_text', |
||||
|
}; |
||||
|
|
||||
|
var X2Many2dMatrixRenderer = BasicRenderer.extend({ |
||||
|
|
||||
|
init: function (parent, state, params) { |
||||
|
this._super.apply(this, arguments); |
||||
|
this.editable = params.editable; |
||||
|
this.columns = params.matrix_data.columns; |
||||
|
this.rows = params.matrix_data.rows; |
||||
|
this.matrix_data = params.matrix_data; |
||||
|
}, |
||||
|
/** |
||||
|
* 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(); |
||||
|
|
||||
|
var $table = $('<table>').addClass('o_list_view table table-condensed table-striped'); |
||||
|
this.$el |
||||
|
.addClass('table-responsive') |
||||
|
.append($table); |
||||
|
|
||||
|
this._computeColumnAggregates(); |
||||
|
this._computeRowAggregates(); |
||||
|
|
||||
|
$table |
||||
|
.append(this._renderHeader()) |
||||
|
.append(this._renderBody()); |
||||
|
if (self.matrix_data.show_column_totals) { |
||||
|
$table.append(this._renderFooter()); |
||||
|
} |
||||
|
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 |
||||
|
* return {jQueryElement} The table body element that was just filled. |
||||
|
*/ |
||||
|
_renderBody: function () { |
||||
|
var $body = $('<tbody>').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 |
||||
|
* @return {jQueryElement} The thead element that was inserted into. |
||||
|
*/ |
||||
|
_renderHeader: function () { |
||||
|
var $tr = $('<tr>').append('<th/>'); |
||||
|
$tr= $tr.append(_.map(this.columns, this._renderHeaderCell.bind(this))); |
||||
|
if (this.matrix_data.show_row_totals) { |
||||
|
$tr.append($('<th/>', {class: 'total'})); |
||||
|
} |
||||
|
return $('<thead>').append($tr); |
||||
|
}, |
||||
|
/** |
||||
|
* Render a single header cell. Creates a th and adds the description as text. |
||||
|
* |
||||
|
* @private |
||||
|
* @param {jQueryElement} node |
||||
|
* @returns {jQueryElement} the created <th> node. |
||||
|
*/ |
||||
|
_renderHeaderCell: function (node) { |
||||
|
var name = node.attrs.name; |
||||
|
var field = this.state.fields[name]; |
||||
|
var $th = $('<th>'); |
||||
|
if (!field) { |
||||
|
return $th; |
||||
|
} |
||||
|
var description; |
||||
|
if (node.attrs.widget) { |
||||
|
description = this.state.fieldsInfo.list[name].Widget.prototype.description; |
||||
|
} |
||||
|
if (description === undefined) { |
||||
|
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'); |
||||
|
} |
||||
|
|
||||
|
if (config.debug) { |
||||
|
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, 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. |
||||
|
* |
||||
|
* @private |
||||
|
* @param {Object} row: The row that will be rendered. |
||||
|
* @returns {jQueryElement} the <tr> element that has been rendered. |
||||
|
*/ |
||||
|
_renderRow: function (row) { |
||||
|
var self = this; |
||||
|
var $tr = $('<tr/>', {class: 'o_data_row'}); |
||||
|
$tr = $tr.append(self._renderLabelCell(row.data[0])); |
||||
|
var $cells = _.map(this.columns, function (node, 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:''}); |
||||
|
}); |
||||
|
$tr = $tr.append($cells); |
||||
|
if (row.aggregate) { |
||||
|
$tr.append(self._renderAggregateRowCell(row)); |
||||
|
} |
||||
|
return $tr; |
||||
|
}, |
||||
|
/** |
||||
|
* Renders the label for a specific row. |
||||
|
* |
||||
|
* @private |
||||
|
* @params {Object} record: Contains the information about the record. |
||||
|
* @params {jQueryElement} the cell that was rendered. |
||||
|
*/ |
||||
|
_renderLabelCell: function(record) { |
||||
|
var $td = $('<td>'); |
||||
|
var value = record.data[this.matrix_data.field_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 = $('<td/>', {class: 'row-total text-right'}); |
||||
|
this._apply_aggregate_value($cell, row.aggregate); |
||||
|
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 === 'button') { |
||||
|
tdClassName += ' o_list_button'; |
||||
|
} else 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 = $('<td>', { |
||||
|
'class': tdClassName, |
||||
|
'data-form-id': record.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.
|
||||
|
if (modifiers.invisible && !(options && options.renderInvisible)) { |
||||
|
return $td; |
||||
|
} |
||||
|
options.mode = 'edit'; // enforce edit mode
|
||||
|
var widget = this._renderFieldWidget(node, record, _.pick(options, 'mode')); |
||||
|
this._handleAttributes(widget.$el, node); |
||||
|
return $td.append(widget.$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) { |
||||
|
return $('<tfoot>').append($('<tr>').append('<td/>').append($cells)); |
||||
|
} |
||||
|
return; |
||||
|
}, |
||||
|
/** |
||||
|
* Render the Aggregate cells for the column. |
||||
|
* |
||||
|
* @private |
||||
|
* @returns {List} the rendered cells |
||||
|
*/ |
||||
|
_renderAggregateColCells: function () { |
||||
|
var self = this; |
||||
|
return _.map(this.columns, function (column, index) { |
||||
|
var $cell = $('<td>', {class: 'col-total text-right'}); |
||||
|
if (column.aggregate) { |
||||
|
self._apply_aggregate_value($cell, column.aggregate); |
||||
|
} |
||||
|
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 self = this, |
||||
|
fname = this.matrix_data.field_value, |
||||
|
field = this.state.fields[fname]; |
||||
|
if (!field) { return; } |
||||
|
var type = field.type; |
||||
|
if (type !== 'integer' && type !== 'float' && type !== 'monetary') { |
||||
|
return; |
||||
|
} |
||||
|
_.each(self.columns, function (column, index) { |
||||
|
column.aggregate = { |
||||
|
fname: fname, |
||||
|
ftype: type, |
||||
|
// TODO: translate
|
||||
|
help: 'Sum', |
||||
|
value: 0 |
||||
|
}; |
||||
|
_.each(self.rows, function (row) { |
||||
|
// var record = _.findWhere(self.state.data, {id: col.data.id});
|
||||
|
column.aggregate.value += row.data[index].data[fname]; |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
/** |
||||
|
* 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 self = this, |
||||
|
fname = this.matrix_data.field_value, |
||||
|
field = this.state.fields[fname]; |
||||
|
if (!field) { return; } |
||||
|
var type = field.type; |
||||
|
if (type !== 'integer' && type !== 'float' && type !== 'monetary') { |
||||
|
return; |
||||
|
} |
||||
|
_.each(self.rows, function (row) { |
||||
|
row.aggregate = { |
||||
|
fname: fname, |
||||
|
ftype: type, |
||||
|
// TODO: translate
|
||||
|
help: 'Sum', |
||||
|
value: 0 |
||||
|
}; |
||||
|
_.each(row.data, function (col) { |
||||
|
row.aggregate.value += col.data[fname]; |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
/** |
||||
|
* 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} aggregate: The object which contains the information about the aggregate value |
||||
|
*/ |
||||
|
_apply_aggregate_value: function ($cell, aggregate) { |
||||
|
var field = this.state.fields[aggregate.fname], |
||||
|
formatter = field_utils.format[field.type]; |
||||
|
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. |
||||
|
* 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. |
||||
|
*/ |
||||
|
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 |
||||
|
*/ |
||||
|
_refresh: function (id) { |
||||
|
this._updateRow(id); |
||||
|
this._refreshColTotals(); |
||||
|
this._refreshRowTotals(); |
||||
|
}, |
||||
|
/** |
||||
|
*Update row data in our internal rows. |
||||
|
* |
||||
|
* @params {String} id: The id of the row that needs to be updated. |
||||
|
*/ |
||||
|
_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) { |
||||
|
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)); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
/* |
||||
|
x2m fields expect this |
||||
|
*/ |
||||
|
getEditableRecordID: function (){ return false;} |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
return X2Many2dMatrixRenderer; |
||||
|
}); |
@ -1,433 +0,0 @@ |
|||||
/* Copyright 2015 Holger Brunn <hbrunn@therp.nl> |
|
||||
* Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
|
||||
* 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 core = require('web.core'); |
|
||||
var FieldManagerMixin = require('web.FieldManagerMixin'); |
|
||||
var Widget = require('web.Widget'); |
|
||||
var fieldRegistry = require('web.field_registry'); |
|
||||
var widgetRegistry = require('web.widget_registry'); |
|
||||
var widgetOne2many = widgetRegistry.get('one2many'); |
|
||||
var data = require('web.data'); |
|
||||
var $ = require('jquery'); |
|
||||
|
|
||||
var WidgetX2Many2dMatrix = widgetOne2Many.extend(FieldManagerMixin, { |
|
||||
template: 'FieldX2Many2dMatrix', |
|
||||
widget_class: 'oe_form_field_x2many_2d_matrix', |
|
||||
|
|
||||
// those will be filled with rows from the dataset
|
|
||||
by_x_axis: {}, |
|
||||
by_y_axis: {}, |
|
||||
by_id: {}, |
|
||||
// configuration values
|
|
||||
field_x_axis: 'x', |
|
||||
field_label_x_axis: 'x', |
|
||||
field_y_axis: 'y', |
|
||||
field_label_y_axis: 'y', |
|
||||
field_value: 'value', |
|
||||
x_axis_clickable: true, |
|
||||
y_axis_clickable: true, |
|
||||
// information about our datatype
|
|
||||
is_numeric: false, |
|
||||
show_row_totals: true, |
|
||||
show_column_totals: true, |
|
||||
// this will be filled with the model's fields_get
|
|
||||
fields: {}, |
|
||||
// Store fields used to fill HTML attributes
|
|
||||
fields_att: {}, |
|
||||
|
|
||||
parse_boolean: function(val) |
|
||||
{ |
|
||||
if (val.toLowerCase() === 'true' || val === '1') { |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
}, |
|
||||
|
|
||||
// read parameters
|
|
||||
init: function (parent, fieldname, record, therest) { |
|
||||
var res = this._super(parent, fieldname, record, therest); |
|
||||
FieldManagerMixin.init.call(this); |
|
||||
var node = record.fieldsInfo[therest.viewType][fieldname]; |
|
||||
|
|
||||
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; |
|
||||
for (var property in node) { |
|
||||
if (property.startsWith("field_att_")) { |
|
||||
this.fields_att[property.substring(10)] = node[property]; |
|
||||
} |
|
||||
} |
|
||||
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'); |
|
||||
this.init_fields(); |
|
||||
// this.set_value(undefined);
|
|
||||
|
|
||||
return res; |
|
||||
}, |
|
||||
|
|
||||
init_fields: function() { |
|
||||
return; |
|
||||
}, |
|
||||
|
|
||||
// return a field's value, id in case it's a one2many field
|
|
||||
get_field_value: function(row, field, many2one_as_name) |
|
||||
// FIXME looks silly
|
|
||||
{ |
|
||||
if(this.fields[field].type == 'many2one' && _.isArray(row[field])) |
|
||||
{ |
|
||||
if(many2one_as_name) |
|
||||
{ |
|
||||
return row[field][1]; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return row[field][0]; |
|
||||
} |
|
||||
} |
|
||||
return row[field]; |
|
||||
}, |
|
||||
|
|
||||
// setup our datastructure for simple access in the template
|
|
||||
set_value: function(value_) |
|
||||
{ |
|
||||
var self = this, |
|
||||
result = this._super(value_); |
|
||||
|
|
||||
self.by_x_axis = {}; |
|
||||
self.by_y_axis = {}; |
|
||||
self.by_id = {}; |
|
||||
|
|
||||
return $.when(result).then(function() |
|
||||
{ |
|
||||
return self.dataset._model.call('fields_get').then(function(fields) |
|
||||
{ |
|
||||
self.fields = fields; |
|
||||
self.is_numeric = fields[self.field_value].type == 'float'; |
|
||||
self.show_row_totals &= self.is_numeric; |
|
||||
self.show_column_totals &= self.is_numeric; |
|
||||
}) |
|
||||
// if there are cached writes on the parent dataset, read below
|
|
||||
// only returns the written data, which is not enough to properly
|
|
||||
// set up our data structure. Read those ids here and patch the
|
|
||||
// cache
|
|
||||
.then(function() |
|
||||
{ |
|
||||
var ids_written = _.map( |
|
||||
self.dataset.to_write, function(x) { return x.id }); |
|
||||
if(!ids_written.length) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
return (new data.Query(self.dataset._model)) |
|
||||
.filter([['id', 'in', ids_written]]) |
|
||||
.all() |
|
||||
.then(function(rows) |
|
||||
{ |
|
||||
_.each(rows, function(row) |
|
||||
{ |
|
||||
var cache = _.find( |
|
||||
self.dataset.cache, |
|
||||
function(x) { return x.id == row.id } |
|
||||
); |
|
||||
_.extend(cache.values, row, _.clone(cache.values)); |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
.then(function() |
|
||||
{ |
|
||||
return self.dataset.read_ids(self.dataset.ids, self.fields).then(function(rows) |
|
||||
{ |
|
||||
// setup data structure
|
|
||||
_.each(rows, function(row) |
|
||||
{ |
|
||||
self.add_xy_row(row); |
|
||||
}); |
|
||||
if(self.is_started && !self.no_rerender) |
|
||||
{ |
|
||||
self.renderElement(); |
|
||||
self.compute_totals(); |
|
||||
self.setup_many2one_axes(); |
|
||||
self.$el.find('.edit').on( |
|
||||
'change', self.proxy(self.xy_value_change)); |
|
||||
self.effective_readonly_change(); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
// do whatever needed to setup internal data structure
|
|
||||
add_xy_row: function(row) |
|
||||
{ |
|
||||
var x = this.get_field_value(row, this.field_x_axis), |
|
||||
y = this.get_field_value(row, this.field_y_axis); |
|
||||
// row is a *copy* of a row in dataset.cache, fetch
|
|
||||
// a reference to this row in order to have the
|
|
||||
// internal data structure point to the same data
|
|
||||
// the dataset manipulates
|
|
||||
_.every(this.dataset.cache, function(cached_row) |
|
||||
{ |
|
||||
if(cached_row.id == row.id) |
|
||||
{ |
|
||||
row = cached_row.values; |
|
||||
// new rows don't have that
|
|
||||
row.id = cached_row.id; |
|
||||
return false; |
|
||||
} |
|
||||
return true; |
|
||||
}); |
|
||||
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] = row; |
|
||||
this.by_y_axis[y][x] = row; |
|
||||
this.by_id[row.id] = row; |
|
||||
}, |
|
||||
|
|
||||
// get x axis values in the correct order
|
|
||||
get_x_axis_values: function() |
|
||||
{ |
|
||||
return _.keys(this.by_x_axis); |
|
||||
}, |
|
||||
|
|
||||
// get y axis values in the correct order
|
|
||||
get_y_axis_values: function() |
|
||||
{ |
|
||||
return _.keys(this.by_y_axis); |
|
||||
}, |
|
||||
|
|
||||
// get the label for a value on the x axis
|
|
||||
get_x_axis_label: function(x) |
|
||||
{ |
|
||||
return this.get_field_value( |
|
||||
_.first(_.values(this.by_x_axis[x])), |
|
||||
this.field_label_x_axis, true); |
|
||||
}, |
|
||||
|
|
||||
// get the label for a value on the y axis
|
|
||||
get_y_axis_label: function(y) |
|
||||
{ |
|
||||
return this.get_field_value( |
|
||||
_.first(_.values(this.by_y_axis[y])), |
|
||||
this.field_label_y_axis, true); |
|
||||
}, |
|
||||
|
|
||||
// return the class(es) the inputs should have
|
|
||||
get_xy_value_class: function() |
|
||||
{ |
|
||||
var classes = 'oe_form_field oe_form_required'; |
|
||||
if(this.is_numeric) |
|
||||
{ |
|
||||
classes += ' oe_form_field_float'; |
|
||||
} |
|
||||
return classes; |
|
||||
}, |
|
||||
|
|
||||
// return row id of a coordinate
|
|
||||
get_xy_id: function(x, y) |
|
||||
{ |
|
||||
return this.by_x_axis[x][y]['id']; |
|
||||
}, |
|
||||
|
|
||||
get_xy_att: function(x, y) |
|
||||
{ |
|
||||
var vals = {}; |
|
||||
for (var att in this.fields_att) { |
|
||||
var val = this.get_field_value( |
|
||||
this.by_x_axis[x][y], this.fields_att[att]); |
|
||||
// Discard empty values
|
|
||||
if (val) { |
|
||||
vals[att] = val; |
|
||||
} |
|
||||
} |
|
||||
return vals; |
|
||||
}, |
|
||||
|
|
||||
// return the value of a coordinate
|
|
||||
get_xy_value: function(x, y) |
|
||||
{ |
|
||||
return this.get_field_value( |
|
||||
this.by_x_axis[x][y], this.field_value); |
|
||||
}, |
|
||||
|
|
||||
// validate a value
|
|
||||
validate_xy_value: function(val) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
this.parse_xy_value(val); |
|
||||
} |
|
||||
catch(e) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
return true; |
|
||||
}, |
|
||||
|
|
||||
// parse a value from user input
|
|
||||
parse_xy_value: function(val) |
|
||||
{ |
|
||||
return val; |
|
||||
}, |
|
||||
|
|
||||
// format a value from the database for display
|
|
||||
format_xy_value: function(val) |
|
||||
{ |
|
||||
return val; |
|
||||
}, |
|
||||
|
|
||||
// compute totals
|
|
||||
compute_totals: function() |
|
||||
{ |
|
||||
var self = this, |
|
||||
grand_total = 0, |
|
||||
totals_x = {}, |
|
||||
totals_y = {}, |
|
||||
rows = this.by_id, |
|
||||
deferred = $.Deferred(); |
|
||||
_.each(rows, function(row) |
|
||||
{ |
|
||||
var key_x = self.get_field_value(row, self.field_x_axis), |
|
||||
key_y = self.get_field_value(row, self.field_y_axis); |
|
||||
totals_x[key_x] = (totals_x[key_x] || 0) + self.get_field_value(row, self.field_value); |
|
||||
totals_y[key_y] = (totals_y[key_y] || 0) + self.get_field_value(row, self.field_value); |
|
||||
grand_total += self.get_field_value(row, self.field_value); |
|
||||
}); |
|
||||
_.each(totals_y, function(total, y) |
|
||||
{ |
|
||||
self.$el.find( |
|
||||
_.str.sprintf('td.row_total[data-y="%s"]', y)).text( |
|
||||
self.format_xy_value(total)); |
|
||||
}); |
|
||||
_.each(totals_x, function(total, x) |
|
||||
{ |
|
||||
self.$el.find( |
|
||||
_.str.sprintf('td.column_total[data-x="%s"]', x)).text( |
|
||||
self.format_xy_value(total)); |
|
||||
}); |
|
||||
self.$el.find('.grand_total').text( |
|
||||
self.format_xy_value(grand_total)) |
|
||||
deferred.resolve({ |
|
||||
totals_x: totals_x, |
|
||||
totals_y: totals_y, |
|
||||
grand_total: grand_total, |
|
||||
rows: rows, |
|
||||
}); |
|
||||
return deferred; |
|
||||
}, |
|
||||
|
|
||||
setup_many2one_axes: function() |
|
||||
{ |
|
||||
if(this.fields[this.field_x_axis].type == 'many2one' && this.x_axis_clickable) |
|
||||
{ |
|
||||
this.$el.find('th[data-x]').addClass('oe_link') |
|
||||
.click(_.partial( |
|
||||
this.proxy(this.many2one_axis_click), |
|
||||
this.field_x_axis, 'x')); |
|
||||
} |
|
||||
if(this.fields[this.field_y_axis].type == 'many2one' && this.y_axis_clickable) |
|
||||
{ |
|
||||
this.$el.find('tr[data-y] th').addClass('oe_link') |
|
||||
.click(_.partial( |
|
||||
this.proxy(this.many2one_axis_click), |
|
||||
this.field_y_axis, 'y')); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
many2one_axis_click: function(field, id_attribute, e) |
|
||||
{ |
|
||||
this.do_action({ |
|
||||
type: 'ir.actions.act_window', |
|
||||
name: this.fields[field].string, |
|
||||
res_model: this.fields[field].relation, |
|
||||
res_id: $(e.currentTarget).data(id_attribute), |
|
||||
views: [[false, 'form']], |
|
||||
target: 'current', |
|
||||
}) |
|
||||
}, |
|
||||
|
|
||||
start: function() |
|
||||
{ |
|
||||
var self = this; |
|
||||
this.$el.find('.edit').on( |
|
||||
'change', self.proxy(this.xy_value_change)); |
|
||||
this.compute_totals(); |
|
||||
this.setup_many2one_axes(); |
|
||||
this.on("change:effective_readonly", |
|
||||
this, this.proxy(this.effective_readonly_change)); |
|
||||
this.effective_readonly_change(); |
|
||||
return this._super(); |
|
||||
}, |
|
||||
|
|
||||
xy_value_change: function(e) |
|
||||
{ |
|
||||
var $this = $(e.currentTarget), |
|
||||
val = $this.val(); |
|
||||
if(this.validate_xy_value(val)) |
|
||||
{ |
|
||||
var data = {}, value = this.parse_xy_value(val); |
|
||||
data[this.field_value] = value; |
|
||||
|
|
||||
$this.siblings('.read').text(this.format_xy_value(value)); |
|
||||
$this.val(this.format_xy_value(value)); |
|
||||
|
|
||||
this.dataset.write($this.data('id'), data); |
|
||||
this.by_id[$this.data('id')][this.field_value] = value; |
|
||||
$this.parent().removeClass('oe_form_invalid'); |
|
||||
this.compute_totals(); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
$this.parent().addClass('oe_form_invalid'); |
|
||||
} |
|
||||
|
|
||||
}, |
|
||||
|
|
||||
effective_readonly_change: function() |
|
||||
{ |
|
||||
this.$el |
|
||||
.find('tbody .edit') |
|
||||
.toggle(!this.get('effective_readonly')); |
|
||||
this.$el |
|
||||
.find('tbody .read') |
|
||||
.toggle(this.get('effective_readonly')); |
|
||||
this.$el.find('.edit').first().focus(); |
|
||||
}, |
|
||||
|
|
||||
is_syntax_valid: function() |
|
||||
{ |
|
||||
return this.$el.find('.oe_form_invalid').length == 0; |
|
||||
}, |
|
||||
|
|
||||
load_views: function() { |
|
||||
// Needed for removing the initial empty tree view when the widget
|
|
||||
// is loaded
|
|
||||
var self = this, |
|
||||
result = this._super(); |
|
||||
|
|
||||
return $.when(result).then(function() |
|
||||
{ |
|
||||
self.renderElement(); |
|
||||
self.compute_totals(); |
|
||||
self.$el.find('.edit').on( |
|
||||
'change', self.proxy(self.xy_value_change)); |
|
||||
}); |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
fieldRegistry.add( |
|
||||
'x2many_2d_matrix', WidgetX2Many2dMatrix |
|
||||
); |
|
||||
|
|
||||
return { |
|
||||
WidgetX2Many2dMatrix: WidgetX2Many2dMatrix |
|
||||
}; |
|
||||
}); |
|
@ -0,0 +1,172 @@ |
|||||
|
/* Copyright 2015 Holger Brunn <hbrunn@therp.nl> |
||||
|
* Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
* Copyright 2018 Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
* 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 core = require('web.core'); |
||||
|
// var FieldManagerMixin = require('web.FieldManagerMixin');
|
||||
|
var field_registry = require('web.field_registry'); |
||||
|
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 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: Contains the information about the database records. |
||||
|
* @param {Object} options: Contains the 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_x_axis = {}; |
||||
|
this.by_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]; |
||||
|
} |
||||
|
} |
||||
|
// 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'); |
||||
|
this.init_matrix(); |
||||
|
}, |
||||
|
/** |
||||
|
* Initializes the Value matrix. |
||||
|
* 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; |
||||
|
_.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
|
||||
|
x = x.data.display_name; |
||||
|
} |
||||
|
if (y.type == 'record') { |
||||
|
// we have a related record
|
||||
|
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 |
||||
|
}; |
||||
|
|
||||
|
}, |
||||
|
/** |
||||
|
* Create scaffold for a column. |
||||
|
* |
||||
|
* @params {String} x: The string used as a column title |
||||
|
*/ |
||||
|
_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. |
||||
|
* |
||||
|
* @params {String} x: The string used as a row title |
||||
|
*/ |
||||
|
_make_row: function(y){ |
||||
|
var self = this; |
||||
|
// use object so that we can attach more data if needed
|
||||
|
var row = {'data': []}; |
||||
|
$.each(self.by_x_axis, function(x) { |
||||
|
row.data.push(self.by_y_axis[y][x]); |
||||
|
}); |
||||
|
return row; |
||||
|
}, |
||||
|
/** |
||||
|
*Parse a String containing a Python bool or 1 and convert it to a proper bool. |
||||
|
* |
||||
|
* @params {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(); |
||||
|
} |
||||
|
var arch = this.view.arch, |
||||
|
viewType = 'list'; |
||||
|
this.renderer = new X2Many2dMatrixRenderer(this, this.value, { |
||||
|
arch: arch, |
||||
|
editable: true, |
||||
|
viewType: viewType, |
||||
|
matrix_data: this.matrix_data |
||||
|
}); |
||||
|
this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix'); |
||||
|
return this.renderer.appendTo(this.$el); |
||||
|
} |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix); |
||||
|
|
||||
|
return { |
||||
|
WidgetX2Many2dMatrix: WidgetX2Many2dMatrix |
||||
|
}; |
||||
|
}); |
@ -1,36 +0,0 @@ |
|||||
<templates> |
|
||||
<t t-name="FieldX2Many2dMatrix"> |
|
||||
<div t-att-class="widget.widget_class"> |
|
||||
<table class="o_list_view table table-condensed table-striped"> |
|
||||
<thead> |
|
||||
<tr class="oe_list_header_columns"> |
|
||||
<th /> |
|
||||
<th t-foreach="widget.get_x_axis_values()" t-as="x" t-att-data-x="x"> |
|
||||
<t t-esc="widget.get_x_axis_label(x)" /> |
|
||||
</th> |
|
||||
<th t-if="widget.show_row_totals">Total</th> |
|
||||
</tr> |
|
||||
</thead> |
|
||||
<tbody> |
|
||||
<tr t-foreach="widget.get_y_axis_values()" t-as="y" t-att-data-y="y"> |
|
||||
<th><t t-esc="widget.get_y_axis_label(y)" /></th> |
|
||||
<td t-foreach="widget.get_x_axis_values()" t-as="x" t-att-class="'' + (widget.is_numeric ? ' oe_number' : '')" t-att-data-x="x"> |
|
||||
<span t-att-class="widget.get_xy_value_class()"> |
|
||||
<input class="edit o_form_input oe_edit_only" t-att-data-id="widget.get_xy_id(x, y)" t-att-value="widget.format_xy_value(widget.get_xy_value(x, y))" t-att="widget.get_xy_att(x, y)"/> |
|
||||
<span class="read oe_read_only"><t t-esc="widget.format_xy_value(widget.get_xy_value(x, y))" /></span> |
|
||||
</span> |
|
||||
</td> |
|
||||
<td t-if="widget.show_row_totals" class="row_total oe_number" t-att-data-y="y"/> |
|
||||
</tr> |
|
||||
</tbody> |
|
||||
<tfoot t-if="widget.show_column_totals"> |
|
||||
<tr> |
|
||||
<th>Total</th> |
|
||||
<td t-foreach="widget.get_x_axis_values()" t-as="x" class="oe_list_footer oe_number column_total" t-att-data-x="x" /> |
|
||||
<td class="grand_total oe_number" /> |
|
||||
</tr> |
|
||||
</tfoot> |
|
||||
</table> |
|
||||
</div> |
|
||||
</t> |
|
||||
</templates> |
|