summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/services/config.js2
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js614
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js63
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js463
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js263
-rw-r--r--addons/web/static/src/scss/pivot_view.scss5
-rw-r--r--addons/web/static/src/xml/base.xml26
-rw-r--r--addons/web/static/src/xml/pivot.xml99
8 files changed, 746 insertions, 789 deletions
diff --git a/addons/web/static/src/js/services/config.js b/addons/web/static/src/js/services/config.js
index 91c3f1c764c..7ded43e291c 100644
--- a/addons/web/static/src/js/services/config.js
+++ b/addons/web/static/src/js/services/config.js
@@ -6,7 +6,7 @@ odoo.define('web.config', function () {
* This is often necessary to allow the rest of the web client to properly
* render itself.
*
- * Note that many informations currently stored in session should be moved to
+ * Note that many information currently stored in session should be moved to
* this file someday.
*/
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index 71427baff1b..606367d4d3a 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -1,355 +1,311 @@
odoo.define('web.PivotController', function (require) {
-"use strict";
-/**
- * Odoo Pivot Table Controller
- *
- * This class is the Controller for the pivot table view. It has to coordinate
- * the actions coming from the search view (through the update method), from
- * the renderer, from the model, and from the control panel.
- *
- * It can display action buttons in the control panel, to select a different
- * measure, or to perform some other actions such as download/expand/flip the
- * view.
- */
-
-var AbstractController = require('web.AbstractController');
-var core = require('web.core');
-var framework = require('web.framework');
-var session = require('web.session');
-
-var _t = core._t;
-var QWeb = core.qweb;
-
-var PivotController = AbstractController.extend({
- contentTemplate: 'PivotView',
- events: {
- 'click .o_pivot_field_menu a': '_onGroupByMenuSelection',
- 'click .o_pivot_header_cell_closed': '_onClosedHeaderClick',
- },
- custom_events: _.extend({}, AbstractController.prototype.custom_events, {
- close_group: '_onCloseGroup',
- open_view: '_onOpenView',
- sort_rows: '_onSortRows',
- }),
+ "use strict";
/**
- * @override
- * @param {Object} params
- * @param {Object} params.groupableFields a map from field names to field
- * props
+ * Odoo Pivot Table Controller
+ *
+ * This class is the Controller for the pivot table view. It has to coordinate
+ * the actions coming from the search view (through the update method), from
+ * the renderer, from the model, and from the control panel.
+ *
+ * It can display action buttons in the control panel, to select a different
+ * measure, or to perform some other actions such as download/expand/flip the
+ * view.
*/
- init: function (parent, model, renderer, params) {
- this._super.apply(this, arguments);
- this.measures = params.measures;
- this.groupableFields = params.groupableFields;
- this.title = params.title;
- // views to use in the action triggered when a data cell is clicked
- this.views = params.views;
- this.groupSelected = null;
- },
- /**
- * @override
- */
- start: function () {
- this.$groupBySelection = this.$('.o_field_selection');
- core.bus.on('click', this, function () {
- this.$groupBySelection.empty();
- });
- return this._super();
- },
- /**
- * @override
- */
- destroy: function () {
- if (this.$buttons) {
- // remove jquery's tooltip() handlers
- this.$buttons.find('button').off();
- }
- return this._super.apply(this, arguments);
- },
+ const ControllerAdapter = require('web.ControllerAdapter');
+ const core = require('web.core');
+ const framework = require('web.framework');
+ const session = require('web.session');
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
+ const _t = core._t;
+ const QWeb = core.qweb;
- /**
- * Returns the current measures and groupbys, so we can restore the view
- * when we save the current state in the search view, or when we add it to
- * the dashboard.
- *
- * @override method from AbstractController
- * @returns {Object}
- */
- getOwnedQueryParams: function () {
- var state = this.model.get({raw: true});
- return {
- context: {
- pivot_measures: state.measures,
- pivot_column_groupby: state.colGroupBys,
- pivot_row_groupby: state.rowGroupBys,
+ const PivotController = ControllerAdapter.extend({
+ events: Object.assign({}, ControllerAdapter.prototype.events, {
+ closed_header_click: '_onClosedHeaderClicked',
+ open_view: '_onOpenView',
+ opened_header_click: '_onOpenedHeaderClicked',
+ sort_rows: '_onSortRows',
+ groupby_menu_selection: '_onGroupByMenuSelection',
+ }),
+ /**
+ * @override
+ * @param parent
+ * @param model
+ * @param renderer
+ * @param {Object} params
+ * @param {Object} params.groupableFields a map from field names to field
+ * props
+ */
+ init: function (parent, model, renderer, params) {
+ this._super(...arguments);
+
+ this.measures = params.measures;
+ this.title = params.title;
+ // views to use in the action triggered when a data cell is clicked
+ this.views = params.views;
+ this.groupSelected = null;
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ if (this.$buttons) {
+ // remove jquery's tooltip() handlers
+ this.$buttons.find('button').off();
}
- };
- },
- /**
- * Render the buttons according to the PivotView.buttons template and
- * add listeners on it.
- * Set this.$buttons with the produced jQuery element
- *
- * @param {jQuery} [$node] a jQuery node where the rendered buttons should
- * be inserted. $node may be undefined, in which case the PivotView
- * does nothing
- */
- renderButtons: function ($node) {
- if ($node) {
- var context = {
- measures: _.sortBy(_.pairs(_.omit(this.measures, '__count')), function (x) {
- return x[1].string.toLowerCase();
- }),
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the current measures and groupbys, so we can restore the view
+ * when we save the current state in the search view, or when we add it to
+ * the dashboard.
+ *
+ * @override method from AbstractController
+ * @returns {Object}
+ */
+ getOwnedQueryParams: function () {
+ const state = this.model.get({ raw: true });
+ return {
+ context: {
+ pivot_measures: state.measures,
+ pivot_column_groupby: state.colGroupBys,
+ pivot_row_groupby: state.rowGroupBys,
+ }
};
- this.$buttons = $(QWeb.render('PivotView.buttons', context));
- this.$buttons.click(this._onButtonClick.bind(this));
- this.$buttons.find('button').tooltip();
+ },
+ /**
+ * Render the buttons according to the PivotView.buttons template and
+ * add listeners on it.
+ * Set this.$buttons with the produced jQuery element
+ *
+ * @param {jQuery} [$node] a jQuery node where the rendered buttons should
+ * be inserted. $node may be undefined, in which case the PivotView
+ * does nothing
+ */
+ renderButtons: function ($node) {
+ if ($node) {
+ const context = {
+ measures: Object.entries(this.measures)
+ .filter(x => x[0] !== '__count')
+ .sort((a, b) => a[1].string.toLowerCase() > b[1].string.toLowerCase() ? 1 : -1),
+ };
+ this.$buttons = $(QWeb.render('PivotView.buttons', context));
+ this.$buttons.click(this._onButtonClick.bind(this));
+ this.$buttons.find('button').tooltip();
- this.$buttons.appendTo($node);
- this._updateButtons();
- }
- },
+ this.$buttons.appendTo($node);
+ this._updateButtons();
+ }
+ },
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- /**
- * Export the current pivot table data in a xls file. For this, we have to
- * serialize the current state, then call the server /web/pivot/export_xls.
- * Force a reload before exporting to ensure to export up-to-date data.
- *
- * @private
- */
- _downloadTable: function () {
- var self = this;
- if (self.model.getTableWidth() > 256) {
- this.call('crash_manager', 'show_message', _t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."));
- framework.unblockUI();
- return;
- }
- var table = self.model.exportData();
- table.title = self.title;
- session.get_file({
- url: '/web/pivot/export_xls',
- data: {data: JSON.stringify(table)},
- complete: framework.unblockUI,
- error: (error) => this.call('crash_manager', 'rpc_error', error),
- });
- },
- /**
- * Render the field selection menu, to select a groupable field. We disable
- * already selected groupbys.
- *
- * @private
- * @param {number} top top coordinate where we have to render the menu
- * @param {number} left left coordinate for the menu
- */
- _renderGroupBySelection: function (top, left) {
- var state = this.model.get({raw: true});
- var groupedFields = state.rowGroupBys
- .concat(state.colGroupBys)
- .map(function (f) {
- return f.split(':')[0];
+ /**
+ * Export the current pivot table data in a xls file. For this, we have to
+ * serialize the current state, then call the server /web/pivot/export_xls.
+ * Force a reload before exporting to ensure to export up-to-date data.
+ *
+ * @private
+ */
+ _downloadTable: function () {
+ if (this.model.getTableWidth() > 256) {
+ this.call('crash_manager', 'show_message', _t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."));
+ framework.unblockUI();
+ return;
+ }
+ const table = this.model.exportData();
+ table.title = this.title;
+ session.get_file({
+ url: '/web/pivot/export_xls',
+ data: { data: JSON.stringify(table) },
+ complete: framework.unblockUI,
+ error: (error) => this.call('crash_manager', 'rpc_error', error),
});
+ },
+ /**
+ * @override
+ * @private
+ */
+ _update: function () {
+ this._updateButtons();
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @private
+ */
+ _updateButtons: function () {
+ if (!this.$buttons) {
+ return;
+ }
+ const state = this.model.get({ raw: true });
+ Object.entries(this.measures).forEach(elt => {
+ const name = elt[0];
+ const isSelected = state.measures.includes(name);
+ this.$buttons.find('.dropdown-item[data-field="' + name + '"]')
+ .toggleClass('selected', isSelected);
- var fields = _.chain(this.groupableFields)
- .pairs()
- .sortBy(function (f) {
- return f[1].string;
- })
- .map(function (f) {
- return [f[0], f[1], _.contains(groupedFields, f[0])];
- })
- .value();
+ });
+ const noDataDisplayed = !state.hasData || !state.measures.length;
+ this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed);
+ this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed);
+ this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed);
+ },
- this.$groupBySelection.html(QWeb.render('PivotView.GroupBySelection', {
- fields: fields
- }));
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
- var cssProps = {top: top};
- var isRTL = _t.database.parameters.direction === 'rtl';
- cssProps[isRTL ? 'right' : 'left'] = isRTL ? this.$el.width() - left : left;
- this.$groupBySelection.find('.dropdown-menu').first().css(cssProps).addClass('show');
- },
- /**
- * @override
- * @private
- */
- _startRenderer: function () {
- return this.renderer.appendTo(this.$('.o_pivot'));
- },
- /**
- * @override
- * @private
- */
- _update: function () {
- this._updateButtons();
- return this._super.apply(this, arguments);
- },
- /**
- * @private
- */
- _updateButtons: function () {
- if (!this.$buttons) {
- return;
- }
- var self = this;
- var state = this.model.get({raw: true});
- _.each(this.measures, function (measure, name) {
- var isSelected = _.contains(state.measures, name);
- self.$buttons.find('.dropdown-item[data-field="' + name + '"]')
- .toggleClass('selected', isSelected);
- });
- var noDataDisplayed = !state.hasData || !state.measures.length;
- this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed);
- this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed);
- this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed);
- },
+ /**
+ * This handler is called when the user clicked on a button in the control
+ * panel. We then have to react properly: it can either be a change in the
+ * current measures, or a request to flip/expand/download data.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onButtonClick: async function (ev) {
+ const $target = $(ev.target);
+ if ($target.hasClass('o_pivot_flip_button')) {
+ this.model.flip();
+ this.update({}, { reload: false });
+ }
+ if ($target.hasClass('o_pivot_expand_button')) {
+ await this.model.expandAll();
+ this.update({}, { reload: false });
+ }
+ if ($target.parents('.o_pivot_measures_list').length) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const field = $target.data('field');
+ await this.model.toggleMeasure(field);
+ this.update({}, { reload: false });
+ }
+ if ($target.hasClass('o_pivot_download')) {
+ this._downloadTable();
+ }
+ },
+ /**
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onCloseGroup: function (ev) {
+ this.model.closeGroup(ev.data.groupId, ev.data.type);
+ this.update({}, { reload: false });
+ },
+ /**
+ * @param {CustomEvent} ev
+ * @private
+ * */
+ _onOpenedHeaderClicked: function (ev) {
+ this.model.closeGroup(ev.detail.cell.groupId, ev.detail.type);
+ this.update({}, { reload: false });
+ },
+ /**
+ * @param {CustomEvent} ev
+ * @private
+ * */
+ _onClosedHeaderClicked: async function (ev) {
+ const cell = ev.detail.cell;
+ const groupId = cell.groupId;
+ const type = ev.detail.type;
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
+ const group = {
+ rowValues: groupId[0],
+ colValues: groupId[1],
+ type: type
+ };
- /**
- * This handler is called when the user clicked on a button in the control
- * panel. We then have to react properly: it can either be a change in the
- * current measures, or a request to flip/expand/download data.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onButtonClick: function (ev) {
- var $target = $(ev.target);
- if ($target.hasClass('o_pivot_flip_button')) {
- this.model.flip();
- this.update({}, {reload: false});
- }
- if ($target.hasClass('o_pivot_expand_button')) {
- this.model.expandAll().then(this.update.bind(this, {}, {reload: false}));
- }
- if ($target.parents('.o_pivot_measures_list').length) {
- ev.preventDefault();
+ const state = this.model.get({ raw: true });
+ const groupValues = type === 'row' ? groupId[0] : groupId[1];
+ const groupBys = type === 'row' ?
+ state.rowGroupBys :
+ state.colGroupBys;
+ this.selectedGroup = group;
+ if (groupValues.length < groupBys.length) {
+ const groupBy = groupBys[groupValues.length];
+ await this.model.expandGroup(this.selectedGroup, groupBy);
+ this.update({}, { reload: false });
+ }
+ },
+ /**
+ * This handler is called when the user selects a groupby in the dropdown menu.
+ *
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onGroupByMenuSelection: async function (ev) {
ev.stopPropagation();
- var field = $target.data('field');
- this.model.toggleMeasure(field).then(this.update.bind(this, {}, {reload: false}));
- }
- if ($target.hasClass('o_pivot_download')) {
- this._downloadTable();
- }
- },
- /**
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onCloseGroup: function (ev) {
- this.model.closeGroup(ev.data.groupId, ev.data.type);
- this.update({}, {reload: false});
- },
- /**
- * When we click on a closed row (col) header, we either want to open the
- * dropdown menu to select a new field to add to rowGroupBys (resp. colGroupBys),
- * or we want to open the clicked header, if rowGroupBys (resp. colGroupBys)
- * has length strictly greater than header
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onClosedHeaderClick: function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
-
- var $target = $(ev.target);
- var groupId = $target.data('groupId');
- var type = $target.data('type');
- var group = {
- rowValues: groupId[0],
- colValues: groupId[1],
- type: type
- };
-
- var state = this.model.get({raw: true});
+ let groupBy = ev.detail.field.name;
+ const interval = ev.detail.interval;
+ if (interval) {
+ groupBy = groupBy + ':' + interval;
+ }
+ this.model.addGroupBy(groupBy, this.selectedGroup.type);
+ await this.model.expandGroup(this.selectedGroup, groupBy);
+ this.update({}, { reload: false });
+ },
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onOpenView: function (ev) {
+ ev.stopPropagation();
+ const cell = ev.detail;
+ if (cell.value === undefined || this.initialState.disableLinking) {
+ return;
+ }
- var groupValues = type === 'row' ? groupId[0] : groupId[1];
- var groupBys = type === 'row' ?
- state.rowGroupBys :
- state.colGroupBys;
+ const context = Object.assign({}, this.model.data.context);
+ Object.keys(context).forEach(x => {
+ if (x === 'group_by' || x.startsWith('search_default_')) {
+ delete context[x];
+ }
+ });
- this.selectedGroup = group;
- if (groupValues.length < groupBys.length) {
- var groupBy = groupBys[groupValues.length];
- this.model
- .expandGroup(this.selectedGroup, groupBy)
- .then(this.update.bind(this, {}, {reload: false}));
- } else {
- var position = $target.position();
- var top = position.top + $target.height();
- var left = ev.clientX;
- this._renderGroupBySelection(top, left);
- }
- },
- /**
- * This handler is called when the user selects a groupby in the dropdown menu.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onGroupByMenuSelection: function (ev) {
- ev.preventDefault();
- var $target = $(ev.target);
- if ($target.hasClass('disabled')) {
- ev.stopPropagation();
- return;
- }
+ const group = {
+ rowValues: cell.groupId[0],
+ colValues: cell.groupId[1],
+ originIndex: cell.originIndexes[0]
+ };
- var groupBy = $target.data('field');
- var interval = $target.data('interval');
- if (interval) {
- groupBy = groupBy + ':' + interval;
- }
- this.model.addGroupBy(groupBy, this.selectedGroup.type);
- this.model
- .expandGroup(this.selectedGroup, groupBy)
- .then(this.update.bind(this, {}, {reload: false}));
- },
- /**
- * @private
- * @param {OdooEvent} ev
- */
- _onOpenView: function (ev) {
- ev.stopPropagation();
- var context = ev.data.context;
- var group = ev.data.group;
- var domain = this.model._getGroupDomain(group);
+ const domain = this.model._getGroupDomain(group);
- this.do_action({
- type: 'ir.actions.act_window',
- name: this.title,
- res_model: this.modelName,
- views: this.views,
- view_mode: 'list',
- target: 'current',
- context: context,
- domain: domain,
- });
- },
- /**
- * @private
- * @param {OdooEvent} ev
- */
- _onSortRows: function (ev) {
- this.model.sortRows(ev.data.sortedColumn);
- this.update({}, {reload: false});
- },
-});
+ this.do_action({
+ type: 'ir.actions.act_window',
+ name: this.title,
+ res_model: this.modelName,
+ views: this.views,
+ view_mode: 'list',
+ target: 'current',
+ context: context,
+ domain: domain,
+ });
+ },
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onSortRows: function (ev) {
+ this.model.sortRows({
+ groupId: ev.detail.groupId,
+ measure: ev.detail.measure,
+ order: (ev.detail.order || 'desc') === 'asc' ? 'desc' : 'asc',
+ originIndexes: ev.detail.originIndexes,
+ });
+ this.update({}, { reload: false });
+ },
+ });
-return PivotController;
+ return PivotController;
});
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index cb09f4a40f9..4669c5af7ec 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -361,7 +361,7 @@ var PivotModel = AbstractModel.extend({
* @param {string} groupBy
* @returns {Promise}
*/
- expandGroup: function (group, groupBy) {
+ expandGroup: async function (group, groupBy) {
var leftDivisors;
var rightDivisors;
@@ -475,6 +475,7 @@ var PivotModel = AbstractModel.extend({
function twistKey(key) {
return JSON.stringify(JSON.parse(key).reverse());
}
+
function twist(object) {
var newObject = {};
Object.keys(object).forEach(function (key) {
@@ -483,6 +484,7 @@ var PivotModel = AbstractModel.extend({
});
return newObject;
}
+
this.measurements = twist(this.measurements);
this.counts = twist(this.counts);
this.groupDomains = twist(this.groupDomains);
@@ -507,6 +509,7 @@ var PivotModel = AbstractModel.extend({
measures: this.data.measures,
origins: this.data.origins,
rowGroupBys: groupBys.rowGroupBys,
+ selectionGroupBys: this._getSelectionGroupBy(groupBys),
};
if (!raw && state.hasData) {
state.table = this._getTable();
@@ -540,6 +543,7 @@ var PivotModel = AbstractModel.extend({
* @param {string} [params.default_order]
* @param {string} [params.timeRangeDescription=""]
* @param {string} params.modelName
+ * @param {Object[]} params.groupableFields
* @returns {Promise}
*/
load: function (params) {
@@ -549,6 +553,7 @@ var PivotModel = AbstractModel.extend({
this.fields = params.fields;
this.modelName = params.modelName;
+ this.groupableFields = params.groupableFields;
this.data = {
expandedRowGroupBys: [],
expandedColGroupBys: [],
@@ -661,6 +666,7 @@ var PivotModel = AbstractModel.extend({
* in-memory sort.
*
* @param {Object} sortedColumn
+ * @param {number[]} sortedColumn.groupId
*/
sortRows: function (sortedColumn) {
var self = this;
@@ -1041,7 +1047,7 @@ var PivotModel = AbstractModel.extend({
var groupId = column.groupId;
var measure = column.measure;
var isSorted = sortedColumn.measure === measure &&
- _.isEqual(sortedColumn.groupId, groupId);
+ _.isEqual(sortedColumn.groupId, groupId);
var isSortedByOrigin = isSorted && !sortedColumn.originIndexes[1];
var isSortedByVariation = isSorted && sortedColumn.originIndexes[1];
@@ -1092,6 +1098,31 @@ var PivotModel = AbstractModel.extend({
}
return origins;
},
+
+ /**
+ * Get the selection needed to display the group by dropdown
+ * @returns {Object[]}
+ * @private
+ */
+ _getSelectionGroupBy: function (groupBys) {
+ let groupedFieldNames = groupBys.rowGroupBys
+ .concat(groupBys.colGroupBys)
+ .map(function (g) {
+ return g.split(':')[0];
+ });
+
+ var fields = Object.keys(this.groupableFields)
+ .map((fieldName, index) => {
+ return {
+ name: fieldName,
+ field: this.groupableFields[fieldName],
+ active: groupedFieldNames.includes(fieldName)
+ }
+ })
+ .sort((left, right) => left.field.string < right.field.string ? -1 : 1);
+ return fields;
+ },
+
/**
* Returns a description of the pivot table.
*
@@ -1132,6 +1163,7 @@ var PivotModel = AbstractModel.extend({
title: "",
width: 1,
});
+
// col groupby cells with group values
/**
* Recursive function that generates the header cells corresponding to
@@ -1139,7 +1171,7 @@ var PivotModel = AbstractModel.extend({
*
* @param {Object} tree
*/
- function generateTreeHeaders(tree) {
+ function generateTreeHeaders(tree, fields) {
var group = tree.root;
var rowIndex = group.values.length;
var row = colGroupRows[rowIndex];
@@ -1150,6 +1182,7 @@ var PivotModel = AbstractModel.extend({
groupId: groupId,
height: isLeaf ? (colGroupBys.length + 1 - rowIndex) : 1,
isLeaf: isLeaf,
+ label: rowIndex === 0 ? undefined : fields[colGroupBys[rowIndex - 1].split(':')[0]].string,
title: group.labels[group.labels.length - 1] || _t('Total'),
width: leafCount * measureCount * (2 * originCount - 1),
};
@@ -1159,10 +1192,11 @@ var PivotModel = AbstractModel.extend({
}
_.values(tree.directSubTrees).forEach(function (subTree) {
- generateTreeHeaders(subTree);
+ generateTreeHeaders(subTree, fields);
});
}
- generateTreeHeaders(this.colGroupTree);
+
+ generateTreeHeaders(this.colGroupTree, this.fields);
// blank top right cell for 'Total' group (if there is more that one leaf)
if (leafCounts[JSON.stringify(this.colGroupTree.root.values)] > 1) {
var groupId = [[], []];
@@ -1205,6 +1239,7 @@ var PivotModel = AbstractModel.extend({
var title = group.labels[group.labels.length - 1] || _t('Total');
var indent = group.labels.length;
var isLeaf = _.isEmpty(tree.directSubTrees);
+ var rowGroupBys = this._getGroupBys().rowGroupBys;
var subGroupMeasurements = columns.map(function (column) {
var colGroupId = column.groupId;
@@ -1226,6 +1261,7 @@ var PivotModel = AbstractModel.extend({
rows.push({
title: title,
+ label: indent === 0 ? undefined : this.fields[rowGroupBys[indent - 1].split(':')[0]].string,
groupId: rowGroupId,
indent: indent,
isLeaf: isLeaf,
@@ -1276,17 +1312,17 @@ var PivotModel = AbstractModel.extend({
_loadData: function () {
var self = this;
- this.rowGroupTree = {root: {labels: [], values: []}, directSubTrees: {}};
- this.colGroupTree = {root: {labels: [], values: []}, directSubTrees: {}};
+ this.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: {} };
+ this.colGroupTree = { root: { labels: [], values: [] }, directSubTrees: {} };
this.measurements = {};
this.counts = {};
- var key = JSON.stringify([[],[]]);
+ var key = JSON.stringify([[], []]);
this.groupDomains = {};
this.groupDomains[key] = this.data.domains.slice(0);
- var group = {rowValues: [], colValues: []};
+ var group = { rowValues: [], colValues: [] };
var groupBys = this._getGroupBys();
var leftDivisors = sections(groupBys.rowGroupBys);
var rightDivisors = sections(groupBys.colGroupBys);
@@ -1305,7 +1341,6 @@ var PivotModel = AbstractModel.extend({
*
* @private
* @param {Object} group
- TO DO
* @param {Object[]} groupSubdivisions
*/
_prepareData: function (group, groupSubdivisions) {
@@ -1447,7 +1482,7 @@ var PivotModel = AbstractModel.extend({
return this._getNumberedLabel(value, fieldName);
}
if (fieldName && this.fields[fieldName] && (this.fields[fieldName].type === 'selection')) {
- var selected = _.where(this.fields[fieldName].selection, {0: value})[0];
+ var selected = _.where(this.fields[fieldName].selection, { 0: value })[0];
return selected ? selected[1] : value;
}
return value;
@@ -1487,7 +1522,11 @@ var PivotModel = AbstractModel.extend({
// if group is known to be empty for the given origin,
// we don't need to fetch data fot that origin.
if (!self.counts[key] || self.counts[key][originIndex] > 0) {
- var subGroup = {rowValues: group.rowValues, colValues: group.colValues, originIndex: originIndex};
+ var subGroup = {
+ rowValues: group.rowValues,
+ colValues: group.colValues,
+ originIndex: originIndex
+ };
divisors.forEach(function (divisor) {
acc.push(self._getGroupSubdivision(subGroup, divisor[0], divisor[1]));
});
diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js
index 3affc975225..6345d6ac0cb 100644
--- a/addons/web/static/src/js/views/pivot/pivot_renderer.js
+++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js
@@ -1,319 +1,200 @@
odoo.define('web.PivotRenderer', function (require) {
-"use strict";
+ "use strict";
-var AbstractRenderer = require('web.AbstractRenderer');
-var config = require('web.config');
-var core = require('web.core');
-var dataComparisonUtils = require('web.dataComparisonUtils');
-var field_utils = require('web.field_utils');
-
-var QWeb = core.qweb;
-
-var _t = core._t;
-
-var PivotRenderer = AbstractRenderer.extend({
- tagName: 'table',
- className: 'table-hover table-sm table-bordered',
- events: _.extend({}, AbstractRenderer.prototype.events, {
- 'click td.o_pivot_cell_value': '_onCellValueClicked',
- 'click .o_pivot_header_cell_opened': '_onOpenHeaderClick',
- 'click .o_pivot_measure_row': '_onSpecialRowClick',
- 'click .o_pivot_origin_row': '_onSpecialRowClick',
- 'mouseenter thead tr:last th': '_onMouseenterCell',
- 'mouseenter tbody td': '_onMouseenterCell',
- 'mouseleave thead tr:last th': '_onMouseleaveCell',
- 'mouseleave tbody td': '_onMouseleaveCell',
- }),
+ const { useState, onMounted, onPatched } = owl.hooks;
+ const { useExternalListener } = require('web.custom_hooks');
+ const OwlAbstractRenderer = require('web.AbstractRendererOwl');
+ const field_utils = require('web.field_utils');
/**
- * @override
- * @param {Widget} parent
- * @param {Object} state
- * @param {Object} params
- * @param {boolean} params.enableLinking configure the pivot view to allow
- * opening a list view by clicking on a cell with some data.
+ * Here is a basic example of the structure of the Pivot Table:
+ *
+ * ┌─────────────────────────┬─────────────────────────────────────────────┬─────────────────┐
+ * │ │ - web.PivotHeader │ │
+ * │ ├──────────────────────┬──────────────────────┤ │
+ * │ │ + web.PivotHeader │ + web.PivotHeader │ │
+ * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤
+ * │ │ web.PivotMeasure │ web.PivotMeasure │ │
+ * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤
+ * │ ─ web.PivotHeader │ │ │ │
+ * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤
+ * │ + web.PivotHeader │ │ │ │
+ * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤
+ * │ + web.PivotHeader │ │ │ │
+ * └─────────────────────────┴──────────────────────┴──────────────────────┴─────────────────┘
+ *
*/
- init: function (parent, state, params) {
- this._super.apply(this, arguments);
- this.enableLinking = params.enableLinking;
- this.fieldWidgets = params.widgets || {};
- this.paddingLeftHeaderTabWidth = config.device.isMobile ? 5 : 30;
- },
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
+ class PivotRenderer extends OwlAbstractRenderer {
+ /**
+ * @override
+ * @param {boolean} props.disableLinking Disallow opening records by clicking on a cell
+ * @param {Object} props.widgets Widgets defined in the arch
+ */
+ constructor(parent, props) {
+ super(...arguments);
+
+ this.state = useState({
+ activeNodeHeader: {
+ groupId: false,
+ isXAxis: false,
+ click: false
+ },
+ });
- /**
- * @private
- * @param {string[]} groupBy list of 'fieldName[:period]'
- * @returns {string[]} list of fields label
- */
- _getGroupByLabels: function (groupBy) {
- var self = this;
- return _.map(groupBy, function (gb) {
- return self.state.fields[gb.split(':')[0]].string;
- });
- },
- /**
- * @override
- * @private
- * @returns {Promise}
- */
- _render: function () {
- var hasContent = this.state.hasData && this.state.measures.length;
- if (!hasContent) {
- // display the nocontent helper
- this._replaceElement(QWeb.render('View.NoContentHelper', {
- description: _t("Try to add some records, or make sure that there is at least " +
- "one measure and no active filter in the search bar."),
- }));
- return this._super.apply(this, arguments);
- }
+ onMounted(() => this._updateTooltip());
- this.renderElement(); // in case we come from no content helper
- this.$el.toggleClass('o_enable_linking', this.enableLinking);
+ onPatched(() => this._updateTooltip());
- var $thead = $('<thead>');
- var $tbody = $('<tbody>');
- this._renderHeaders($thead);
- this._renderRows($tbody);
- this.$el.empty().append($thead).append($tbody);
- this.$('.o_pivot_header_cell_opened, .o_pivot_header_cell_closed').tooltip();
+ if (!this.env.device.isMobile) {
+ useExternalListener(window, 'click', this._resetState);
+ }
+ }
- return this._super.apply(this, arguments);
- },
- /**
- * @private
- * @param {jQuery} $thead
- */
- _renderHeaders: function ($thead) {
- // groupbyLabels is the list of col groupby fields label
- var groupbyLabels = this._getGroupByLabels(this.state.colGroupBys);
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * Get the formatted value of the cell
+ *
+ * @private
+ * @param {Object} cell
+ * @returns {string} Formatted value
+ */
+ _getFormattedValue(cell) {
+ const type = this.props.widgets[cell.measure] ||
+ (this.props.fields[cell.measure].type === 'many2one' ? 'integer' : this.props.fields[cell.measure].type);
+ const formatter = field_utils.format[type];
+ return formatter(cell.value, this.props.fields[cell.measure]);
+ }
- this.state.table.headers.forEach(function (row, rowIndex) {
- var $tr = $('<tr>');
- row.forEach(function (cell) {
- var cellParams = {
- text: cell.title,
- colspan: cell.width,
- rowspan: cell.height,
- data: {
- groupId: cell.groupId,
- type: 'col',
- }
- }
- var className;
- if (cell.measure) {
- if (cell.originIndexes) {
- cellParams.data.originIndexes = cell.originIndexes
- className = 'o_pivot_origin_row';
- } else {
- className = 'o_pivot_measure_row';
- }
- className += ' text-muted';
- if (cell.order) {
- className += ' o_pivot_sort_order_' + cell.order;
- if (cell.order === 'asc') {
- cellParams['aria-sorted'] = 'ascending'
- } else {
- cellParams['aria-sorted'] = 'descending'
- }
- }
- cellParams.data.measure = cell.measure;
- } else if ('isLeaf' in cell) {
- if (rowIndex > 0) {
- cellParams.title = groupbyLabels[rowIndex - 1];
- }
- className = 'o_pivot_header_cell' + (cell.isLeaf ? '_closed' : '_opened');
- }
- cellParams.class = className;
-
- $tr.append($('<th>', cellParams));
- });
- $thead.append($tr);
- });
- },
- /**
- * @private
- * @param {jQuery} $tbody
- */
- _renderRows: function ($tbody) {
- var self = this;
+ /**
+ * Get the formatted variation of a cell
+ *
+ * @private
+ * @param {Object} cell
+ * @returns {string} Formatted variation
+ */
+ _getFormattedVariation(cell) {
+ const value = cell.value;
+ return isNaN(value) ? '-' : field_utils.format.percentage(value, this.props.fields[cell.measure]);
+ }
- // measureTypes is a mapping from measure fields to their field type,
- // with a special case for many2one fields which are mapped to the
- // 'integer' type (as their group_operator is 'count_distinct')
- var measureTypes = this.state.measures.reduce(
- function (acc, measureName) {
- var type = self.state.fields[measureName].type;
- acc[measureName] = type === 'many2one' ? 'integer' : type;
- return acc;
- },
- {}
- );
+ /**
+ * Retrieves the padding of a left header
+ *
+ * @private
+ * @param {Object} cell
+ * @returns {Number} Padding
+ */
+ _getPadding(cell) {
+ return 5 + cell.indent * 30;
+ }
- // groupbyLabels is the list of row groupby fields label
- var groupbyLabels = this._getGroupByLabels(this.state.rowGroupBys);
+ /**
+ * Compute if a cell is active (with its groupId)
+ *
+ * @private
+ * @param {Array} groupId GroupId of a cell
+ * @param {Boolean} isXAxis true if the cell is on the x axis
+ * @returns {Boolean} true if the cell is active
+ */
+ _isClicked(groupId, isXAxis) {
+ return _.isEqual(groupId, this.state.activeNodeHeader.groupId) && this.state.activeNodeHeader.isXAxis === isXAxis;
+ }
- this.state.table.rows.forEach(function (row) {
- var $tr = $('<tr>');
- var paddingLeft = 5 + row.indent * self.paddingLeftHeaderTabWidth;
- $tr.append($('<th>', {
- text: row.title,
- title: row.indent > 0 ? groupbyLabels[row.indent - 1] : null,
- data: {
- groupId: row.groupId,
- type: 'row',
- },
- css: {
- 'padding-left': paddingLeft + 'px',
- },
- class: 'o_pivot_header_cell_' + (row.isLeaf ? 'closed' : 'opened'),
- }));
+ /**
+ * Reset the state of the node.
+ *
+ * @private
+ */
+ _resetState() {
+ // This is a pourrav solution
+ if (this.isHeaderClicked) {
+ this.isHeaderClicked = false;
+ return;
+ }
+ this.state.activeNodeHeader = {
+ groupId: false,
+ isXAxis: false,
+ click: false
+ };
+ }
- row.subGroupMeasurements.forEach(function (measurement) {
- var cellParams = {
- data: {
- groupId: measurement.groupId,
- originIndexes: measurement.originIndexes,
- },
- class: 'o_pivot_cell_value text-right',
- };
- if (measurement.isBold) {
- cellParams.css = {
- 'font-weight': 'bold',
- };
- }
- if (measurement.value !== undefined) {
- var measure = measurement.measure;
- var measureField = self.state.fields[measure];
- var $value;
- if (measurement.originIndexes.length > 1) {
- $value = dataComparisonUtils.renderVariation(measurement.value, measureField);
- } else {
- var formatType = self.fieldWidgets[measure] || measureTypes[measure];
- var formatter = field_utils.format[formatType];
- $value = $('<div>', {
- class: 'o_value',
- html: formatter(measurement.value, measureField),
- });
- }
- cellParams.html = $value;
- } else {
- cellParams.class += ' o_empty'
- }
- $tr.append($('<td>', cellParams));
- });
- $tbody.append($tr);
- });
- },
+ /**
+ * Configure the tooltips on the headers.
+ *
+ * @private
+ */
+ _updateTooltip() {
+ $(this.el).find('.o_pivot_header_cell_opened, .o_pivot_header_cell_closed').tooltip();
+ }
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
- /**
- * When the user clicks on a non empty cell, and the view is configured to
- * allow 'linking' (with enableLinking), we want to open a list view with
- * the corresponding record.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onCellValueClicked: function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
- var $target = $(ev.currentTarget);
- if ($target.hasClass('o_empty') || !this.enableLinking) {
- return;
+ /**
+ * Handles a click on a menu item in the dropdown to select a groupby.
+ *
+ * @private
+ * @param {Object} field
+ * @param {string} interval
+ */
+ _onClickMenuGroupBy(field, interval) {
+ this.trigger('groupby_menu_selection', { field, interval });
}
- var context = _.omit(this.state.context, function (val, key) {
- return key === 'group_by' || _.str.startsWith(key, 'search_default_');
- });
-
- var groupId = $target.data('groupId');
- var originIndexes = $target.data('originIndexes');
- var group = {
- rowValues: groupId[0],
- colValues: groupId[1],
- originIndex: originIndexes[0]
- };
-
- this.trigger_up('open_view', {
- group: group,
- context: context,
- });
- },
- /**
- * Highlight the column when hovering a cell.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onMouseenterCell: function (ev) {
- var index = $(ev.currentTarget).index();
- if ($(ev.currentTarget).is('th')) { // header cell
- index += 1; // increment by 1 to compensate the top left empty cell
+ /**
+ * Handles a click on a header node
+ *
+ * @private
+ * @param {Object} cell
+ * @param {string} type col or row
+ */
+ _onHeaderClick(cell, type) {
+ const groupValues = cell.groupId[type === 'col' ? 1 : 0];
+ const groupByLength = type === 'col' ? this.props.colGroupBys.length : this.props.rowGroupBys.length;
+ if (cell.isLeaf && groupValues.length >= groupByLength) {
+ this.isHeaderClicked = true;
+ this.state.activeNodeHeader = {
+ groupId: cell.groupId,
+ isXAxis: type === 'col',
+ click: 'leftClick'
+ };
+ }
+ this.trigger(cell.isLeaf ? 'closed_header_click' : 'opened_header_click', { cell, type });
}
- this.$("td").filter(":nth-child(" + (index + 1) + ")").addClass("o_cell_hover");
- },
- /**
- * @private
- */
- _onMouseleaveCell: function () {
- this.$('.o_cell_hover').removeClass('o_cell_hover');
- },
- /**
- * This method is called when someone clicks on an open header. When that
- * happens, we want to close the header, then redisplay the view.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onOpenHeaderClick: function (ev) {
- ev.preventDefault();
- ev.stopImmediatePropagation();
- var $target = $(ev.target);
- var groupId = $target.data('groupId');
- var type = $target.data('type');
+ /**
+ * Hover the column in which the mouse is.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onMouseEnter(ev) {
+ var index = [...ev.currentTarget.parentNode.children].indexOf(ev.currentTarget);
+ if (ev.currentTarget.tagName === 'TH') {
+ index += 1;
+ }
+ this.el.querySelectorAll('td:nth-child(' + (index + 1) + ')').forEach(elt => elt.classList.add('o_cell_hover'));
+ }
- this.trigger_up('close_group', {
- groupId: groupId,
- type: type,
- });
- },
- /**
- * If the user clicks on a measure or origin row, we perform an in-memory sort.
- *
- * @private
- * @param {MouseEvent} ev
- */
- _onSpecialRowClick: function (ev) {
- ev.preventDefault();
- ev.stopImmediatePropagation();
+ /**
+ * Remove the hover on the columns.
+ *
+ * @private
+ */
+ _onMouseLeave() {
+ this.el.querySelectorAll('.o_cell_hover').forEach(elt => elt.classList.remove('o_cell_hover'));
+ }
+ }
- var $target = $(ev.target);
- var groupId = $target.data('groupId');
- var measure = $target.data('measure');
- var originIndexes = $target.data('originIndexes');
- var isAscending = $target.hasClass('o_pivot_sort_order_asc');
- var order = isAscending ? 'desc' : 'asc';
+ PivotRenderer.template = 'web.PivotRenderer';
- this.trigger_up('sort_rows', {
- sortedColumn: {
- groupId: groupId,
- measure: measure,
- order: order,
- originIndexes: originIndexes,
- }
- });
- },
-});
+ return PivotRenderer;
-return PivotRenderer;
});
diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js
index 1b878cf3530..17cc478ba9b 100644
--- a/addons/web/static/src/js/views/pivot/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot/pivot_view.js
@@ -1,137 +1,150 @@
odoo.define('web.PivotView', function (require) {
-"use strict";
-
-/**
- * The Pivot View is a view that represents data in a 'pivot grid' form. It
- * aggregates data on 2 dimensions and displays the result, allows the user to
- * 'zoom in' data.
- */
-
-var AbstractView = require('web.AbstractView');
-var config = require('web.config');
-var core = require('web.core');
-var PivotModel = require('web.PivotModel');
-var PivotController = require('web.PivotController');
-var PivotRenderer = require('web.PivotRenderer');
-
-var _t = core._t;
-var _lt = core._lt;
-
-var controlPanelViewParameters = require('web.controlPanelViewParameters');
-var GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES;
-
-var PivotView = AbstractView.extend({
- display_name: _lt('Pivot'),
- icon: 'fa-table',
- config: _.extend({}, AbstractView.prototype.config,{
- Model: PivotModel,
- Controller: PivotController,
- Renderer: PivotRenderer,
- }),
- viewType: 'pivot',
- searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
+ "use strict";
/**
- * @override
- * @param {Object} params
+ * The Pivot View is a view that represents data in a 'pivot grid' form. It
+ * aggregates data on 2 dimensions and displays the result, allows the user to
+ * 'zoom in' data.
*/
- init: function (viewInfo, params) {
- var self = this;
- this._super.apply(this, arguments);
-
- var activeMeasures = [];
- var colGroupBys = [];
- var rowGroupBys = [];
-
- var measures = {};
- var groupableFields = {};
- var widgets = {};
-
- this.fields.__count = {string: _t("Count"), type: "integer"};
- _.each(this.fields, function (field, name) {
- if ((name !== 'id') && (field.store === true)) {
- if (_.contains(['integer', 'float', 'monetary'], field.type) ||
- _.contains(params.additionalMeasures, name)) {
+
+ const AbstractView = require('web.AbstractView');
+ const config = require('web.config');
+ const core = require('web.core');
+ const PivotModel = require('web.PivotModel');
+ const PivotController = require('web.PivotController');
+ const PivotRenderer = require('web.PivotRenderer');
+ const RendererWrapper = require('web.RendererWrapper');
+
+ const _t = core._t;
+ const _lt = core._lt;
+
+ const controlPanelViewParameters = require('web.controlPanelViewParameters');
+ const GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES;
+
+ const PivotView = AbstractView.extend({
+ display_name: _lt('Pivot'),
+ icon: 'fa-table',
+ config: Object.assign({}, AbstractView.prototype.config, {
+ Model: PivotModel,
+ Controller: PivotController,
+ Renderer: PivotRenderer,
+ }),
+ viewType: 'pivot',
+ searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
+
+ /**
+ * @override
+ * @param {Object} params
+ * @param {Array} params.additionalMeasures
+ */
+ init: function (viewInfo, params) {
+ this._super.apply(this, arguments);
+
+ const activeMeasures = []; // Store the defined active measures
+ const colGroupBys = []; // Store the defined group_by used on cols
+ const rowGroupBys = []; // Store the defined group_by used on rows
+ const measures = {}; // All the available measures
+ const groupableFields = {}; // The fields which can be used to group data
+ const widgets = {}; // Wigdets defined in the arch
+ const additionalMeasures = params.additionalMeasures || [];
+
+ this.fields.__count = { string: _t("Count"), type: "integer" };
+
+ //Compute the measures and the groupableFields
+ Object.keys(this.fields).forEach(name => {
+ const field = this.fields[name];
+ if (name !== 'id' && field.store === true) {
+ if (['integer', 'float', 'monetary'].includes(field.type) || additionalMeasures.includes(name)) {
measures[name] = field;
+ }
+ if (GROUPABLE_TYPES.includes(field.type)) {
+ groupableFields[name] = field;
+ }
+ }
+ });
+ measures.__count = { string: _t("Count"), type: "integer" };
+
+
+ this.arch.children.forEach(field => {
+ let name = field.attrs.name;
+
+ // Remove invisible fields from the measures
+ if (field.attrs.invisible && py.eval(field.attrs.invisible)) {
+ delete measures[name];
+ return;
+ }
+ if (field.attrs.interval) {
+ name += ':' + field.attrs.interval;
+ }
+ if (field.attrs.widget) {
+ widgets[name] = field.attrs.widget;
+ }
+ // add active measures to the measure list. This is very rarely
+ // necessary, but it can be useful if one is working with a
+ // functional field non stored, but in a model with an overrided
+ // read_group method. In this case, the pivot view could work, and
+ // the measure should be allowed. However, be careful if you define
+ // a measure in your pivot view: non stored functional fields will
+ // probably not work (their aggregate will always be 0).
+ if (field.attrs.type === 'measure' && !(name in measures)) {
+ measures[name] = this.fields[name];
+ }
+ if (field.attrs.string) {
+ measures[name].string = field.attrs.string;
+ }
+ if (field.attrs.type === 'measure' || 'operator' in field.attrs) {
+ activeMeasures.push(name);
+ measures[name] = this.fields[name];
+ }
+ if (field.attrs.type === 'col') {
+ colGroupBys.push(name);
}
- if (_.contains(GROUPABLE_TYPES, field.type)) {
- groupableFields[name] = field;
+ if (field.attrs.type === 'row') {
+ rowGroupBys.push(name);
}
+ });
+ if ((!activeMeasures.length) || this.arch.attrs.display_quantity) {
+ activeMeasures.splice(0, 0, '__count');
}
- });
- measures.__count = {string: _t("Count"), type: "integer"};
- this.arch.children.forEach(function (field) {
- var name = field.attrs.name;
- if (field.attrs.interval) {
- name += ':' + field.attrs.interval;
- }
- if (field.attrs.widget) {
- widgets[name] = field.attrs.widget;
- }
- if (field.attrs.invisible && py.eval(field.attrs.invisible)) {
- delete measures[name];
- return;
- }
- // add active measures to the measure list. This is very rarely
- // necessary, but it can be useful if one is working with a
- // functional field non stored, but in a model with an overrided
- // read_group method. In this case, the pivot view could work, and
- // the measure should be allowed. However, be careful if you define
- // a measure in your pivot view: non stored functional fields will
- // probably not work (their aggregate will always be 0).
- if (field.attrs.type === 'measure' && !(name in measures)) {
- measures[name] = self.fields[name];
- }
- if (field.attrs.string) {
- measures[name].string = field.attrs.string;
- }
- if (field.attrs.type === 'measure' || 'operator' in field.attrs) {
- activeMeasures.push(name);
- measures[name] = self.fields[name];
+ this.loadParams.measures = activeMeasures;
+ this.loadParams.colGroupBys = config.device.isMobile ? [] : colGroupBys;
+ this.loadParams.rowGroupBys = rowGroupBys;
+ this.loadParams.fields = this.fields;
+ this.loadParams.default_order = params.default_order || this.arch.attrs.default_order;
+ this.loadParams.groupableFields = groupableFields;
+
+ this.rendererParams.widgets = widgets;
+ this.rendererParams.disableLinking = this.arch.attrs.disable_linking;
+
+ this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled");
+ this.controllerParams.measures = measures;
+
+ // retrieve form and list view ids from the action to open those views
+ // when a data cell of the pivot view is clicked
+ this.controllerParams.views = [
+ _findView(params.actionViews, 'list'),
+ _findView(params.actionViews, 'form'),
+ ];
+
+ function _findView(views, viewType) {
+ const view = views.find(view => {
+ return view.type === viewType;
+ });
+ return [view ? view.viewID : false, viewType];
}
- if (field.attrs.type === 'col') {
- colGroupBys.push(name);
- }
- if (field.attrs.type === 'row') {
- rowGroupBys.push(name);
- }
- });
- if ((!activeMeasures.length) || this.arch.attrs.display_quantity) {
- activeMeasures = ['__count'].concat(activeMeasures);
- }
-
- this.loadParams.measures = activeMeasures;
- this.loadParams.colGroupBys = colGroupBys;
- this.loadParams.rowGroupBys = rowGroupBys;
- this.loadParams.fields = this.fields;
- this.loadParams.default_order = params.default_order || this.arch.attrs.default_order;
- if (config.device.isMobile) {
- this.loadParams.colGroupBys = [];
- }
-
-
- this.rendererParams.widgets = widgets;
- this.rendererParams.enableLinking = !this.arch.attrs.disable_linking;
-
- this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled");
- this.controllerParams.measures = measures;
- this.controllerParams.groupableFields = groupableFields;
- // retrieve form and list view ids from the action to open those views
- // when a data cell of the pivot view is clicked
- this.controllerParams.views = [
- _findView(params.actionViews, 'list'),
- _findView(params.actionViews, 'form'),
- ];
- function _findView(views, viewType) {
- var view = _.find(views, function (view) {
- return view.type === viewType;
- });
- return [view ? view.viewID : false, viewType];
- }
- },
-});
-
-return PivotView;
+ },
+
+ /**
+ *
+ * @override
+ */
+ getRenderer(parent, state) {
+ state = Object.assign(state || {}, this.rendererParams);
+ return new RendererWrapper(null, state, this.config.Renderer);
+ },
+ });
+
+ return PivotView;
});
diff --git a/addons/web/static/src/scss/pivot_view.scss b/addons/web/static/src/scss/pivot_view.scss
index d67607fe558..3dbfc809d77 100644
--- a/addons/web/static/src/scss/pivot_view.scss
+++ b/addons/web/static/src/scss/pivot_view.scss
@@ -1,5 +1,4 @@
.o_pivot {
- overflow: auto;
.o_pivot_cell_value {
font-size: 1em;
@@ -91,10 +90,6 @@
}
}
- .o_field_selection {
- position: absolute;
- }
-
.o_pivot_field_selection::after {
@include o-caret-right;
position: absolute;
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 8854426367c..c3f57baba47 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -469,9 +469,6 @@
</table>
</div>
-<div t-name="PivotView" t-attf-class="o_pivot #{widget.enableLinking ? 'o_enable_linking' : ''}">
- <div class="o_field_selection"/>
-</div>
<t t-name="PivotView.buttons">
<div class="btn-group" role="toolbar" aria-label="Main actions">
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
@@ -491,29 +488,6 @@
<button class="btn btn-secondary fa fa-download o_pivot_download" title="Download xls" aria-label="Download xls"/>
</div>
</t>
-<t t-name="PivotView.GroupBySelection">
- <div class="dropdown-menu o_pivot_field_menu" role="menu">
- <t t-foreach="fields" t-as="field">
- <t t-if="(field[1].type === 'date') || (field[1].type === 'datetime')">
- <div role="menuitem" aria-haspopup="true" class="o_inline_dropdown">
- <a href="#" t-att-data-field="field[0]" t-attf-class="dropdown-item o_pivot_field_selection#{field[2] ? ' disabled' : ''}">
- <t t-esc="field[1].string"/>
- </a>
- <div class="dropdown-menu" role="menu">
- <a role="menuitem" href="#" class="dropdown-item" t-att-data-interval="'day'" t-att-data-field="field[0]">Day</a>
- <a role="menuitem" href="#" class="dropdown-item" t-att-data-interval="'week'" t-att-data-field="field[0]">Week</a>
- <a role="menuitem" href="#" class="dropdown-item" t-att-data-interval="'month'" t-att-data-field="field[0]">Month</a>
- <a role="menuitem" href="#" class="dropdown-item" t-att-data-interval="'quarter'" t-att-data-field="field[0]">Quarter</a>
- <a role="menuitem" href="#" class="dropdown-item" t-att-data-interval="'year'" t-att-data-field="field[0]">Year</a>
- </div>
- </div>
- </t>
- <t t-else="">
- <a href="#" role="menuitem" t-att-data-field="field[0]" t-attf-class="dropdown-item#{field[2] ? ' disabled' : ''}"><t t-esc="field[1].string"/></a>
- </t>
- </t>
- </div>
-</t>
<t t-name="FormSelection">
<div class="btn-group o_selection" aria-atomic="true">
diff --git a/addons/web/static/src/xml/pivot.xml b/addons/web/static/src/xml/pivot.xml
new file mode 100644
index 00000000000..6e85f1170de
--- /dev/null
+++ b/addons/web/static/src/xml/pivot.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates>
+
+ <div t-name="web.PivotRenderer" class="o_pivot" owl="1">
+ <t t-if="!props.hasData || !props.measures.length" t-call="web.NoContentHelper">
+ <t t-set="description" t-value="'Try to add some records, or make sure
+ that there is at least one measure and
+ no active filter in the search bar'"/>
+ </t>
+ <table t-else="1" class="table-hover table-sm table-bordered" t-att-class="{ o_enable_linking: !props.disableLinking }">
+ <thead>
+ <tr t-foreach="props.table.headers" t-as="row" t-key="'header_' + row_index">
+ <t t-foreach="row" t-as="cell" t-key="'header_row_' + cell_index">
+ <t t-if="cell.measure" t-call="web.PivotMeasure"/>
+ <t t-elif="cell.isLeaf !== undefined" t-call="web.PivotHeader">
+ <t t-set="isXAxis" t-value="true"/>
+ </t>
+ <th t-else="1" t-att-colspan="cell.width" t-att-rowspan="cell.height"/>
+ </t>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="props.table.rows" t-as="row" t-key="'row_' + row_index">
+ <t t-call="web.PivotHeader">
+ <t t-set="isXAxis" t-value="false"/>
+ <t t-set="cell" t-value="row"/>
+ </t>
+ <t t-foreach="row.subGroupMeasurements" t-as="cell" t-key="'row_cell_' + cell_index">
+ <td class="o_pivot_cell_value text-right" t-att-class="{
+ o_empty: cell.value === undefined,
+ 'font-weight-bold': cell.isBold,
+ }" t-on-click="trigger('open_view', cell)"
+ t-on-mouseover="_onMouseEnter" t-on-mouseout="_onMouseLeave">
+ <t t-if="cell.value !== undefined">
+ <div t-if="cell.originIndexes.length > 1" class="o_variation" t-att-class="{
+ o_positive: cell.value &gt; 0,
+ o_negative: cell.value &lt; 0,
+ o_null: cell.value === 0,
+ }" t-esc="_getFormattedVariation(cell)"/>
+ <div t-else="1" class="o_value" t-esc="_getFormattedValue(cell)"/>
+ </t>
+ </td>
+ </t>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <t t-name="web.PivotHeader" owl="1">
+ <th t-att-colspan="isXAxis ? cell.width : undefined" t-att-rowspan="isXAxis ? cell.height : undefined" t-att-class="{
+ o_pivot_header_cell_closed: cell.isLeaf,
+ o_pivot_header_cell_opened: !cell.isLeaf,
+ }" t-attf-style="{{ isXAxis ? undefined : 'padding-left: ' + _getPadding(cell) + 'px;' }}" t-att-title="cell.label" t-on-click.self.prevent="_onHeaderClick(cell, isXAxis ? 'col' : 'row')">
+ <span t-on-click.self.prevent="_onHeaderClick(cell, isXAxis ? 'col' : 'row')" t-esc="cell.title"/>
+ <t t-if="_isClicked(cell.groupId, isXAxis)" t-call="web.PivotGroupBySelection"/>
+ </th>
+ </t>
+
+ <t t-name="web.PivotMeasure" owl="1">
+ <th class="text-muted" t-att-colspan="cell.width" t-att-rowspan="cell.height" t-att-class="{
+ o_pivot_origin_row: cell.originIndexes,
+ o_pivot_measure_row: !cell.originIndexes,
+ o_pivot_sort_order_asc: cell.order === 'asc',
+ o_pivot_sort_order_desc: cell.order === 'desc',
+ }" t-on-click.prevent="trigger('sort_rows',cell)" t-on-mouseover="_onMouseEnter" t-on-mouseout="_onMouseLeave">
+ <t t-esc="cell.title"/>
+ </th>
+ </t>
+
+ <div t-name="web.PivotGroupBySelection" class="dropdown" owl="1">
+ <div class="dropdown-menu o_pivot_field_menu show" data-display="static" role="menu">
+ <t t-foreach="props.selectionGroupBys" t-as="field" t-key="field.field.name">
+ <t t-if="(field.field.type === 'date') || (field.field.type === 'datetime')">
+ <div role="menuitem" aria-haspopup="true" class="o_inline_dropdown">
+ <a href="#"
+ t-on-click.prevent="_onClickMenuGroupBy(field, 'month')"
+ class="dropdown-item o_pivot_field_selection"
+ t-att-class="{ disabled: field.active }"
+ t-att-data-field="field.name"
+ >
+ <t t-esc="field.field.string"/>
+ </a>
+ <div class="dropdown-menu" role="menu">
+ <a role="menuitem" t-att-data-field="field.name" href="#" class="dropdown-item" t-on-click.prevent="_onClickMenuGroupBy(field, 'day')">Day</a>
+ <a role="menuitem" t-att-data-field="field.name" href="#" class="dropdown-item" t-on-click.prevent="_onClickMenuGroupBy(field, 'week')">Week</a>
+ <a role="menuitem" t-att-data-field="field.name" href="#" class="dropdown-item" t-on-click.prevent="_onClickMenuGroupBy(field, 'month')">Month</a>
+ <a role="menuitem" t-att-data-field="field.name" href="#" class="dropdown-item" t-on-click.prevent="_onClickMenuGroupBy(field, 'quarter')">Quarter</a>
+ <a role="menuitem" t-att-data-field="field.name" href="#" class="dropdown-item" t-on-click.prevent="_onClickMenuGroupBy(field, 'year')">Year</a>
+ </div>
+ </div>
+ </t>
+ <a t-else="" href="#" role="menuitem" class="dropdown-item" t-att-data-field="field.name" t-att-class="{ disabled: field.active }" t-on-click.prevent="_onClickMenuGroupBy(field, false)">
+ <t t-esc="field.field.string"/>
+ </a>
+ </t>
+ </div>
+ </div>
+
+</templates>