[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:
committed by
Mathieu Duckerts-Antoine
parent
333ac27cd5
commit
d4461cd434
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user