[MERGE] forward port of branch saas-3 up to fe8106f
[odoo/odoo.git] / addons / web_graph / static / src / js / pivot_table.js
index 2d68cb4..a46203f 100644 (file)
@@ -15,6 +15,7 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
                this.domain = domain;
         this.context = options.context;
                this.no_data = true;
+               this.updating = false;
                this.model = model;
                this.fields = fields;
         this.fields.__count = {type: 'integer', string:_t('Quantity')};
@@ -28,7 +29,7 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
        // ----------------------------------------------------------------------
     // this.measures: list of measure [measure], measure = {field: _, string: _, type: _}
     // this.rows.groupby, this.cols.groupby : list of groupbys used for describing rows (...),
-    //      a groupby is also {field:_, string:_, type:_} 
+    //      a groupby is also {field:_, string:_, type:_}
     //      If its type is date/datetime, field can have the corresponding interval in its description,
     //      for example 'create_date:week'.
        set_measures: function (measures) {
@@ -55,16 +56,27 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
         }
        },
 
-    set: function (domain, row_groupby, col_groupby) {
+    set: function (domain, row_groupby, col_groupby, measures_groupby) {
+        var self = this;
+        if (this.updating) {
+            return this.updating.then(function () {
+                self.updating = false;
+                return self.set(domain, row_groupby, col_groupby, measures_groupby);
+            });
+        }
         var row_gb_changed = !_.isEqual(row_groupby, this.rows.groupby),
-            col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby);
-        
+            col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby),
+                       measures_gb_changed = !_.isEqual(measures_groupby, this.measures);
+
         this.domain = domain;
         this.rows.groupby = row_groupby;
         this.cols.groupby = col_groupby;
 
+               if (measures_groupby.length) { this.measures = measures_groupby; }
+
         if (row_gb_changed) { this.rows.headers = null; }
         if (col_gb_changed) { this.cols.headers = null; }
+               if (measures_gb_changed && measures_groupby.length) { this.set_measures(measures_groupby); }
 
         return this.update_data();
     },
@@ -73,7 +85,7 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
        // Cells manipulation methods
        // ----------------------------------------------------------------------
     // cells are objects {x:_, y:_, values:_} where x < y and values is an array
-    //       of values (one for each measure).  The condition x < y might look 
+    //       of values (one for each measure).  The condition x < y might look
     //       unnecessary, but it makes the rest of the code simpler: headers
     //       don't krow if they are rows or cols, they just know their id, so
     //       it is useful that a call get_values(id1, id2) is the same as get_values(id2, id1)
@@ -82,16 +94,23 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
        },
 
        get_values: function (id1, id2, default_values) {
-               var cell = _.findWhere(this.cells, {x: Math.min(id1, id2), y: Math.max(id1, id2)});
-               return (cell !== undefined) ? cell.values : (default_values || new Array(this.measures.length));
+        var cells = this.cells,
+            x = Math.min(id1, id2),
+            y = Math.max(id1, id2);
+        for (var i = 0; i < cells.length; i++) {
+            if (cells[i].x == x && cells[i].y == y) {
+                return cells[i].values;
+            }
+        }
+        return (default_values || new Array(this.measures.length));
        },
 
        // ----------------------------------------------------------------------
        // Headers/Rows/Cols manipulation methods
        // ----------------------------------------------------------------------
-    // this.rows.headers, this.cols.headers = [header] describe the tree structure 
+    // this.rows.headers, this.cols.headers = [header] describe the tree structure
     //      of rows/cols.  Headers are objects
-    //      { 
+    //      {
     //          id:_,               (unique id obviously)
     //          path: [...],        (array of all parents title, with its own title at the end)
     //          title:_,            (name of the row/col)
@@ -128,25 +147,33 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
         return this._get_headers_with_depth(this.rows.headers, depth);
        },
 
+    get_ancestor_leaves: function (header) {
+        return _.where(this.get_ancestors_and_self(header), {expanded:false});
+    },
+
        // return all non expanded rows
        get_rows_leaves: function () {
                return _.where(this.rows.headers, {expanded:false});
        },
-       
+
        // return all non expanded cols
        get_cols_leaves: function () {
                return _.where(this.cols.headers, {expanded:false});
        },
