2 /* jshint undef: false */
7 var _lt = openerp.web._lt;
8 var _t = openerp.web._t;
10 // PivotTable requires a call to update_data after initialization
11 openerp.web_graph.PivotTable = openerp.web.Class.extend({
13 init: function (model, domain, fields, options) {
16 this.context = options.context;
18 this.updating = false;
21 this.fields.__count = {type: 'integer', string:_t('Quantity')};
22 this.measures = options.measures || [];
23 this.rows = { groupby: options.row_groupby, headers: null };
24 this.cols = { groupby: options.col_groupby, headers: null };
27 // ----------------------------------------------------------------------
28 // Configuration methods
29 // ----------------------------------------------------------------------
30 // this.measures: list of measure [measure], measure = {field: _, string: _, type: _}
31 // this.rows.groupby, this.cols.groupby : list of groupbys used for describing rows (...),
32 // a groupby is also {field:_, string:_, type:_}
33 // If its type is date/datetime, field can have the corresponding interval in its description,
34 // for example 'create_date:week'.
35 set_measures: function (measures) {
36 this.measures = measures;
37 return this.update_data();
40 toggle_measure: function (measure) {
41 var current_measure = _.findWhere(this.measures, measure);
42 if (current_measure) { // remove current_measure
43 var index = this.measures.indexOf(current_measure);
44 this.measures = _.without(this.measures, current_measure);
45 if (this.measures.length === 0) {
48 _.each(this.cells, function (cell) {
49 cell.values.splice(index, 1);
52 return $.Deferred().resolve();
53 } else { // add a new measure
54 this.measures.push(measure);
55 return this.update_data();
59 set: function (domain, row_groupby, col_groupby, measures_groupby) {
62 return this.updating.then(function () {
63 self.updating = false;
64 return self.set(domain, row_groupby, col_groupby, measures_groupby);
67 var row_gb_changed = !_.isEqual(row_groupby, this.rows.groupby),
68 col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby),
69 measures_gb_changed = !_.isEqual(measures_groupby, this.measures);
72 this.rows.groupby = row_groupby;
73 this.cols.groupby = col_groupby;
75 if (measures_groupby.length) { this.measures = measures_groupby; }
77 if (row_gb_changed) { this.rows.headers = null; }
78 if (col_gb_changed) { this.cols.headers = null; }
79 if (measures_gb_changed && measures_groupby.length) { this.set_measures(measures_groupby); }
81 return this.update_data();
84 // ----------------------------------------------------------------------
85 // Cells manipulation methods
86 // ----------------------------------------------------------------------
87 // cells are objects {x:_, y:_, values:_} where x < y and values is an array
88 // of values (one for each measure). The condition x < y might look
89 // unnecessary, but it makes the rest of the code simpler: headers
90 // don't krow if they are rows or cols, they just know their id, so
91 // it is useful that a call get_values(id1, id2) is the same as get_values(id2, id1)
92 add_cell : function (id1, id2, values) {
93 this.cells.push({x: Math.min(id1, id2), y: Math.max(id1, id2), values: values});
96 get_values: function (id1, id2, default_values) {
97 var cells = this.cells,
98 x = Math.min(id1, id2),
99 y = Math.max(id1, id2);
100 for (var i = 0; i < cells.length; i++) {
101 if (cells[i].x == x && cells[i].y == y) {
102 return cells[i].values;
105 return (default_values || new Array(this.measures.length));
108 // ----------------------------------------------------------------------
109 // Headers/Rows/Cols manipulation methods
110 // ----------------------------------------------------------------------
111 // this.rows.headers, this.cols.headers = [header] describe the tree structure
112 // of rows/cols. Headers are objects
114 // id:_, (unique id obviously)
115 // path: [...], (array of all parents title, with its own title at the end)
116 // title:_, (name of the row/col)
117 // children:[_], (subrows or sub cols of this row/col)
118 // domain:_, (domain of data in this row/col)
119 // root:_ (ref to this.rows or this.cols corresponding to the header)
120 // expanded:_ (boolean, true if it has been expanded)
122 is_row: function (id) {
123 return !!_.findWhere(this.rows.headers, {id:id});
126 is_col: function (id) {
127 return !!_.findWhere(this.cols.headers, {id:id});
130 get_header: function (id) {
131 return _.findWhere(this.rows.headers.concat(this.cols.headers), {id:id});
134 _get_headers_with_depth: function (headers, depth) {
135 return _.filter(headers, function (header) {
136 return header.path.length === depth;
140 // return all columns with a path length of 'depth'
141 get_cols_with_depth: function (depth) {
142 return this._get_headers_with_depth(this.cols.headers, depth);
145 // return all rows with a path length of 'depth'
146 get_rows_with_depth: function (depth) {
147 return this._get_headers_with_depth(this.rows.headers, depth);
150 get_ancestor_leaves: function (header) {
151 return _.where(this.get_ancestors_and_self(header), {expanded:false});
154 // return all non expanded rows
155 get_rows_leaves: function () {
156 return _.where(this.rows.headers, {expanded:false});
159 // return all non expanded cols
160 get_cols_leaves: function () {
161 return _.where(this.cols.headers, {expanded:false});
164 get_ancestors: function (header) {
166 if (!header.children) return [];
167 return [].concat.apply([], _.map(header.children, function (c) {
168 return self.get_ancestors_and_self(c);
172 get_ancestors_and_self: function (header) {
174 return [].concat.apply([header], _.map(header.children, function (c) {
175 return self.get_ancestors_and_self(c);
179 get_total: function (header) {
180 return (header) ? this.get_values(header.id, this.get_other_root(header).headers[0].id)
181 : this.get_values(this.rows.headers[0].id, this.cols.headers[0].id);
184 get_other_root: function (header) {
185 return (header.root === this.rows) ? this.cols : this.rows;
188 main_row: function () { return this.rows.headers[0]; },
190 main_col: function () { return this.cols.headers[0]; },
192 // ----------------------------------------------------------------------
193 // Table manipulation methods : fold/expand/swap
194 // ----------------------------------------------------------------------
195 // return true if the folding changed the groupbys, false if not
196 fold: function (header) {
197 var ancestors = this.get_ancestors(header),
198 removed_ids = _.pluck(ancestors, 'id');
200 header.root.headers = _.difference(header.root.headers, ancestors);
201 header.children = [];
202 header.expanded = false;
204 this.cells = _.reject(this.cells, function (cell) {
205 return (_.contains(removed_ids, cell.x) || _.contains(removed_ids, cell.y));
208 var new_groupby_length = _.max(_.pluck(_.pluck(header.root.headers, 'path'), 'length'));
209 if (new_groupby_length < header.root.groupby.length) {
210 header.root.groupby.splice(new_groupby_length);
216 fold_with_depth: function (root, depth) {
218 _.each(this._get_headers_with_depth(root.headers, depth), function (header) {
223 expand_all: function () {
224 this.rows.headers = null;
225 this.cols.headers = null;
226 return this.update_data();
229 expand: function (header_id, groupby) {
231 header = this.get_header(header_id),
232 other_root = this.get_other_root(header),
233 this_gb = [groupby.field],
234 other_gbs = _.pluck(other_root.groupby, 'field');
236 if (header.path.length === header.root.groupby.length) {
237 header.root.groupby.push(groupby);
239 return this.perform_requests(this_gb, other_gbs, header.domain).then(function () {
240 var data = Array.prototype.slice.call(arguments).slice(other_gbs.length + 1);
241 _.each(data, function (data_pt) {
242 self.make_headers_and_cell(
243 data_pt, header.root.headers, other_root.headers, 1, header.path, true);
245 header.expanded = true;
246 header.children.forEach(function (child) {
247 child.expanded = false;
248 child.root = header.root;
253 swap_axis: function () {
254 var temp = this.rows;
255 this.rows = this.cols;
259 // ----------------------------------------------------------------------
260 // Data updating methods
261 // ----------------------------------------------------------------------
262 // update_data will try to preserve the expand/not expanded status of each
263 // column/row. If you want to expand all, then set this.cols.headers/this.rows.headers
264 // to null before calling update_data.
265 update_data: function () {
267 this.updating = this.perform_requests().then (function () {
268 var data = Array.prototype.slice.call(arguments);
269 self.no_data = !data[0].length;
273 var row_headers = [],
277 var dim_col = self.cols.groupby.length,
280 for (i = 0; i < self.rows.groupby.length + 1; i++) {
281 for (j = 0; j < dim_col + 1; j++) {
282 index = i*(dim_col + 1) + j;
283 self.make_headers_and_cell(data[index], row_headers, col_headers, i);
286 self.set_headers(row_headers, self.rows);
287 self.set_headers(col_headers, self.cols);
289 return this.updating;
292 make_headers_and_cell: function (data_pts, row_headers, col_headers, index, prefix, expand) {
294 data_pts.forEach(function (data_pt) {
295 var row_value = (prefix || []).concat(data_pt.attributes.value.slice(0,index));
296 var col_value = data_pt.attributes.value.slice(index);
298 if (expand && !_.find(col_headers, function (hdr) {return self.isEqual(col_value, hdr.path);})) {
301 var row = self.find_or_create_header(row_headers, row_value, data_pt);
302 var col = self.find_or_create_header(col_headers, col_value, data_pt);
304 var cell_value = _.map(self.measures, function (m) {
305 return data_pt.attributes.aggregates[m.field];
308 x: Math.min(row.id, col.id),
309 y: Math.max(row.id, col.id),
315 make_header: function (values) {
327 find_or_create_header: function (headers, path, data_pt) {
329 var hdr = _.find(headers, function (header) {
330 return self.isEqual(path, header.path);
336 hdr = this.make_header({title: _t('Total')});
340 hdr = this.make_header({
342 domain:data_pt.model._domain,
343 title: _t(_.last(path))
345 var parent = _.find(headers, function (header) {
346 return self.isEqual(header.path, _.initial(path, 1));
349 var previous = parent.children.length ? _.last(parent.children) : parent;
350 headers.splice(headers.indexOf(previous) + 1, 0, hdr);
351 parent.children.push(hdr);
355 perform_requests: function (group1, group2, domain) {
358 row_gbs = _.pluck(this.rows.groupby, 'field'),
359 col_gbs = _.pluck(this.cols.groupby, 'field'),
360 field_list = row_gbs.concat(col_gbs, _.pluck(this.measures, 'field')),
361 fields = field_list.map(function (f) { return self.raw_field(f); });
363 group1 = group1 || row_gbs;
364 group2 = group2 || col_gbs;
367 for (i = 0; i < group1.length + 1; i++) {
368 for (j = 0; j < group2.length + 1; j++) {
369 groupbys = group1.slice(0,i).concat(group2.slice(0,j));
370 requests.push(self.get_groups(groupbys, fields, domain || self.domain));
373 return $.when.apply(null, requests);
376 // set the 'expanded' status of new_headers more or less like root.headers, with root as root
377 set_headers: function(new_headers, root) {
380 _.each(root.headers, function (header) {
381 var corresponding_header = _.find(new_headers, function (h) {
382 return self.isEqual(h.path, header.path);
384 if (corresponding_header && header.expanded) {
385 corresponding_header.expanded = true;
386 _.each(corresponding_header.children, function (c) {
390 if (corresponding_header && (!header.expanded)) {
391 corresponding_header.expanded = false;
392 corresponding_header.children = [];
395 var updated_headers = _.filter(new_headers, function (header) {
396 return (header.expanded !== undefined);
398 _.each(updated_headers, function (header) {
401 root.headers = updated_headers;
403 root.headers = new_headers;
404 _.each(root.headers, function (header) {
406 header.expanded = (header.children.length > 0);
412 get_groups: function (groupbys, fields, domain) {
414 return this.model.query(_.without(fields, '__count'))
416 .context(this.context)
419 .then(function (groups) {
420 return groups.filter(function (group) {
421 return group.attributes.length > 0;
422 }).map(function (group) {
423 var attrs = group.attributes,
424 grouped_on = attrs.grouped_on instanceof Array ? attrs.grouped_on : [attrs.grouped_on],
425 raw_grouped_on = grouped_on.map(function (f) {
426 return self.raw_field(f);
428 if (grouped_on.length === 1) {
429 attrs.value = [attrs.value];
431 attrs.value = _.range(grouped_on.length).map(function (i) {
432 var grp = grouped_on[i],
433 field = self.fields[grp];
434 if (attrs.value[i] === false) {
435 return _t('Undefined');
436 } else if (attrs.value[i] instanceof Array) {
437 return attrs.value[i][1];
438 } else if (field && field.type === 'selection') {
439 var selected = _.where(field.selection, {0: attrs.value[i]})[0];
440 return selected ? selected[1] : attrs.value[i];
442 return attrs.value[i];
444 attrs.aggregates.__count = group.attributes.length;
445 attrs.grouped_on = raw_grouped_on;
451 // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id
452 raw_field: function (field) {
453 return field.split(':')[0];
456 isEqual: function (path1, path2) {
457 if (path1.length !== path2.length) { return false; }
458 for (var i = 0; i < path1.length; i++) {
459 if (path1[i] !== path2[i]) {