odoo.define('web.data', function (require) { "use strict"; var Class = require('web.Class'); var Context = require('web.Context'); var concurrency = require('web.concurrency'); var mixins = require('web.mixins'); var Model = require('web.Model'); var session = require('web.session'); var translation = require('web.translation'); var pyeval = require('web.pyeval'); var _t = translation._t; /** * Serializes the sort criterion array of a dataset into a form which can be * consumed by OpenERP's RPC APIs. * * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting * @returns {String} SQL-like sorting string (``ORDER BY``) clause */ function serialize_sort(criterion) { return _.map(criterion, function (criteria) { if (criteria[0] === '-') { return criteria.slice(1) + ' DESC'; } return criteria + ' ASC'; }).join(', '); } /** * Reverse of the serialize_sort function: convert an array of SQL-like sort * descriptors into a list of fields prefixed with '-' if necessary. */ function deserialize_sort(criterion) { return _.map(criterion, function (criteria) { var split = _.without(criteria.split(' '), ''); return (split[1] && split[1].toLowerCase() === 'desc' ? '-' : '') + split[0]; }); } var Query = Class.extend({ init: function (model, fields) { this._model = model; this._fields = fields; this._filter = []; this._context = {}; this._lazy = true; this._limit = false; this._offset = 0; this._order_by = []; }, clone: function (to_set) { to_set = to_set || {}; var q = new Query(this._model, this._fields); q._context = this._context; q._filter = this._filter; q._lazy = this._lazy; q._limit = this._limit; q._offset = this._offset; q._order_by = this._order_by; for(var key in to_set) { if (!to_set.hasOwnProperty(key)) { continue; } switch(key) { case 'filter': q._filter = new CompoundDomain( q._filter, to_set.filter); break; case 'context': q._context = new Context( q._context, to_set.context); break; case 'lazy': case 'limit': case 'offset': case 'order_by': q['_' + key] = to_set[key]; } } return q; }, _execute: function (options) { var self = this; options = options || {}; return session.rpc('/web/dataset/search_read', { model: this._model.name, fields: this._fields || false, domain: pyeval.eval('domains', [this._model.domain(this._filter)]), context: pyeval.eval('contexts', [this._model.context(this._context)]), offset: this._offset, limit: this._limit, sort: serialize_sort(this._order_by) }, options).then(function (results) { self._count = results.length; return results.records; }, null); }, /** * Fetches the first record matching the query, or null * * @param {Object} [options] additional options for the rpc() method * @returns {jQuery.Deferred} */ first: function (options) { var self = this; return this.clone({limit: 1})._execute(options).then(function (records) { delete self._count; if (records.length) { return records[0]; } return null; }); }, /** * Fetches all records matching the query * * @param {Object} [options] additional options for the rpc() method * @returns {jQuery.Deferred>} */ all: function (options) { return this._execute(options); }, /** * Fetches the number of records matching the query in the database * * @returns {jQuery.Deferred} */ count: function () { if (this._count !== undefined) { return $.when(this._count); } return this._model.call( 'search_count', [this._filter], { context: this._model.context(this._context)}); }, /** * Performs a groups read according to the provided grouping criterion * * @param {String|Array} grouping * @returns {jQuery.Deferred> | null} */ group_by: function (grouping) { var ctx = pyeval.eval( 'context', this._model.context(this._context)); // undefined passed in explicitly (!) if (_.isUndefined(grouping)) { grouping = []; } if (!(grouping instanceof Array)) { grouping = _.toArray(arguments); } if (_.isEmpty(grouping) && !ctx.group_by_no_leaf) { return null; } var raw_fields = _.map(grouping.concat(this._fields || []), function (field) { return field.split(':')[0]; }); var self = this; return this._model.call('read_group', { groupby: grouping, fields: _.uniq(raw_fields), domain: this._model.domain(this._filter), context: ctx, offset: this._offset, lazy: this._lazy, limit: this._limit, orderby: serialize_sort(this._order_by) || false }).then(function (results) { return _(results).map(function (result) { // FIX: querygroup initialization result.__context = result.__context || {}; result.__context.group_by = result.__context.group_by || []; _.defaults(result.__context, ctx); var grouping_fields = self._lazy ? [grouping[0]] : grouping; return new QueryGroup( self._model.name, grouping_fields, result); }); }); }, /** * Creates a new query with the union of the current query's context and * the new context. * * @param context context data to add to the query * @returns {openerp.web.Query} */ context: function (context) { if (!context) { return this; } return this.clone({context: context}); }, /** * Creates a new query with the union of the current query's filter and * the new domain. * * @param domain domain data to AND with the current query filter * @returns {openerp.web.Query} */ filter: function (domain) { if (!domain) { return this; } return this.clone({filter: domain}); }, /** * Creates a new query with the provided parameter lazy replacing the current * query's own. * * @param {Boolean} lazy indicates if the read_group should return only the * first level of groupby records, or should return the records grouped by * all levels at once (so, it makes only 1 db request). * @returns {openerp.web.Query} */ lazy: function (lazy) { return this.clone({lazy: lazy}); }, /** * Creates a new query with the provided limit replacing the current * query's own limit * * @param {Number} limit maximum number of records the query should retrieve * @returns {openerp.web.Query} */ limit: function (limit) { return this.clone({limit: limit}); }, /** * Creates a new query with the provided offset replacing the current * query's own offset * * @param {Number} offset number of records the query should skip before starting its retrieval * @returns {openerp.web.Query} */ offset: function (offset) { return this.clone({offset: offset}); }, /** * Creates a new query with the provided ordering parameters replacing * those of the current query * * @param {String...} fields ordering clauses * @returns {openerp.web.Query} */ order_by: function (fields) { if (fields === undefined) { return this; } if (!(fields instanceof Array)) { fields = _.toArray(arguments); } if (_.isEmpty(fields)) { return this; } return this.clone({order_by: fields}); } }); var QueryGroup = Class.extend({ init: function (model, grouping_fields, read_group_group) { // In cases where group_by_no_leaf and no group_by, the result of // read_group has aggregate fields but no __context or __domain. // Create default (empty) values for those so that things don't break var fixed_group = _.extend( {__context: {group_by: []}, __domain: []}, read_group_group); var count_key = (grouping_fields[0] && grouping_fields[0].split(':')[0]) + '_count'; var aggregates = {}; _(fixed_group).each(function (value, key) { if (key.indexOf('__') === 0 || _.contains(grouping_fields, key) || (key === count_key)) { return; } aggregates[key] = value || 0; }); this.model = new Model( model, fixed_group.__context, fixed_group.__domain); var group_size = fixed_group[count_key] || fixed_group.__count || 0; var leaf_group = fixed_group.__context.group_by.length === 0; var value = (grouping_fields.length === 1) ? fixed_group[grouping_fields[0]] : _.map(grouping_fields, function (field) { return fixed_group[field]; }); var grouped_on = (grouping_fields.length === 1) ? grouping_fields[0] : grouping_fields; this.attributes = { folded: !!(fixed_group.__fold), grouped_on: grouped_on, // if terminal group (or no group) and group_by_no_leaf => use group.__count length: group_size, value: value, // A group is open-able if it's not a leaf in group_by_no_leaf mode has_children: !(leaf_group && fixed_group.__context.group_by_no_leaf), aggregates: aggregates }; }, get: function (key) { return this.attributes[key]; }, subgroups: function () { return this.model.query().group_by(this.model.context().group_by); }, query: function () { return this.model.query.apply(this.model, arguments); } }); var DataSet = Class.extend(mixins.PropertiesMixin, { /** * Collection of OpenERP records, used to share records and the current selection between views. * * @constructs instance.web.DataSet * * @param {String} model the OpenERP model this dataset will manage */ init: function (parent, model, context) { mixins.PropertiesMixin.init.call(this); this.model = model; this.context = context || {}; this.index = null; this._sort = []; this._model = new Model(model, context); this.orderer = new concurrency.DropMisordered(); }, previous: function () { this.index -= 1; if (!this.ids.length) { this.index = null; } else if (this.index < 0) { this.index = this.ids.length - 1; } return this; }, next: function () { this.index += 1; if (!this.ids.length) { this.index = null; } else if (this.index >= this.ids.length) { this.index = 0; } return this; }, select_id: function (id) { var idx = this.get_id_index(id); if (idx === null) { return false; } else { this.index = idx; return true; } }, get_id_index: function (id) { for (var i=0, ii=this.ids.length; i= ids.length - 1) { this.index = ids.length - 1; } }, unlink: function (ids) { this.set_ids(_.without.apply(null, [this.ids].concat(ids))); this.trigger('unlink', ids); return $.Deferred().resolve({result: true}); }, }); var DataSetSearch = DataSet.extend({ /** * @constructs instance.web.DataSetSearch * @extends instance.web.DataSet * * @param {Object} parent * @param {String} model * @param {Object} context * @param {Array} domain */ init: function (parent, model, context, domain) { this._super(parent, model, context); this.domain = domain || []; this._length = null; this.ids = []; this._model = new Model(model, context, domain); }, /** * Read a slice of the records represented by this DataSet, based on its * domain and context. * * @params {Object} options * @param {Array} [options.fields] fields to read and return, by default all fields are returned * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain * @param {Number} [options.offset=0] The index from which selected records should be returned * @param {Number} [options.limit=null] The maximum number of records to return * @returns {$.Deferred} */ read_slice: function (fields, options) { options = options || {}; var self = this; var q = this._model.query(fields || false) .filter(options.domain) .context(options.context) .offset(options.offset || 0) .limit(options.limit || false); q = q.order_by.apply(q, this._sort); return this.orderer.add(q.all()).done(function (records) { // FIXME: not sure about that one, *could* have discarded count q.count().done(function (count) { self._length = count; }); self.ids = _(records).pluck('id'); }); }, get_domain: function (other_domain) { return this._model.domain(other_domain); }, alter_ids: function (ids) { this._super(ids); if (this.index !== null && this.index >= this.ids.length) { this.index = this.ids.length > 0 ? this.ids.length - 1 : 0; } }, remove_ids: function (ids) { var before = this.ids.length; this._super(ids); if (this._length) { this._length -= (before - this.ids.length); } }, add_ids: function (ids, at) { var before = this.ids.length; this._super(ids, at); if(this._length){ this._length += (this.ids.length - before); } }, unlink: function (ids, callback, error_callback) { var self = this; return this._super(ids).done(function () { self.remove_ids( ids); self.trigger("dataset_changed", ids, callback, error_callback); }); }, size: function () { if (this._length !== null) { return this._length; } return this._super(); } }); var CompoundDomain = Class.extend({ init: function () { this.__ref = "compound_domain"; this.__domains = []; this.__eval_context = null; var self = this; _.each(arguments, function (x) { self.add(x); }); }, add: function (domain) { this.__domains.push(domain); return this; }, set_eval_context: function (eval_context) { this.__eval_context = eval_context; return this; }, get_eval_context: function () { return this.__eval_context; }, eval: function () { return pyeval.eval('domain', this); }, }); function rawify (data) { // replaces the value of relational fields by their raw value (the id, if // any, for many2ones and an array of ids for x2manys), in turn return _.mapObject(data, function (value) { if (value instanceof moment) { return moment; } if (_.isObject(value)) { return value.type === 'record' ? value.res_id : value.res_ids; } return value; }); } function build_eval_context (record) { var field_values = rawify(_.extend({}, record.data, { active_model: record.model })); if ('id' in record.data) { field_values.active_id = record.data.id; field_values.active_ids = [record.data.id]; } // FIXME: parent? return new Context(field_values); } function build_context(record, context) { context = context || {}; var eval_context = build_eval_context(record); return new Context(context).set_eval_context(eval_context); } function build_domain(record, domain) { domain = domain || []; var eval_context = build_eval_context(record); return new CompoundDomain(domain).set_eval_context(eval_context); } return { Query: Query, DataSet: DataSet, DataSetStatic: DataSetStatic, DataSetSearch: DataSetSearch, CompoundDomain: CompoundDomain, noDisplayContent: "" + _t("Unnamed") + "", build_context: build_context, build_domain: build_domain, }; });