-       
+
     get_ancestors: function (header) {
         var self = this;
         if (!header.children) return [];
-        return  [].concat.apply([], _.map(header.children, function (c) {return self.get_ancestors_and_self(c); }));
+        return  [].concat.apply([], _.map(header.children, function (c) {
+            return self.get_ancestors_and_self(c);
+        }));
     },
 
     get_ancestors_and_self: function (header) {
         var self = this;
-        return [].concat.apply([header], _.map(header.children, function (c) { return self.get_ancestors_and_self(c); }));
+        return [].concat.apply([header], _.map(header.children, function (c) {
+            return self.get_ancestors_and_self(c);
+        }));
     },
 
        get_total: function (header) {
@@ -159,7 +186,7 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
        },
 
     main_row: function () { return this.rows.headers[0]; },
-    
+
     main_col: function () { return this.cols.headers[0]; },
 
        // ----------------------------------------------------------------------
@@ -202,54 +229,27 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
        expand: function (header_id, groupby) {
         var self = this,
             header = this.get_header(header_id),
-                       otherRoot = this.get_other_root(header),
-                       fields = otherRoot.groupby.concat(this.measures);
+            other_root = this.get_other_root(header),
+            this_gb = [groupby.field],
+            other_gbs = _.pluck(other_root.groupby, 'field');
 
         if (header.path.length === header.root.groupby.length) {
             header.root.groupby.push(groupby);
         }
-        groupby = [groupby].concat(otherRoot.groupby);
-
-        return this.get_groups(groupby, fields, header.domain).then(function (groups) {
-            _.each(groups.reverse(), function (group) {
-                // make header
-                var child = self.make_header(group, header);
-                child.expanded = false;
-                header.children.splice(0,0, child);
-                header.root.headers.splice(header.root.headers.indexOf(header) + 1, 0, child);
-                // make cells
-                _.each(self.get_ancestors_and_self(group), function (data) {
-                    var values = _.map(self.measures, function (m) {
-                        return data.attributes.aggregates[m.field];
-                    });
-                    var other = _.find(otherRoot.headers, function (h) {
-                        if (header.root === self.cols) {
-                            return _.isEqual(data.path.slice(1), h.path);
-                        } else {
-                            return _.isEqual(_.rest(data.path), h.path);
-                            }
-                        });
-                    if (other) {
-                        self.add_cell(child.id, other.id, values);
-                    }
-                });
+        return this.perform_requests(this_gb, other_gbs, header.domain).then(function () {
+            var data = Array.prototype.slice.call(arguments).slice(other_gbs.length + 1);
+            _.each(data, function (data_pt) {
+                self.make_headers_and_cell(
+                    data_pt, header.root.headers, other_root.headers, 1, header.path, true);
             });
             header.expanded = true;
+            header.children.forEach(function (child) {
+                child.expanded = false;
+                child.root = header.root;
+            });
         });
        },
 
-       make_header: function (group, parent) {
-               var title = parent ? group.attributes.value : _t('Total');
-        return {
-                       id: _.uniqueId(),
-                       path: parent ? parent.path.concat(title) : [],
-                       title: title,
-                       children: [],
-                       domain: parent ? group.model._domain : this.domain,
-                       root: parent ? parent.root : undefined,
-               };
-       },
-
        swap_axis: function () {
                var temp = this.rows;
                this.rows = this.cols;
@@ -259,209 +259,210 @@ openerp.web_graph.PivotTable = openerp.web.Class.extend({
        // ----------------------------------------------------------------------
        // Data updating methods
        // ----------------------------------------------------------------------
-    // Load the data from the db, using the method this.load_data
     // update_data will try to preserve the expand/not expanded status of each
-    // column/row.  If you want to expand all, then set this.cols.headers/this.rows.headers 
+    // column/row.  If you want to expand all, then set this.cols.headers/this.rows.headers
     // to null before calling update_data.
-       update_data: function () {
-               var self = this;
-
-               return this.load_data().then (function (result) {
-                       if (result) {
-                               self.no_data = false;
-                               self[self.cols.headers ? 'update_headers' : 'expand_headers'](self.cols, result.col_headers);
-                               self[self.rows.headers ? 'update_headers' : 'expand_headers'](self.rows, result.row_headers);
-                       } else {
-                               self.no_data = true;
-                       }
-               });
-       },
+    update_data: function () {
+        var self = this;
+        this.updating = this.perform_requests().then (function () {
+            var data = Array.prototype.slice.call(arguments);
+            self.no_data = !data[0].length;
+            if (self.no_data) {
+                return;
+            }
+            var row_headers = [],
+                col_headers = [];
+            self.cells = [];
+
+            var dim_col = self.cols.groupby.length,
+                i, j, index;
+
+            for (i = 0; i < self.rows.groupby.length + 1; i++) {
+                for (j = 0; j < dim_col + 1; j++) {
+                    index = i*(dim_col + 1) + j;
+                    self.make_headers_and_cell(data[index], row_headers, col_headers, i);
+                }
+            }
+            self.set_headers(row_headers, self.rows);
+            self.set_headers(col_headers, self.cols);
+        });
+        return this.updating;
+    },
 
-       expand_headers: function (root, new_headers) {
-               root.headers = new_headers;
-               _.each(root.headers, function (header) {
-                       header.root = root;
-                       header.expanded = (header.children.length > 0);
-               });
-       },
+    make_headers_and_cell: function (data_pts, row_headers, col_headers, index, prefix, expand) {
+        var self = this;
+        data_pts.forEach(function (data_pt) {
+            var row_value = (prefix || []).concat(data_pt.attributes.value.slice(0,index));
+            var col_value = data_pt.attributes.value.slice(index);
 
-       update_headers: function (root, new_headers) {
-               _.each(root.headers, function (header) {
-                       var corresponding_header = _.find(new_headers, function (h) {
-                               return _.isEqual(h.path, header.path);
-                       });
-                       if (corresponding_header && header.expanded) {
-                               corresponding_header.expanded = true;
-                               _.each(corresponding_header.children, function (c) {
-                                       c.expanded = false;
-                               });
-                       }
-                       if (corresponding_header && (!header.expanded)) {
-                               corresponding_header.expanded = false;
-                       }
-               });
-               var updated_headers = _.filter(new_headers, function (header) {
-                       return (header.expanded !== undefined);
-               });
-               _.each(updated_headers, function (header) {
-                       if (!header.expanded) {
-                               header.children = [];
-                       }
-                       header.root = root;
-               });
-               root.headers = updated_headers;
-       },
+            if (expand && !_.find(col_headers, function (hdr) {return self.isEqual(col_value, hdr.path);})) {
+                return;
+            }
+            var row = self.find_or_create_header(row_headers, row_value, data_pt);
+            var col = self.find_or_create_header(col_headers, col_value, data_pt);
 
-    // ----------------------------------------------------------------------
-    // Data loading methods
-    // ----------------------------------------------------------------------
-
-    // To obtain all the values required to draw the full table, we have to do 
-    // at least      2 + min(row.groupby.length, col.groupby.length)
-    // calls to readgroup. To simplify the code, we will always do 
-    // 2 + row.groupby.length calls. For example, if row.groupby = [r1, r2, r3] 
-    // and col.groupby = [c1, c2], then we will make the call with the following 
-    // groupbys: [r1,r2,r3], [c1,r1,r2,r3], [c1,c2,r1,r2,r3], [].
-    load_data: function () {
-        var self = this,
-            cols = this.cols.groupby,
-            rows = this.rows.groupby,
-            visible_fields = rows.concat(cols, self.measures);
+            var cell_value = _.map(self.measures, function (m) {
+                return data_pt.attributes.aggregates[m.field];
+            });
+            self.cells.push({
+                x: Math.min(row.id, col.id),
+                y: Math.max(row.id, col.id),
+                values: cell_value
+            });
+        });
+    },
 
-        if (this.measures.length === 0) {
-            return $.Deferred.resolve().promise();
-        }
+    make_header: function (values) {
+        return _.extend({
+            children: [],
+            domain: this.domain,
+            expanded: undefined,
+            id: _.uniqueId(),
+            path: [],
+            root: undefined,
+            title: undefined
+        }, values || {});
+    },
 
-        var groupbys = _.map(_.range(cols.length + 1), function (i) {
-            return cols.slice(0, i).concat(rows);
+    find_or_create_header: function (headers, path, data_pt) {
+        var self = this;
+        var hdr = _.find(headers, function (header) {
+            return self.isEqual(path, header.path);
         });
-        groupbys.push([]);
-
-        var get_data_requests = _.map(groupbys, function (groupby) {
-            return self.get_groups(groupby, visible_fields, self.domain);
+        if (hdr) {
+            return hdr;
+        }
+        if (!path.length) {
+            hdr = this.make_header({title: _t('Total')});
+            headers.push(hdr);
+            return hdr;
+        }
+        hdr = this.make_header({
+            path:path,
+            domain:data_pt.model._domain,
+            title: _t(_.last(path))
         });
-
-        return $.when.apply(null, get_data_requests).then(function () {
-            var data = Array.prototype.slice.call(arguments),
-                row_data = data[0],
-                col_data = (cols.length !== 0) ? data[data.length - 2] : [],
-                has_data = data[data.length - 1][0];
-
-            return has_data && self.format_data(col_data, row_data, data);
+        var parent = _.find(headers, function (header) {
+            return self.isEqual(header.path, _.initial(path, 1));
         });
-    },
 
-       get_groups: function (groupbys, fields, domain, path) {
-               var self = this,
-            groupby = (groupbys.length) ? groupbys[0] : [];
-        path = path || [];
+        var previous = parent.children.length ? _.last(parent.children) : parent;
+        headers.splice(headers.indexOf(previous) + 1, 0, hdr);
+        parent.children.push(hdr);
+        return hdr;
+    },
 
-               return this._query_db(groupby, fields, domain, path).then(function (groups) {
-            if (groupbys.length > 1) {
-                var get_subgroups = $.when.apply(null, _.map(groups, function (group) {
-                    return self.get_groups(_.rest(groupbys), fields, group.model._domain, path.concat(group.attributes.value)).then(function (subgroups) {
-                        group.children = subgroups;
-                    });
-                }));
-                 return get_subgroups.then(function () {
-                     return groups;
-                 });
-            } else {
-                return groups;
+    perform_requests: function (group1, group2, domain) {
+        var self = this,
+            requests = [],
+            row_gbs = _.pluck(this.rows.groupby, 'field'),
+            col_gbs = _.pluck(this.cols.groupby, 'field'),
+            field_list = row_gbs.concat(col_gbs, _.pluck(this.measures, 'field')),
+            fields = field_list.map(function (f) { return self.raw_field(f); });
+
+        group1 = group1 || row_gbs;
+        group2 = group2 || col_gbs;
+
+        var i,j, groupbys;
+        for (i = 0; i < group1.length + 1; i++) {
+            for (j = 0; j < group2.length + 1; j++) {
+                groupbys = group1.slice(0,i).concat(group2.slice(0,j));
+                requests.push(self.get_groups(groupbys, fields, domain || self.domain));
             }
-               });
-
-       },
+        }
+        return $.when.apply(null, requests);
+    },
 
-       _query_db: function (groupby, fields, domain, path) {
-               var self = this,
-            field_ids = _.without(_.pluck(fields, 'field'), '__count'),
-            fields = _.map(field_ids, function(f) { return self.raw_field(f); });
+    // set the 'expanded' status of new_headers more or less like root.headers, with root as root
+    set_headers: function(new_headers, root) {
+        var self = this;
+        if (root.headers) {
+            _.each(root.headers, function (header) {
+                var corresponding_header = _.find(new_headers, function (h) {
+                    return self.isEqual(h.path, header.path);
+                });
+                if (corresponding_header && header.expanded) {
+                    corresponding_header.expanded = true;
+                    _.each(corresponding_header.children, function (c) {
+                        c.expanded = false;
+                    });
+                }
+                if (corresponding_header && (!header.expanded)) {
+                    corresponding_header.expanded = false;
+                    corresponding_header.children = [];
+                }
+            });
+            var updated_headers = _.filter(new_headers, function (header) {
+                return (header.expanded !== undefined);
+            });
+            _.each(updated_headers, function (header) {
+                header.root = root;
+            });
+            root.headers = updated_headers;
+        } else {
+            root.headers = new_headers;
+            _.each(root.headers, function (header) {
+                header.root = root;
+                header.expanded = (header.children.length > 0);
+            });
+        }
+        return new_headers;
+    },
 
-               return this.model.query(field_ids)
-                       .filter(domain)
+    get_groups: function (groupbys, fields, domain) {
+        var self = this;
+        return this.model.query(_.without(fields, '__count'))
+            .filter(domain)
             .context(this.context)
-                       .group_by(groupby.field)
-                       .then(function (results) {
-                               var groups = _.filter(results, function (group) {
-                                       return group.attributes.length > 0;
-                               });
-                               return _.map(groups, function (g) { return self.format_group(g, path); });
-                       });
-       },
+            .lazy(false)
+            .group_by(groupbys)
+            .then(function (groups) {
+                return groups.filter(function (group) {
+                    return group.attributes.length > 0;
+                }).map(function (group) {
+                    var attrs = group.attributes,
+                        grouped_on = attrs.grouped_on instanceof Array ? attrs.grouped_on : [attrs.grouped_on],
+                        raw_grouped_on = grouped_on.map(function (f) {
+                            return self.raw_field(f);
+                        });
+                    if (grouped_on.length === 1) {
+                        attrs.value = [attrs.value];
+                    }
+                    attrs.value = _.range(grouped_on.length).map(function (i) {
+                        var grp = grouped_on[i],
+                            field = self.fields[grp];
+                        if (attrs.value[i] === false) {
+                            return _t('Undefined');
+                        } else if (attrs.value[i] instanceof Array) {
+                            return attrs.value[i][1];
+                        } else if (field && field.type === 'selection') {
+                            var selected = _.where(field.selection, {0: attrs.value[i]})[0];
+                            return selected ? selected[1] : attrs.value[i];
+                        }
+                        return attrs.value[i];
+                    });
+                    attrs.aggregates.__count = group.attributes.length;
+                    attrs.grouped_on = raw_grouped_on;
+                    return group;
+                });
+            });
+    },
 
     // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id
     raw_field: function (field) {
         return field.split(':')[0];
     },
 
-    // add the path to the group and sanitize the value...
-    format_group: function (group, current_path) {
-        var attrs = group.attributes,
-            value = attrs.value,
-            grouped_on = attrs.grouped_on ? this.raw_field(attrs.grouped_on) : false;
-
-        if (value === false) {
-            group.attributes.value = _t('Undefined');
-        } else if (grouped_on && this.fields[grouped_on].type === 'selection') {
-            var selection = this.fields[grouped_on].selection,
-                value_lookup = _.where(selection, {0:value}); 
-            group.attributes.value = !_.isEmpty(value_lookup) ? value_lookup[0][1] : _t('Undefined');
-        } else if (value instanceof Array) {
-            group.attributes.value = value[1];
+    isEqual: function (path1, path2) {
+        if (path1.length !== path2.length) { return false; }
+        for (var i = 0; i < path1.length; i++) {
+            if (path1[i] !== path2[i]) {
+                return false;
+            }
         }
-
-        group.path = (value !== undefined) ? (current_path || []).concat(group.attributes.value) : [];
-        group.attributes.aggregates.__count = group.attributes.length;
-
-        return group;
+        return true;
     },
 
-       format_data: function (col_data, row_data, cell_data) {
-               var self = this,
-                       dim_row = this.rows.groupby.length,
-                       dim_col = this.cols.groupby.length,
-            col_headers = this.get_ancestors_and_self(this.make_headers(col_data, dim_col)),
-            row_headers = this.get_ancestors_and_self(this.make_headers(row_data, dim_row));
-
-        this.cells = [];
-               _.each(cell_data, function (data, index) {
-                       self.make_cells(data, index, [], row_headers, col_headers);
-               }); // not pretty. make it more functional?
-
-               return {col_headers: col_headers, row_headers: row_headers};
-       },
-
-       make_headers: function (data, depth, parent) {
-               var self = this,
-            main = this.make_header(data, parent);
-
-               if (main.path.length < depth) {
-                       main.children = _.map(data.children || data, function (data_pt) {
-                               return self.make_headers (data_pt, depth, main);
-                       });
-               }
-               return main;
-       },
-
-       make_cells: function (data, index, current_path, rows, cols) {
-               var self = this;
-               _.each(data, function (group) {
-                       var attr = group.attributes,
-                               path = attr.grouped_on ? current_path.concat(attr.value) : current_path,
-                               values = _.map(self.measures, function (measure) { return attr.aggregates[measure.field]; }),
-                row = _.find(rows, function (header) { return _.isEqual(header.path, path.slice(index)); }),
-                col = _.find(cols, function (header) { return _.isEqual(header.path, path.slice(0, index)); });
-
-                       self.add_cell(row.id, col.id, values);
-                       if (group.children) {
-                               self.make_cells (group.children, index, path, rows, cols);
-                       }
-               });
-       },
-
 });
 
 })();
-
-