diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 12bc57970f4..ccfbd9b8499 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -28,6 +28,7 @@ This module provides the core of the Odoo Web Client. "static/src/xml/kanban.xml", "static/src/xml/menu.xml", "static/src/xml/notification.xml", + "static/src/xml/pivot.xml", "static/src/xml/rainbow_man.xml", "static/src/xml/report.xml", "static/src/xml/web_calendar.xml", 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 - */ - 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); - }, - - //-------------------------------------------------------------------------- - // 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. + * Odoo Pivot Table Controller * - * @override method from AbstractController - * @returns {Object} + * 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. */ - 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 ControllerAdapter = require('web.ControllerAdapter'); + const core = require('web.core'); + const framework = require('web.framework'); + const session = require('web.session'); + + const _t = core._t; + const QWeb = core.qweb; + + 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.$buttons.appendTo($node); + this._updateButtons(); + } + }, + + //-------------------------------------------------------------------------- + // 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 () { + 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); - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- + }); + 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); + }, - /** - * 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]; + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * 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; + + const group = { + rowValues: groupId[0], + colValues: groupId[1], + type: type + }; + + 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(); + + 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; + } + + const context = Object.assign({}, this.model.data.context); + Object.keys(context).forEach(x => { + if (x === 'group_by' || x.startsWith('search_default_')) { + delete context[x]; + } }); - 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 group = { + rowValues: cell.groupId[0], + colValues: cell.groupId[1], + originIndex: cell.originIndexes[0] + }; - this.$groupBySelection.html(QWeb.render('PivotView.GroupBySelection', { - fields: fields - })); + const domain = this.model._getGroupDomain(group); - 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.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 }); + }, + }); - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * 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(); - 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}); - - var groupValues = type === 'row' ? groupId[0] : groupId[1]; - var groupBys = type === 'row' ? - state.rowGroupBys : - state.colGroupBys; - - 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; - } - - 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); - - 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}); - }, -}); - -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); - /** - * @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); - } - - this.renderElement(); // in case we come from no content helper - this.$el.toggleClass('o_enable_linking', this.enableLinking); - - var $thead = $(''); - var $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(); - - 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); - - this.state.table.headers.forEach(function (row, rowIndex) { - var $tr = $('| + |
|---|
|
+ |
+