[IMP] web: comparisons in pivot view

A previous commit [333ac27] has introduced a new widget TimeRangeMenu
that allows to create two sets of data to compare relatively
to a given date or datetime field.

This commit allows the pivot view to render appropriately two
sets of data in a unique table. In comparison mode, instead of
one column, three columns are created. One contains the
aggregates from the data of interest, one contains the
aggregates from the comparison data, and the last one contains
information on the data variation (expressed in percentage).

Task Id: 1835644
This commit is contained in:
Adrien Dieudonne
2018-08-14 12:37:25 +02:00
committed by Mathieu Duckerts-Antoine
parent 333ac27cd5
commit d4461cd434
6 changed files with 585 additions and 109 deletions
@@ -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_');
});
@@ -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;
}
@@ -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($('<col>'));
var i;
if (this.compare) {
for (i=0; i < 3 * nbrCols + 1; i++) {
$table.prepend($('<col>'));
}
} else {
for (i=0; i < nbrCols + 1; i++) {
$table.prepend($('<col>'));
}
}
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 = $('<tr>');
for (j = 0; j < headers[i].length; j++) {
cell = headers[i][j];
$cell = $('<th>')
.text(cell.title)
.attr('rowspan', cell.height)
.attr('colspan', cell.width);
if (i == 0 && j == 0) {
$cell = $('<th>')
.text(cell.title)
.attr('rowspan', cell.height + 1)
.attr('colspan', cell.width);
} else {
$cell = $('<th>')
.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 = $('<tr>');
for (i = 0; i < 3 * nbrCols; i++) {
$cell = $('<th>')
.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 = $('<td>')
.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 = $('<td>')
.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($('<div>', {class: 'o_value'}).html(formatter(
value[origin],
measure
)));
} else if (origin === 'comparisonData') {
$cell.append($('<div>', {class: 'o_comparison_value'}).html(formatter(
value[origin],
measure
)));
} else {
$cell.append($('<div>', {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 = $('<td>')
.data('id', rows[i].id)
.toggleClass('o_empty', true)
.addClass('o_pivot_cell_value text-right');
$row.append($cell);
}
}
} else {
$cell = $('<td>')
.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($('<div>', {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);
}
@@ -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 = [];
@@ -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;
}
}
}
+55 -3
View File
@@ -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', {
'<field name="foo" type="measure"/>' +
'<field name="bouh" type="measure"/>' +
'</pivot>',
})
});
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': '<pivot>' +
'<field name="date" interval="month" type="col"/>' +
'<field name="foo" type="measure"/>' +
'</pivot>',
'partner,false,search': '<search></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();
});
});
});