/* jshint undef: false */ (function () { 'use strict'; var _lt = openerp.web._lt; var _t = openerp.web._t; // PivotTable requires a call to update_data after initialization openerp.web_graph.PivotTable = openerp.web.Class.extend({ init: function (model, domain, fields, options) { this.cells = []; 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('Count')}; this.measures = options.measures || []; this.rows = { groupby: options.row_groupby, headers: null }; this.cols = { groupby: options.col_groupby, headers: null }; this.numbering = {}; }, // ---------------------------------------------------------------------- // Configuration methods // ---------------------------------------------------------------------- // 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:_} // If its type is date/datetime, field can have the corresponding interval in its description, // for example 'create_date:week'. set_measures: function (measures) { this.measures = measures; return this.update_data(); }, toggle_measure: function (measure) { var current_measure = _.findWhere(this.measures, measure); if (current_measure) { // remove current_measure var index = this.measures.indexOf(current_measure); this.measures = _.without(this.measures, current_measure); if (this.measures.length === 0) { this.no_data = true; } else { _.each(this.cells, function (cell) { cell.values.splice(index, 1); }); } return $.Deferred().resolve(); } else { // add a new measure this.measures.push(measure); return this.update_data(); } }, 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), 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(); }, // ---------------------------------------------------------------------- // 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 // 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) add_cell : function (id1, id2, values) { this.cells.push({x: Math.min(id1, id2), y: Math.max(id1, id2), values: values}); }, get_values: function (id1, id2, default_values) { 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 // 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) // children:[_], (subrows or sub cols of this row/col) // domain:_, (domain of data in this row/col) // root:_ (ref to this.rows or this.cols corresponding to the header) // expanded:_ (boolean, true if it has been expanded) // } is_row: function (id) { return !!_.findWhere(this.rows.headers, {id:id}); }, is_col: function (id) { return !!_.findWhere(this.cols.headers, {id:id}); }, get_header: function (id) { return _.findWhere(this.rows.headers.concat(this.cols.headers), {id:id}); }, _get_headers_with_depth: function (headers, depth) { return _.filter(headers, function (header) { return header.path.length === depth; }); }, // return all columns with a path length of 'depth' get_cols_with_depth: function (depth) { return this._get_headers_with_depth(this.cols.headers, depth); }, // return all rows with a path length of 'depth' get_rows_with_depth: function (depth) { 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); })); }, 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); })); }, get_total: function (header) { return (header) ? this.get_values(header.id, this.get_other_root(header).headers[0].id) : this.get_values(this.rows.headers[0].id, this.cols.headers[0].id); }, get_other_root: function (header) { return (header.root === this.rows) ? this.cols : this.rows; }, main_row: function () { return this.rows.headers[0]; }, main_col: function () { return this.cols.headers[0]; }, // ---------------------------------------------------------------------- // Table manipulation methods : fold/expand/swap // ---------------------------------------------------------------------- // return true if the folding changed the groupbys, false if not fold: function (header) { var ancestors = this.get_ancestors(header), removed_ids = _.pluck(ancestors, 'id'); header.root.headers = _.difference(header.root.headers, ancestors); header.children = []; header.expanded = false; this.cells = _.reject(this.cells, function (cell) { return (_.contains(removed_ids, cell.x) || _.contains(removed_ids, cell.y)); }); var new_groupby_length = _.max(_.pluck(_.pluck(header.root.headers, 'path'), 'length')); if (new_groupby_length < header.root.groupby.length) { header.root.groupby.splice(new_groupby_length); return true; } return false; }, fold_with_depth: function (root, depth) { var self = this; _.each(this._get_headers_with_depth(root.headers, depth), function (header) { self.fold(header); }); }, expand_all: function () { this.rows.headers = null; this.cols.headers = null; return this.update_data(); }, expand: function (header_id, groupby) { var self = this, header = this.get_header(header_id), 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); } 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; }); }); }, swap_axis: function () { var temp = this.rows; this.rows = this.cols; this.cols = temp; }, // ---------------------------------------------------------------------- // Data updating methods // ---------------------------------------------------------------------- // 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 // to null before calling update_data. 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; }, 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); 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); 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 }); }); }, make_header: function (values) { return _.extend({ children: [], domain: this.domain, expanded: undefined, id: _.uniqueId(), path: [], root: undefined, title: undefined }, values || {}); }, find_or_create_header: function (headers, path, data_pt) { var self = this; var hdr = _.find(headers, function (header) { return self.isEqual(path, header.path); }); 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)) }); var parent = _.find(headers, function (header) { return self.isEqual(header.path, _.initial(path, 1)); }); var previous = parent.children.length ? _.last(parent.children) : parent; headers.splice(headers.indexOf(previous) + 1, 0, hdr); parent.children.push(hdr); return hdr; }, 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); }, // 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; }, get_groups: function (groupbys, fields, domain) { var self = this; return this.model.query(_.without(fields, '__count')) .filter(domain) .context(this.context) .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 self.get_numbered_value(attrs.value[i], grp); } else if (field && field.type === 'selection') { var selected = _.where(field.selection, {0: attrs.value[i]})[0]; return selected ? self.get_numbered_value(selected, grp) : attrs.value[i]; } return attrs.value[i]; }); attrs.aggregates.__count = group.attributes.length; attrs.grouped_on = raw_grouped_on; return group; }); }); }, get_numbered_value: function(value, grp) { var id = value[0]; var name = value[1] this.numbering[grp] = this.numbering[grp] || {}; this.numbering[grp][name] = this.numbering[grp][name] || {}; var numbers = this.numbering[grp][name]; numbers[id] = numbers[id] || _.size(numbers) + 1; return name + (numbers[id] > 1 ? " (" + numbers[id] + ")" : ""); }, // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id raw_field: function (field) { return field.split(':')[0]; }, 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; } } return true; }, }); })();