[IMP] website: google maps url
[odoo/odoo.git] / addons / web_graph / static / src / js / pivot_table.js
1
2 /* jshint undef: false  */
3
4 (function () {
5 'use strict';
6
7 var _lt = openerp.web._lt;
8 var _t = openerp.web._t;
9
10 //  PivotTable requires a call to update_data after initialization
11 openerp.web_graph.PivotTable = openerp.web.Class.extend({
12
13         init: function (model, domain, fields, options) {
14                 this.cells = [];
15                 this.domain = domain;
16         this.context = options.context;
17                 this.no_data = true;
18                 this.updating = false;
19                 this.model = model;
20                 this.fields = fields;
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 };
25         },
26
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();
38         },
39
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) {
46                 this.no_data = true;
47             } else {
48                 _.each(this.cells, function (cell) {
49                     cell.values.splice(index, 1);
50                 });
51             }
52             return $.Deferred().resolve();
53         } else {  // add a new measure
54             this.measures.push(measure);
55             return this.update_data();
56         }
57         },
58
59     set: function (domain, row_groupby, col_groupby, measures_groupby) {
60         var self = this;
61         if (this.updating) {
62             return this.updating.then(function () {
63                 self.updating = false;
64                 return self.set(domain, row_groupby, col_groupby, measures_groupby);
65             });
66         }
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);
70
71         this.domain = domain;
72         this.rows.groupby = row_groupby;
73         this.cols.groupby = col_groupby;
74
75                 if (measures_groupby.length) { this.measures = measures_groupby; }
76
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); }
80
81         return this.update_data();
82     },
83
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});
94         },
95
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;
103             }
104         }
105         return (default_values || new Array(this.measures.length));
106         },
107
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
113     //      {
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)
121     //      }
122         is_row: function (id) {
123         return !!_.findWhere(this.rows.headers, {id:id});
124     },
125
126         is_col: function (id) {
127         return !!_.findWhere(this.cols.headers, {id:id});
128         },
129
130         get_header: function (id) {
131                 return _.findWhere(this.rows.headers.concat(this.cols.headers), {id:id});
132         },
133
134     _get_headers_with_depth: function (headers, depth) {
135         return _.filter(headers, function (header) {
136             return header.path.length === depth;
137         });
138     },
139
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);
143         },
144
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);
148         },
149
150     get_ancestor_leaves: function (header) {
151         return _.where(this.get_ancestors_and_self(header), {expanded:false});
152     },
153
154         // return all non expanded rows
155         get_rows_leaves: function () {
156                 return _.where(this.rows.headers, {expanded:false});
157         },
158
159         // return all non expanded cols
160         get_cols_leaves: function () {
161                 return _.where(this.cols.headers, {expanded:false});
162         },
163
164     get_ancestors: function (header) {
165         var self = this;
166         if (!header.children) return [];
167         return  [].concat.apply([], _.map(header.children, function (c) {
168             return self.get_ancestors_and_self(c);
169         }));
170     },
171
172     get_ancestors_and_self: function (header) {
173         var self = this;
174         return [].concat.apply([header], _.map(header.children, function (c) {
175             return self.get_ancestors_and_self(c);
176         }));
177     },
178
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);
182         },
183
184         get_other_root: function (header) {
185                 return (header.root === this.rows) ? this.cols : this.rows;
186         },
187
188     main_row: function () { return this.rows.headers[0]; },
189
190     main_col: function () { return this.cols.headers[0]; },
191
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');
199
200                 header.root.headers = _.difference(header.root.headers, ancestors);
201         header.children = [];
202                 header.expanded = false;
203
204         this.cells = _.reject(this.cells, function (cell) {
205             return (_.contains(removed_ids, cell.x) || _.contains(removed_ids, cell.y));
206         });
207
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);
211             return true;
212         }
213         return false;
214         },
215
216     fold_with_depth: function (root, depth) {
217         var self = this;
218         _.each(this._get_headers_with_depth(root.headers, depth), function (header) {
219             self.fold(header);
220         });
221     },
222
223     expand_all: function () {
224         this.rows.headers = null;
225         this.cols.headers = null;
226         return this.update_data();
227     },
228
229         expand: function (header_id, groupby) {
230         var self = this,
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');
235
236         if (header.path.length === header.root.groupby.length) {
237             header.root.groupby.push(groupby);
238         }
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);
244             });
245             header.expanded = true;
246             header.children.forEach(function (child) {
247                 child.expanded = false;
248                 child.root = header.root;
249             });
250         });
251         },
252
253         swap_axis: function () {
254                 var temp = this.rows;
255                 this.rows = this.cols;
256                 this.cols = temp;
257         },
258
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 () {
266         var self = this;
267         this.updating = this.perform_requests().then (function () {
268             var data = Array.prototype.slice.call(arguments);
269             self.no_data = !data[0].length;
270             if (self.no_data) {
271                 return;
272             }
273             var row_headers = [],
274                 col_headers = [];
275             self.cells = [];
276
277             var dim_col = self.cols.groupby.length,
278                 i, j, index;
279
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);
284                 }
285             }
286             self.set_headers(row_headers, self.rows);
287             self.set_headers(col_headers, self.cols);
288         });
289         return this.updating;
290     },
291
292     make_headers_and_cell: function (data_pts, row_headers, col_headers, index, prefix, expand) {
293         var self = this;
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);
297
298             if (expand && !_.find(col_headers, function (hdr) {return self.isEqual(col_value, hdr.path);})) {
299                 return;
300             }
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);
303
304             var cell_value = _.map(self.measures, function (m) {
305                 return data_pt.attributes.aggregates[m.field];
306             });
307             self.cells.push({
308                 x: Math.min(row.id, col.id),
309                 y: Math.max(row.id, col.id),
310                 values: cell_value
311             });
312         });
313     },
314
315     make_header: function (values) {
316         return _.extend({
317             children: [],
318             domain: this.domain,
319             expanded: undefined,
320             id: _.uniqueId(),
321             path: [],
322             root: undefined,
323             title: undefined
324         }, values || {});
325     },
326
327     find_or_create_header: function (headers, path, data_pt) {
328         var self = this;
329         var hdr = _.find(headers, function (header) {
330             return self.isEqual(path, header.path);
331         });
332         if (hdr) {
333             return hdr;
334         }
335         if (!path.length) {
336             hdr = this.make_header({title: _t('Total')});
337             headers.push(hdr);
338             return hdr;
339         }
340         hdr = this.make_header({
341             path:path,
342             domain:data_pt.model._domain,
343             title: _t(_.last(path))
344         });
345         var parent = _.find(headers, function (header) {
346             return self.isEqual(header.path, _.initial(path, 1));
347         });
348
349         var previous = parent.children.length ? _.last(parent.children) : parent;
350         headers.splice(headers.indexOf(previous) + 1, 0, hdr);
351         parent.children.push(hdr);
352         return hdr;
353     },
354
355     perform_requests: function (group1, group2, domain) {
356         var self = this,
357             requests = [],
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); });
362
363         group1 = group1 || row_gbs;
364         group2 = group2 || col_gbs;
365
366         var i,j, groupbys;
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));
371             }
372         }
373         return $.when.apply(null, requests);
374     },
375
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) {
378         var self = this;
379         if (root.headers) {
380             _.each(root.headers, function (header) {
381                 var corresponding_header = _.find(new_headers, function (h) {
382                     return self.isEqual(h.path, header.path);
383                 });
384                 if (corresponding_header && header.expanded) {
385                     corresponding_header.expanded = true;
386                     _.each(corresponding_header.children, function (c) {
387                         c.expanded = false;
388                     });
389                 }
390                 if (corresponding_header && (!header.expanded)) {
391                     corresponding_header.expanded = false;
392                     corresponding_header.children = [];
393                 }
394             });
395             var updated_headers = _.filter(new_headers, function (header) {
396                 return (header.expanded !== undefined);
397             });
398             _.each(updated_headers, function (header) {
399                 header.root = root;
400             });
401             root.headers = updated_headers;
402         } else {
403             root.headers = new_headers;
404             _.each(root.headers, function (header) {
405                 header.root = root;
406                 header.expanded = (header.children.length > 0);
407             });
408         }
409         return new_headers;
410     },
411
412     get_groups: function (groupbys, fields, domain) {
413         var self = this;
414         return this.model.query(_.without(fields, '__count'))
415             .filter(domain)
416             .context(this.context)
417             .lazy(false)
418             .group_by(groupbys)
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);
427                         });
428                     if (grouped_on.length === 1) {
429                         attrs.value = [attrs.value];
430                     }
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];
441                         }
442                         return attrs.value[i];
443                     });
444                     attrs.aggregates.__count = group.attributes.length;
445                     attrs.grouped_on = raw_grouped_on;
446                     return group;
447                 });
448             });
449     },
450
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];
454     },
455
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]) {
460                 return false;
461             }
462         }
463         return true;
464     },
465
466 });
467
468 })();