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 94f4f3c1876..5aaeec9629c 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -238,16 +238,23 @@ var PivotController = AbstractController.extend({ * @param {MouseEvent} event */ _onCellClick: function (event) { - var $target = $(event.target); + var $target = $(event.currentTarget); if ($target.hasClass('o_pivot_header_cell_closed') || $target.hasClass('o_pivot_header_cell_opened') || $target.hasClass('o_empty') || + $target.data('type') === 'variation' || !this.enableLinking) { return; } var state = this.model.get(this.handle); - var colDomain = this.model.getHeader($target.data('col_id')).domain; - var rowDomain = this.model.getHeader($target.data('id')).domain; + var colDomain, rowDomain; + if ($target.data('type') === 'comparisonData') { + colDomain = this.model.getHeader($target.data('col_id')).comparisonDomain || []; + rowDomain = this.model.getHeader($target.data('id')).comparisonDomain || []; + } else { + colDomain = this.model.getHeader($target.data('col_id')).domain || []; + rowDomain = this.model.getHeader($target.data('id')).domain || []; + } var context = _.omit(state.context, function (val, key) { return key === 'group_by' || _.str.startsWith(key, 'search_default_'); }); 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 0cbf095db70..466350d5e99 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -75,46 +75,76 @@ var PivotModel = AbstractModel.extend({ */ expandHeader: function (header, field) { var self = this; - var other_root = header.root.other_root; var other_groupbys = header.root.other_root.groupbys; - var measures = _.map(this.data.measures, function(measure) { var type = self.fields[measure].type; return (type === 'many2one') ? measure + ":count_distinct" : measure; }); - - var groupbys = []; - + var groupBys = []; for (var i = 0; i <= other_groupbys.length; i++) { - groupbys.push([field].concat(other_groupbys.slice(0,i))); + groupBys.push([field].concat(other_groupbys.slice(0,i))); } - - return $.when.apply(null, groupbys.map(function (groupBy) { - return self._rpc({ - model: self.modelName, - method: 'read_group', - context: self.data.context, - domain: header.domain.length ? header.domain : self.data.domain, - fields: measures, - groupBy: groupBy, - lazy: false, - }); - })).then(function () { - var data = Array.prototype.slice.call(arguments); - var datapt, attrs, j, l, row, col, cell_value, groupBys; - for (i = 0; i < data.length; i++) { - for (j = 0; j < data[i].length; j++){ - datapt = data[i][j]; + var defs = []; + if ((typeof header.count === 'object' && header.count.data) || (typeof header.count === 'number' && header.count)) { + defs = defs.concat(groupBys.map(function (groupBy) { + return self._rpc({ + model: self.modelName, + method: 'read_group', + context: self.data.context, + domain: header.domain || + self.data.domain.concat(self.data.timeRange), + fields: measures, + groupBy: groupBy, + lazy: false, + }).then(function (result) { + return ['data', result]; + }); + })); + } + if (header.comparisonCount && this.data.compare) { + defs = defs.concat(groupBys.map(function (groupBy) { + return self._rpc({ + model: self.modelName, + method: 'read_group', + context: self.data.context, + domain: header.comparisonDomain || + self.data.domain.concat(self.data.comparisonTimeRange), + fields: measures, + groupBy: groupBy, + lazy: false, + }).then(function (result) { + return ['comparisonData', result]; + }); + })); + } + return $.when.apply(null, defs).then(function () { + var results = Array.prototype.slice.call(arguments); + var data = []; + var comparisonData = []; + _.each(results, function (result) { + if (result[0] === 'data') { + data.push(result[1]); + } else { + comparisonData.push(result[1]); + } + }); + var allData = self._mergeData(data, comparisonData, groupBys); + var dataPoint, attrs, j, l, row, col, cell_value; + for (i = 0; i < allData.length; i++) { + for (j = 0; j < allData[i].length; j++){ + dataPoint = allData[i][j]; groupBys = [field].concat(other_groupbys.slice(0,i)); attrs = { - value: self._getValue(datapt, groupBys), - domain: datapt.__domain || [], - length: datapt.__count, + value: self._getValue(dataPoint, groupBys), + domain: dataPoint.__domain, + comparisonDomain: dataPoint.__comparisonDomain, + length: dataPoint.__count.data ? dataPoint.__count.data : dataPoint.__count, + comparisonLength: dataPoint.__comparisonCount|| 0, }; if (i === 0) { - row = self._makeHeader(attrs.value, attrs.domain, header.root, 0, 1, header); + row = self._makeHeader(attrs.value, attrs.domain, attrs.comparisonDomain, attrs.length, attrs.comparisonLength , header.root, 0, 1, header); } else { row = self._getHeader(attrs.value, header.root, 0, 1, header); } @@ -123,7 +153,7 @@ var PivotModel = AbstractModel.extend({ continue; } for (cell_value = {}, l=0; l < self.data.measures.length; l++) { - var _value = datapt[self.data.measures[l]]; + var _value = dataPoint[self.data.measures[l]]; if (_value instanceof Array) { // when a many2one field is used as a measure AND as // a grouped field, bad things happen. The server @@ -133,7 +163,11 @@ var PivotModel = AbstractModel.extend({ // array. Fortunately, if we group by a field, // then we can say for certain that the group contains // exactly one distinct value for that field. - _value = 1; + if (self.data.compare) { + _value = dataPoint[self.data.measures[l] + 'Aggregate']; + } else { + _value = 1; + } } cell_value[self.data.measures[l]] =_value; } @@ -155,35 +189,106 @@ var PivotModel = AbstractModel.extend({ * @returns {Object} */ exportData: function () { + var self = this; var measureNbr = this.data.measures.length; var headers = this._computeHeaders(); + if (this.data.compare) { + _.each(headers, function (headerGroup) { + _.each(headerGroup, function (header) { + header.width = header.width ? 3 * header.width : 3; + }); + }); + } var measureRow = measureNbr >= 1 ? _.last(headers) : []; var rows = this._computeRows(); - var i, j, value; + var i, j, value, values, is_bold, additionalHeaders = []; + // remove the empty headers on left side headers[0].splice(0,1); - // process measureRow - for (i = 0; i < measureRow.length; i++) { - measureRow[i].measure = this.fields[measureRow[i].measure].string; + function isBold (i, j) { + return (i === 0) || + ((self.data.main_col.width > 1) && + (j >= rows[i].values.length - measureNbr)); } - // process all rows - for (i =0, j, value; i < rows.length; i++) { - for (j = 0; j < rows[i].values.length; j++) { - value = rows[i].values[j]; - rows[i].values[j] = { - is_bold: (i === 0) || - ((this.data.main_col.width > 1) && - (j >= rows[i].values.length - measureNbr)), - value: (value === undefined) ? "" : value, - }; + + function makeValue (value, is_bold) { + return { + is_bold: is_bold, + value: (value === undefined) ? "" : value, + }; + } + + function makeMeasure (name) { + return { + is_bold: false, + measure: name + }; + } + + // process measureRow + additionalHeaders = []; + for (i = 0; i < measureRow.length; i++) { + if (this.data.compare) { + measureRow[i].title = this.fields[measureRow[i].measure].string; + measureRow[i].height = 1; + measureRow[i].expanded = true; + additionalHeaders = additionalHeaders.concat( + _.map( + [ + this.data.timeRangeDescription, + this.data.comparisonTimeRangeDescription, + 'Variation' + ], + makeMeasure + ) + ); + } else { + measureRow[i].measure = this.fields[measureRow[i].measure].string; } } - return { - headers: _.initial(headers), - measure_row: measureRow, - rows: rows, - nbr_measures: measureNbr, - }; + if (this.data.compare) { + for (i =0, j, value; i < rows.length; i++) { + values = []; + for (j = 0; j < rows[i].values.length; j++) { + value = rows[i].values[j]; + is_bold = isBold(i, j); + if (value instanceof Object) { + for (var origin in value) { + if (origin === 'variation') { + values.push(makeValue(value[origin].magnitude * 100, is_bold)); + } else { + values.push(makeValue(value[origin], is_bold)); + } + } + } else { + for (var l = 0; l < 3; l++) { + values.push(makeValue(undefined, isBold(i, j))); + } + } + } + rows[i].values = values; + } + headers.push(additionalHeaders); + return { + headers: _.initial(headers), + measure_row: additionalHeaders, + rows: rows, + nbr_measures: 3 * measureNbr, + }; + } else { + // process all rows + for (i =0, j, value; i < rows.length; i++) { + for (j = 0; j < rows[i].values.length; j++) { + rows[i].values[j] = makeValue(rows[i].values[j], isBold(i, j)); + } + } + return { + headers: _.initial(headers), + measure_row: measureRow, + rows: rows, + nbr_measures: measureNbr, + }; + } }, /** * Swap the columns and the rows. It is a synchronous operation. @@ -214,6 +319,7 @@ var PivotModel = AbstractModel.extend({ colGroupBys: this.data.main_col.groupbys, context: this.data.context, domain: this.data.domain, + compare: this.data.compare, fields: this.fields, headers: !isRaw && this._computeHeaders(), has_data: true, @@ -240,6 +346,11 @@ var PivotModel = AbstractModel.extend({ * @param {string[]} params.rowGroupBys * @param {string[]} params.colGroupBys * @param {string[]} params.measures + * @param {string[]} params.timeRange + * @param {string[]} params.comparisonTimeRange + * @param {string[]} params.timeRangeDescription + * @param {string[]} params.comparisonTimeRangeDescription + * @param {string[]} params.compare * @param {Object} params.fields * @param {string} params.default_order * @returns {Deferred} @@ -252,13 +363,19 @@ var PivotModel = AbstractModel.extend({ this.fields = params.fields; this.modelName = params.modelName; this.data = { - domain: params.domain, + domain: this.initialDomain, + timeRange: params.timeRange || [], + timeRangeDescription: params.timeRangeDescription || "", + comparisonTimeRange: params.comparisonTimeRange || [], + comparisonTimeRangeDescription: params.comparisonTimeRangeDescription || "", + compare: params.compare || false, context: _.extend({}, session.user_context, params.context), groupedBy: params.groupedBy, colGroupBys: params.context.pivot_column_groupby || params.colGroupBys, measures: this._processMeasures(params.context.pivot_measures) || params.measures, sorted_column: {}, }; + this.variationData = {}; this.defaultGroupedBy = params.groupedBy; return this._loadData().then(function () { @@ -282,6 +399,21 @@ var PivotModel = AbstractModel.extend({ this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy; this.data.measures = this._processMeasures(params.context.pivot_measures) || this.data.measures; this.defaultGroupedBy = this.data.groupedBy.length ? this.data.groupedBy : this.defaultGroupedBy; + var timeRangeMenuData = params.context.timeRangeMenuData; + if (timeRangeMenuData) { + this.data.timeRange = timeRangeMenuData.timeRange || []; + this.data.timeRangeDescription = timeRangeMenuData.timeRangeDescription || ""; + this.data.comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || []; + this.data.comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription || ""; + this.data.compare = this.data.comparisonTimeRange.length > 0; + } else { + this.data.timeRange = []; + this.data.timeRangeDescription = ""; + this.data.comparisonTimeRange = []; + this.data.comparisonTimeRangeDescription = ""; + this.data.compare = false; + this.data.context = _.omit(this.data.context, 'timeRangeMenuData'); + } } if ('domain' in params) { this.data.domain = params.domain; @@ -525,8 +657,8 @@ var PivotModel = AbstractModel.extend({ * @returns {string} */ _getNumberedValue: function (value, field) { - var id= value[0]; - var name= value[1]; + var id = value[0]; + var name = value[1]; this.numbering[field] = this.numbering[field] || {}; this.numbering[field][name] = this.numbering[field][name] || {}; var numbers = this.numbering[field][name]; @@ -534,15 +666,15 @@ var PivotModel = AbstractModel.extend({ return name + (numbers[id] > 1 ? " (" + numbers[id] + ")" : ""); }, /** - * @param {any} datapt + * @param {any} dataPoint * @param {any} fields * @returns {string[]} */ - _getValue: function (datapt, fields) { + _getValue: function (dataPoint, fields) { var result = []; var value; for (var i = 0; i < fields.length; i++) { - value = this._sanitizeValue(datapt[fields[i]],fields[i]); + value = this._sanitizeValue(dataPoint[fields[i]],fields[i]); result.push(value); } return result; @@ -569,35 +701,67 @@ var PivotModel = AbstractModel.extend({ groupBys.push(rowGroupBys.slice(0,i).concat(colGroupBys.slice(0,j))); } } - - return this._loadDataDropPrevious.add($.when.apply(null, groupBys.map(function (groupBy) { + var defs = groupBys.map(function (groupBy) { return self._rpc({ model: self.modelName, method: 'read_group', context: self.data.context, - domain: self.data.domain, + domain: self.data.domain.concat(self.data.timeRange), fields: measures, groupBy: groupBy, lazy: false, + }).then(function (result) { + return ['data', result]; }); - }))).then(function () { - var data = Array.prototype.slice.call(arguments); - if (data[0][0].__count === 0) { + }); + if (this.data.compare) { + defs = defs.concat(groupBys.map(function (groupBy) { + return self._rpc({ + model: self.modelName, + method: 'read_group', + context: self.data.context, + domain: self.data.domain.concat(self.data.comparisonTimeRange), + fields: measures, + groupBy: groupBy, + lazy: false, + }).then(function (result) { + return ['comparisonData', result]; + }); + })); + } + + return this._loadDataDropPrevious.add($.when.apply(null, defs)).then(function () { + var results = Array.prototype.slice.call(arguments); + var data = []; + var comparisonData = []; + _.each(results, function (result) { + if (result[0] === 'data') { + data.push(result[1]); + } else { + comparisonData.push(result[1]); + } + }); + var allData = self._mergeData(data, comparisonData, groupBys); + if (allData[0][0].__count === 0 && allData[0][0].__comparisonCount === 0) { self.data.has_data = false; } - self._prepareData(data); + self._prepareData(allData); }); }, /** * @param {any} value * @param {any} domain + * @param {any} comparisonDomain * @param {any} root + * @param {any} count + * @param {any} comparisonCount * @param {any} i * @param {any} j * @param {any} parent_header * @returns {Object} */ - _makeHeader: function (value, domain, root, i, j, parent_header) { + + _makeHeader: function (value, domain, comparisonDomain, count, comparisonCount, root, i, j, parent_header) { var total = _t("Total"); var title = value.length ? value[value.length - 1] : total; var path, parent; @@ -611,7 +775,10 @@ var PivotModel = AbstractModel.extend({ var header = { id: utils.generateID(), expanded: false, - domain: domain || [], + domain: domain, + comparisonDomain: comparisonDomain, + count: count, + comparisonCount: comparisonCount, children: [], path: value.length ? parent.path.concat(title) : [title] }; @@ -623,12 +790,133 @@ var PivotModel = AbstractModel.extend({ } return header; }, + /** + * @param {Object} data + * @param {Object} comparisonData + * @param {(string[])[]} groupBys + * @returns {Object} + */ + _mergeData: function (data, comparisonData, groupBys) { + if (!this.data.compare) { + return data; + } + var allData = []; + var dataPoints; + var value, groupIdentifier, dataPoint, m, measureName, measureValue, measureComparisonValue; + for (var index = 0; index < groupBys.length; index++) { + dataPoints = {}; + if (data.length) { + for (var k = 0; k < data[index].length; k++) { + dataPoint = data[index][k]; + value = this._getValue(dataPoint, groupBys[index]); + groupIdentifier = value.join(); + for (m=0; m < this.data.measures.length; m++) { + measureName = this.data.measures[m]; + measureValue = dataPoint[measureName]; + if (typeof measureValue === 'boolean') { + measureValue = measureValue ? 1 : 0; + } + if (!(measureValue instanceof Array) && measureName !== '__count') { + dataPoint[measureName] = { + data: measureValue, + comparisonData : 0, + variation: computeVariation(measureValue, 0), + }; + } + if (measureValue instanceof Array) { + dataPoint[measureName + 'Aggregate'] = { + data: 1, + comparisonData: 0, + variation: computeVariation(1, 0), + }; + } + } + dataPoint.__count = { + data: dataPoint.__count, + comparisonData: 0, + variation: computeVariation(dataPoint.__count, 0) + }; + dataPoints[groupIdentifier] = dataPoint; + } + } + if (comparisonData.length) { + for (var l = 0; l < comparisonData[index].length; l++) { + dataPoint = comparisonData[index][l]; + value = this._getValue(dataPoint, groupBys[index]); + groupIdentifier = value.join(); + if (!dataPoints[groupIdentifier]) { + for (m=0; m < this.data.measures.length; m++) { + measureName = this.data.measures[m]; + measureComparisonValue = dataPoint[measureName]; + if (typeof(measureComparisonValue) === 'boolean') { + measureComparisonValue = measureComparisonValue ? 1 : 0; + } + if (!(measureComparisonValue instanceof Array) && measureName !== '__count') { + dataPoint[measureName] = { + data: 0, + comparisonData: measureComparisonValue, + variation: computeVariation(0, measureComparisonValue), + }; + } + if (measureComparisonValue instanceof Array) { + dataPoint[measureName + 'Aggregate'] = { + data: 0, + comparisonData: 1, + variation: computeVariation(0,1), + }; + } + } + dataPoint.__count = { + data: 0, + comparisonData: dataPoint.__count, + variation: computeVariation(0, dataPoint.__count) + }; + dataPoint.__comparisonCount = dataPoint.__count; + dataPoint.__comparisonDomain = dataPoint.__domain; + dataPoints[groupIdentifier] = _.omit(dataPoint, '__domain'); + } else { + for (m=0; m < this.data.measures.length; m++) { + measureName = this.data.measures[m]; + measureComparisonValue = dataPoint[measureName]; + if (typeof(measureComparisonValue) === 'boolean') { + measureComparisonValue = measureComparisonValue ? 1 : 0; + } + if (!(measureComparisonValue instanceof Array) && measureName !== '__count') { + dataPoints[groupIdentifier][measureName].comparisonData = measureComparisonValue; + dataPoints[groupIdentifier][measureName].variation = computeVariation( + dataPoints[groupIdentifier][measureName].data, + measureComparisonValue + ); + } + if (measureComparisonValue instanceof Array) { + dataPoints[groupIdentifier][measureName + 'Aggregate'].comparisonData = 1; + dataPoints[groupIdentifier][measureName].variation = computeVariation( + dataPoints[groupIdentifier][measureName].data, + 1 + ); + + } + } + dataPoints[groupIdentifier].__count.comparisonData = dataPoint.__count; + dataPoints[groupIdentifier].__count.variation = computeVariation( + dataPoints[groupIdentifier].__count.data, + dataPoint.__count + ); + dataPoints[groupIdentifier].__comparisonCount = dataPoint.__count; + dataPoints[groupIdentifier].__comparisonDomain = dataPoint.__domain; + } + } + } + allData.push(_.values(dataPoints)); + } + return allData; + }, /** * @param {Object} data */ _prepareData: function (data) { var self = this; - _.extend(self.data, { + _.extend(this.data, { main_row: {}, main_col: {}, headers: {}, @@ -638,7 +926,7 @@ var PivotModel = AbstractModel.extend({ var index = 0; var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys; var colGroupBys = this.data.colGroupBys; - var datapt, row, col, attrs, cell_value; + var dataPoint, row, col, attrs, cell_value; var main_row_header, main_col_header; var groupBys; var m; @@ -647,32 +935,35 @@ var PivotModel = AbstractModel.extend({ for (var i = 0; i < rowGroupBys.length + 1; i++) { for (var j = 0; j < colGroupBys.length + 1; j++) { for (var k = 0; k < data[index].length; k++) { - datapt = data[index][k]; + dataPoint = data[index][k]; groupBys = rowGroupBys.slice(0,i).concat(colGroupBys.slice(0,j)); attrs = { - value: self._getValue(datapt, groupBys), - domain: datapt.__domain || [], - length: datapt.__count, + // value could be named 'groupIdentifier' + value: self._getValue(dataPoint, groupBys), + domain: dataPoint.__domain, + comparisonDomain: dataPoint.__comparisonDomain, + length: dataPoint.__count.data ? dataPoint.__count.data : dataPoint.__count, + comparisonLength: dataPoint.__count.comparisonData || 0 }; if (j === 0) { - row = this._makeHeader(attrs.value, attrs.domain, main_row_header, 0, i); + row = this._makeHeader(attrs.value, attrs.domain, attrs.comparisonDomain, attrs.length, attrs.comparisonLength, main_row_header, 0, i); } else { row = this._getHeader(attrs.value, main_row_header, 0, i); } if (i === 0) { - col = this._makeHeader(attrs.value, attrs.domain, main_col_header, i, i+j); + col = this._makeHeader(attrs.value, attrs.domain, attrs.comparisonDomain, attrs.length, attrs.comparisonLength, main_col_header, i, i+j); } else { col = this._getHeader(attrs.value, main_col_header, i, i+j); } if (i + j === 0) { - this.data.has_data = attrs.length > 0; + this.data.has_data = attrs.length > 0 || attrs.comparisonLength > 0; main_row_header = row; main_col_header = col; } if (!this.data.cells[row.id]) this.data.cells[row.id] = []; for (cell_value = {}, m=0; m < this.data.measures.length; m++) { - var _value = datapt[this.data.measures[m]]; + var _value = dataPoint[this.data.measures[m]]; if (_value instanceof Array) { // when a many2one field is used as a measure AND as // a grouped field, bad things happen. The server @@ -682,7 +973,11 @@ var PivotModel = AbstractModel.extend({ // array. Fortunately, if we group by a field, // then we can say for certain that the group contains // exactly one distinct value for that field. - _value = 1; + if (this.data.compare) { + _value = dataPoint[this.data.measures[m] + 'Aggregate']; + } else { + _value = 1; + } } cell_value[this.data.measures[m]] = _value; } 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 9312b27f1ef..370c0c009d5 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -14,6 +14,44 @@ var PivotRenderer = AbstractRenderer.extend({ 'hover td': '_onTdHover', }), + /** + * @overide + * + * @param {Widget} parent + * @param {Object} state + * @param {Object} params + */ + init: function (parent, state, params) { + this._super.apply(this, arguments); + this.compare = state.compare; + this.timeRangeDescription = params.timeRangeDescription; + this.comparisonTimeRangeDescription = params.comparisonTimeRangeDescription; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @param {Object} state + * @param {Object} params + */ + updateState: function (state, params) { + if (params.context !== undefined) { + var timeRangeMenuData = params.context.timeRangeMenuData; + if (timeRangeMenuData) { + this.timeRangeDescription = timeRangeMenuData.timeRangeDescription; + this.comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription; + } else { + this.timeRangeDescription = undefined; + this.comparisonTimeRangeDescription = undefined; + } + } + this.compare = state.compare; + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -52,10 +90,17 @@ var PivotRenderer = AbstractRenderer.extend({ var nbrCols = (this.state.mainColWidth === 1) ? nbr_measures : (this.state.mainColWidth + 1) * nbr_measures; - for (var i=0; i < nbrCols + 1; i++) { - $table.prepend($('')); + var i; + if (this.compare) { + for (i=0; i < 3 * nbrCols + 1; i++) { + $table.prepend($('')); + } + } else { + for (i=0; i < nbrCols + 1; i++) { + $table.prepend($('')); + } } - this._renderHeaders($thead, this.state.headers); + this._renderHeaders($thead, this.state.headers, nbrCols); this._renderRows($tbody, this.state.rows); // todo: make sure the next line does something $table.find('.o_pivot_header_cell_opened,.o_pivot_header_cell_closed').tooltip(); @@ -67,7 +112,7 @@ var PivotRenderer = AbstractRenderer.extend({ * @param {jQueryElement} $thead * @param {jQueryElement} headers */ - _renderHeaders: function ($thead, headers) { + _renderHeaders: function ($thead, headers, nbrCols) { var self = this; var i, j, cell, $row, $cell; @@ -79,10 +124,17 @@ var PivotRenderer = AbstractRenderer.extend({ $row = $(''); for (j = 0; j < headers[i].length; j++) { cell = headers[i][j]; - $cell = $('') - .text(cell.title) - .attr('rowspan', cell.height) - .attr('colspan', cell.width); + if (i == 0 && j == 0) { + $cell = $('') + .text(cell.title) + .attr('rowspan', cell.height + 1) + .attr('colspan', cell.width); + } else { + $cell = $('') + .text(cell.title) + .attr('rowspan', cell.height) + .attr('colspan', (cell.width && self.compare) ? 3 * cell.width : cell.width || (self.compare && cell.measure && 3)); + } if (i > 0) { $cell.attr('title', groupbyLabels[i-1]); } @@ -94,7 +146,7 @@ var PivotRenderer = AbstractRenderer.extend({ $cell.addClass('o_pivot_measure_row text-muted') .text(this.state.fields[cell.measure].string); $cell.data('id', cell.id).data('measure', cell.measure); - if (cell.id === this.state.sortedColumn.id && cell.measure === this.state.sortedColumn.measure) { + if (!this.compare && cell.id === this.state.sortedColumn.id && cell.measure === this.state.sortedColumn.measure) { $cell.addClass('o_pivot_measure_row_sorted_' + this.state.sortedColumn.order); if (this.state.sortedColumn.order == 'asc') { $cell.attr('aria-sorted', 'ascending'); @@ -112,6 +164,18 @@ var PivotRenderer = AbstractRenderer.extend({ } $thead.append($row); } + if (this.compare) { + var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, 'Variation']; + $row = $(''); + for (i = 0; i < 3 * nbrCols; i++) { + $cell = $('') + .text(colLabels[i % 3]) + .attr('rowspan', 1) + .attr('colspan', 1); + $row.append($cell); + } + $thead.append($row); + } }, /** * @private @@ -120,7 +184,7 @@ var PivotRenderer = AbstractRenderer.extend({ */ _renderRows: function ($tbody, rows) { var self = this; - var i, j, value, measure, name, $row, $cell, $header; + var i, j, value, measure, name, formatter, $row, $cell, $header; var nbrMeasures = this.state.measures.length; var length = rows[0].values.length; var shouldDisplayTotal = this.state.mainColWidth > 1; @@ -143,23 +207,67 @@ var PivotRenderer = AbstractRenderer.extend({ $header.appendTo($row); for (j = 0; j < length; j++) { value = rows[i].values[j]; - if (value !== undefined) { - name = this.state.measures[j % nbrMeasures]; - measure = this.state.fields[name]; - value = field_utils.format[measureTypes[j % nbrMeasures]](value, measure); - } - $cell = $('') - .data('id', rows[i].id) - .data('col_id', rows[i].col_ids[Math.floor(j / nbrMeasures)]) - .toggleClass('o_empty', !value) - .text(value) - .addClass('o_pivot_cell_value text-right'); - if (((j >= length - this.state.measures.length) && shouldDisplayTotal) || i === 0){ - $cell.css('font-weight', 'bold'); - } - $row.append($cell); + name = this.state.measures[j % nbrMeasures]; + formatter = field_utils.format[measureTypes[j % nbrMeasures]]; + measure = this.state.fields[name]; + if (this.compare) { + if (value instanceof Object) { + for (var origin in value) { + $cell = $('') + .data('id', rows[i].id) + .data('col_id', rows[i].col_ids[Math.floor(j / nbrMeasures)]) + .data('type' , origin) + .toggleClass('o_empty', false) + .addClass('o_pivot_cell_value text-right'); + if (origin === 'data') { + $cell.append($('
', {class: 'o_value'}).html(formatter( + value[origin], + measure + ))); + } else if (origin === 'comparisonData') { + $cell.append($('
', {class: 'o_comparison_value'}).html(formatter( + value[origin], + measure + ))); + } else { + $cell.append($('
', {class: 'o_variation' + value[origin].signClass}).html( + field_utils.format.percentage( + value[origin].magnitude, + measure + ) + )); + } + if (((j >= length - this.state.measures.length) && shouldDisplayTotal) || i === 0){ + $cell.css('font-weight', 'bold'); + } + $cell.toggleClass('d-none d-md-table-cell', j < length - nbrMeasures); + $row.append($cell); + } + } else { + for (var l=0; l < 3; l++) { + $cell = $('') + .data('id', rows[i].id) + .toggleClass('o_empty', true) + .addClass('o_pivot_cell_value text-right'); + $row.append($cell); + } + } + } else { + $cell = $('') + .data('id', rows[i].id) + .data('col_id', rows[i].col_ids[Math.floor(j / nbrMeasures)]) + .toggleClass('o_empty', !value) + .addClass('o_pivot_cell_value text-right'); + if (value !== undefined) { + $cell.append($('
', {class: 'o_value'}).html(formatter(value, measure))); + } + if (((j >= length - this.state.measures.length) && shouldDisplayTotal) || i === 0){ + $cell.css('font-weight', 'bold'); + } + $row.append($cell); - $cell.toggleClass('d-none d-md-table-cell', j < length - nbrMeasures); + $cell.toggleClass('d-none d-md-table-cell', j < length - nbrMeasures); + } } $tbody.append($row); } 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 9ed3835a39a..0bd973b4468 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -28,6 +28,7 @@ var PivotView = AbstractView.extend({ Renderer: PivotRenderer, }, viewType: 'pivot', + enableTimeRangeMenu: 'true', /** * @override * @param {Object} params @@ -36,8 +37,6 @@ var PivotView = AbstractView.extend({ var self = this; this._super.apply(this, arguments); - var self = this; - var activeMeasures = []; var colGroupBys = []; var rowGroupBys = []; diff --git a/addons/web/static/src/scss/pivot_view.scss b/addons/web/static/src/scss/pivot_view.scss index ae0e97fa3dd..1006e4ada8f 100644 --- a/addons/web/static/src/scss/pivot_view.scss +++ b/addons/web/static/src/scss/pivot_view.scss @@ -7,6 +7,21 @@ .o_pivot_cell_value:hover { color: $o-brand-primary; cursor: pointer; + + } + } + .o_pivot_cell_value { + font-size: 1em; + .o_comparison { + font-size: 0.61em; + } + .o_variation { + &.o_positive { + color: green; + } + &.o_negative { + color: red; + } } } diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 6e034a19a79..66fb366e055 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -5,6 +5,9 @@ var core = require('web.core'); var PivotView = require('web.PivotView'); var testUtils = require('web.test_utils'); +var createActionManager = testUtils.createActionManager; +var patchDate = testUtils.patchDate; + var _t = core._t; var createView = testUtils.createView; @@ -15,7 +18,7 @@ QUnit.module('Views', { fields: { foo: {string: "Foo", type: "integer"}, bar: {string: "bar", type: "boolean"}, - date: {string: "Date", type: "date", store: true}, + date: {string: "Date", type: "date", store: true, sortable: true}, product_id: {string: "Product", type: "many2one", relation: 'product', store: true}, other_product_id: {string: "Other Product", type: "many2one", relation: 'product', store: true}, non_stored_m2o: {string: "Non Stored M2O", type: "many2one", relation: 'product'}, @@ -1194,7 +1197,7 @@ QUnit.module('Views', { '' + '' + '', - }) + }); assert.strictEqual(pivot.$buttons.find('.o_pivot_measures_list .dropdown-item:first').data('field'), 'bouh', "Bouh should be the first measure"); @@ -1221,4 +1224,53 @@ QUnit.module('Views', { pivot.destroy(); }); -});}); + + QUnit.test('rendering of pivot view with comparison active', function (assert) { + assert.expect(1); + + this.data.partner.records[0].date = '2016-12-15'; + this.data.partner.records[1].date = '2016-12-17'; + this.data.partner.records[2].date = '2016-11-22'; + this.data.partner.records[3].date = '2016-11-03'; + + this.data.partner.fields.company_type = {string: "Company Type", type: "selection", selection: [["company", "Company"], ["individual", "Individual"]], searchable: true, store: true, sortable: true}; + + this.data.partner.records[0].company_type = 'company'; + this.data.partner.records[1].company_type = 'individual'; + this.data.partner.records[2].company_type = 'company'; + this.data.partner.records[3].company_type = 'individual'; + + + var unpatchDate = patchDate(2016, 11, 20, 1, 0, 0); + + + // create an action manager to test the interactions with the search view + var actionManager = createActionManager({ + data: this.data, + archs: { + 'partner,false,pivot': '' + + '' + + '' + + '', + 'partner,false,search': '', + }, + }); + + actionManager.doAction({ + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'pivot']], + }); + + $('.o_time_range_menu_button').click(); + $('.o_time_range_menu .custom-control-label').click(); + $('.o_time_range_menu .o_apply_range').click(); + + // Test to modify and extend + assert.strictEqual($('.o_pivot p.o_view_nocontent_empty_folder').length, 1); + + unpatchDate(); + actionManager.destroy(); + }); +}); +});