[FIX] add a correct rounding algorithm to be able to duplicate server-side financial...
[odoo/odoo.git] / addons / web / static / src / js / data.js
1
2 openerp.web.data = function(instance) {
3
4 /**
5  * Serializes the sort criterion array of a dataset into a form which can be
6  * consumed by OpenERP's RPC APIs.
7  *
8  * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting
9  * @returns {String} SQL-like sorting string (``ORDER BY``) clause
10  */
11 instance.web.serialize_sort = function (criterion) {
12     return _.map(criterion,
13         function (criteria) {
14             if (criteria[0] === '-') {
15                 return criteria.slice(1) + ' DESC';
16             }
17             return criteria + ' ASC';
18         }).join(', ');
19 };
20
21 instance.web.Query = instance.web.Class.extend({
22     init: function (model, fields) {
23         this._model = model;
24         this._fields = fields;
25         this._filter = [];
26         this._context = {};
27         this._limit = false;
28         this._offset = 0;
29         this._order_by = [];
30     },
31     clone: function (to_set) {
32         to_set = to_set || {};
33         var q = new instance.web.Query(this._model, this._fields);
34         q._context = this._context;
35         q._filter = this._filter;
36         q._limit = this._limit;
37         q._offset = this._offset;
38         q._order_by = this._order_by;
39
40         for(var key in to_set) {
41             if (!to_set.hasOwnProperty(key)) { continue; }
42             switch(key) {
43             case 'filter':
44                 q._filter = new instance.web.CompoundDomain(
45                         q._filter, to_set.filter);
46                 break;
47             case 'context':
48                 q._context = new instance.web.CompoundContext(
49                         q._context, to_set.context);
50                 break;
51             case 'limit':
52             case 'offset':
53             case 'order_by':
54                 q['_' + key] = to_set[key];
55             }
56         }
57         return q;
58     },
59     _execute: function () {
60         var self = this;
61         return instance.session.rpc('/web/dataset/search_read', {
62             model: this._model.name,
63             fields: this._fields || false,
64             domain: instance.web.pyeval.eval('domains',
65                     [this._model.domain(this._filter)]),
66             context: instance.web.pyeval.eval('contexts',
67                     [this._model.context(this._context)]),
68             offset: this._offset,
69             limit: this._limit,
70             sort: instance.web.serialize_sort(this._order_by)
71         }).then(function (results) {
72             self._count = results.length;
73             return results.records;
74         }, null);
75     },
76     /**
77      * Fetches the first record matching the query, or null
78      *
79      * @returns {jQuery.Deferred<Object|null>}
80      */
81     first: function () {
82         var self = this;
83         return this.clone({limit: 1})._execute().then(function (records) {
84             delete self._count;
85             if (records.length) { return records[0]; }
86             return null;
87         });
88     },
89     /**
90      * Fetches all records matching the query
91      *
92      * @returns {jQuery.Deferred<Array<>>}
93      */
94     all: function () {
95         return this._execute();
96     },
97     /**
98      * Fetches the number of records matching the query in the database
99      *
100      * @returns {jQuery.Deferred<Number>}
101      */
102     count: function () {
103         if (this._count != undefined) { return $.when(this._count); }
104         return this._model.call(
105             'search_count', [this._filter], {
106                 context: this._model.context(this._context)});
107     },
108     /**
109      * Performs a groups read according to the provided grouping criterion
110      *
111      * @param {String|Array<String>} grouping
112      * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
113      */
114     group_by: function (grouping) {
115         if (grouping === undefined) {
116             return null;
117         }
118
119         if (!(grouping instanceof Array)) {
120             grouping = _.toArray(arguments);
121         }
122         if (_.isEmpty(grouping)) { return null; }
123
124         var self = this;
125
126         var ctx = instance.web.pyeval.eval(
127             'context', this._model.context(this._context));
128         return this._model.call('read_group', {
129             groupby: grouping,
130             fields: _.uniq(grouping.concat(this._fields || [])),
131             domain: this._model.domain(this._filter),
132             context: this._model.context(this._context),
133             offset: this._offset,
134             limit: this._limit,
135             orderby: instance.web.serialize_sort(this._order_by) || false
136         }).then(function (results) {
137             return _(results).map(function (result) {
138                 // FIX: querygroup initialization
139                 result.__context = result.__context || {};
140                 result.__context.group_by = result.__context.group_by || [];
141                 _.defaults(result.__context, ctx);
142                 return new instance.web.QueryGroup(
143                     self._model.name, grouping[0], result);
144             });
145         });
146     },
147     /**
148      * Creates a new query with the union of the current query's context and
149      * the new context.
150      *
151      * @param context context data to add to the query
152      * @returns {openerp.web.Query}
153      */
154     context: function (context) {
155         if (!context) { return this; }
156         return this.clone({context: context});
157     },
158     /**
159      * Creates a new query with the union of the current query's filter and
160      * the new domain.
161      *
162      * @param domain domain data to AND with the current query filter
163      * @returns {openerp.web.Query}
164      */
165     filter: function (domain) {
166         if (!domain) { return this; }
167         return this.clone({filter: domain});
168     },
169     /**
170      * Creates a new query with the provided limit replacing the current
171      * query's own limit
172      *
173      * @param {Number} limit maximum number of records the query should retrieve
174      * @returns {openerp.web.Query}
175      */
176     limit: function (limit) {
177         return this.clone({limit: limit});
178     },
179     /**
180      * Creates a new query with the provided offset replacing the current
181      * query's own offset
182      *
183      * @param {Number} offset number of records the query should skip before starting its retrieval
184      * @returns {openerp.web.Query}
185      */
186     offset: function (offset) {
187         return this.clone({offset: offset});
188     },
189     /**
190      * Creates a new query with the provided ordering parameters replacing
191      * those of the current query
192      *
193      * @param {String...} fields ordering clauses
194      * @returns {openerp.web.Query}
195      */
196     order_by: function (fields) {
197         if (fields === undefined) { return this; }
198         if (!(fields instanceof Array)) {
199             fields = _.toArray(arguments);
200         }
201         if (_.isEmpty(fields)) { return this; }
202         return this.clone({order_by: fields});
203     }
204 });
205
206 instance.web.QueryGroup = instance.web.Class.extend({
207     init: function (model, grouping_field, read_group_group) {
208         // In cases where group_by_no_leaf and no group_by, the result of
209         // read_group has aggregate fields but no __context or __domain.
210         // Create default (empty) values for those so that things don't break
211         var fixed_group = _.extend(
212             {__context: {group_by: []}, __domain: []},
213             read_group_group);
214
215         var aggregates = {};
216         _(fixed_group).each(function (value, key) {
217             if (key.indexOf('__') === 0
218                     || key === grouping_field
219                     || key === grouping_field + '_count') {
220                 return;
221             }
222             aggregates[key] = value || 0;
223         });
224
225         this.model = new instance.web.Model(
226             model, fixed_group.__context, fixed_group.__domain);
227
228         var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0;
229         var leaf_group = fixed_group.__context.group_by.length === 0;
230         this.attributes = {
231             folded: !!(fixed_group.__fold),
232             grouped_on: grouping_field,
233             // if terminal group (or no group) and group_by_no_leaf => use group.__count
234             length: group_size,
235             value: fixed_group[grouping_field],
236             // A group is open-able if it's not a leaf in group_by_no_leaf mode
237             has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),
238
239             aggregates: aggregates
240         };
241     },
242     get: function (key) {
243         return this.attributes[key];
244     },
245     subgroups: function () {
246         return this.model.query().group_by(this.model.context().group_by);
247     },
248     query: function () {
249         return this.model.query.apply(this.model, arguments);
250     }
251 });
252
253 instance.web.Model = instance.web.Class.extend({
254     /**
255      * @constructs instance.web.Model
256      * @extends instance.web.Class
257      *
258      * @param {String} model_name name of the OpenERP model this object is bound to
259      * @param {Object} [context]
260      * @param {Array} [domain]
261      */
262     init: function (model_name, context, domain) {
263         this.name = model_name;
264         this._context = context || {};
265         this._domain = domain || [];
266     },
267     /**
268      * @deprecated does not allow to specify kwargs, directly use call() instead
269      */
270     get_func: function (method_name) {
271         var self = this;
272         return function () {
273             return self.call(method_name, _.toArray(arguments));
274         };
275     },
276     /**
277      * Call a method (over RPC) on the bound OpenERP model.
278      *
279      * @param {String} method name of the method to call
280      * @param {Array} [args] positional arguments
281      * @param {Object} [kwargs] keyword arguments
282      * @param {Object} [options] additional options for the rpc() method
283      * @returns {jQuery.Deferred<>} call result
284      */
285     call: function (method, args, kwargs, options) {
286         args = args || [];
287         kwargs = kwargs || {};
288         if (!_.isArray(args)) {
289             // call(method, kwargs)
290             kwargs = args;
291             args = [];
292         }
293         instance.web.pyeval.ensure_evaluated(args, kwargs);
294         var debug = instance.session.debug ? '/'+this.name+':'+method : '';
295         return instance.session.rpc('/web/dataset/call_kw' + debug, {
296             model: this.name,
297             method: method,
298             args: args,
299             kwargs: kwargs
300         }, options);
301     },
302     /**
303      * Fetches a Query instance bound to this model, for searching
304      *
305      * @param {Array<String>} [fields] fields to ultimately fetch during the search
306      * @returns {instance.web.Query}
307      */
308     query: function (fields) {
309         return new instance.web.Query(this, fields);
310     },
311     /**
312      * Executes a signal on the designated workflow, on the bound OpenERP model
313      *
314      * @param {Number} id workflow identifier
315      * @param {String} signal signal to trigger on the workflow
316      */
317     exec_workflow: function (id, signal) {
318         return instance.session.rpc('/web/dataset/exec_workflow', {
319             model: this.name,
320             id: id,
321             signal: signal
322         });
323     },
324     /**
325      * Fetches the model's domain, combined with the provided domain if any
326      *
327      * @param {Array} [domain] to combine with the model's internal domain
328      * @returns The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
329      */
330     domain: function (domain) {
331         if (!domain) { return this._domain; }
332         return new instance.web.CompoundDomain(
333             this._domain, domain);
334     },
335     /**
336      * Fetches the combination of the user's context and the domain context,
337      * combined with the provided context if any
338      *
339      * @param {Object} [context] to combine with the model's internal context
340      * @returns The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
341      */
342     context: function (context) {
343         return new instance.web.CompoundContext(
344             instance.session.user_context, this._context, context || {});
345     },
346     /**
347      * Button action caller, needs to perform cleanup if an action is returned
348      * from the button (parsing of context and domain, and fixup of the views
349      * collection for act_window actions)
350      *
351      * FIXME: remove when evaluator integrated
352      */
353     call_button: function (method, args) {
354         instance.web.pyeval.ensure_evaluated(args, {});
355         return instance.session.rpc('/web/dataset/call_button', {
356             model: this.name,
357             method: method,
358             // Should not be necessary anymore. Integrate remote in this?
359             domain_id: null,
360             context_id: args.length - 1,
361             args: args || []
362         });
363     },
364 });
365
366 instance.web.DataSet =  instance.web.Class.extend(instance.web.PropertiesMixin, {
367     /**
368      * Collection of OpenERP records, used to share records and the current selection between views.
369      *
370      * @constructs instance.web.DataSet
371      *
372      * @param {String} model the OpenERP model this dataset will manage
373      */
374     init: function(parent, model, context) {
375         instance.web.PropertiesMixin.init.call(this);
376         this.model = model;
377         this.context = context || {};
378         this.index = null;
379         this._sort = [];
380         this._model = new instance.web.Model(model, context);
381     },
382     previous: function () {
383         this.index -= 1;
384         if (!this.ids.length) {
385             this.index = null;
386         } else if (this.index < 0) {
387             this.index = this.ids.length - 1;
388         }
389         return this;
390     },
391     next: function () {
392         this.index += 1;
393         if (!this.ids.length) {
394             this.index = null;
395         } else if (this.index >= this.ids.length) {
396             this.index = 0;
397         }
398         return this;
399     },
400     select_id: function(id) {
401         var idx = this.get_id_index(id);
402         if (idx === null) {
403             return false;
404         } else {
405             this.index = idx;
406             return true;
407         }
408     },
409     get_id_index: function(id) {
410         for (var i=0, ii=this.ids.length; i<ii; i++) {
411             // Here we use type coercion because of the mess potentially caused by
412             // OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
413             // OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
414             if (id == this.ids[i]) {
415                 return i;
416             }
417         }
418         return null;
419     },
420     /**
421      * Read records.
422      *
423      * @param {Array} ids identifiers of the records to read
424      * @param {Array} fields fields to read and return, by default all fields are returned
425      * @returns {$.Deferred}
426      */
427     read_ids: function (ids, fields, options) {
428         options = options || {};
429         // TODO: reorder results to match ids list
430         return this._model.call('read',
431             [ids, fields || false],
432             {context: this._model.context(options.context)});
433     },
434     /**
435      * Read a slice of the records represented by this DataSet, based on its
436      * domain and context.
437      *
438      * @param {Array} [fields] fields to read and return, by default all fields are returned
439      * @params {Object} [options]
440      * @param {Number} [options.offset=0] The index from which selected records should be returned
441      * @param {Number} [options.limit=null] The maximum number of records to return
442      * @returns {$.Deferred}
443      */
444     read_slice: function (fields, options) {
445         var self = this;
446         options = options || {};
447         return this._model.query(fields)
448                 .limit(options.limit || false)
449                 .offset(options.offset || 0)
450                 .all().done(function (records) {
451             self.ids = _(records).pluck('id');
452         });
453     },
454     /**
455      * Reads the current dataset record (from its index)
456      *
457      * @params {Array} [fields] fields to read and return, by default all fields are returned
458      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
459      * @returns {$.Deferred}
460      */
461     read_index: function (fields, options) {
462         options = options || {};
463         return this.read_ids([this.ids[this.index]], fields, options).then(function (records) {
464             if (_.isEmpty(records)) { return $.Deferred().reject().promise(); }
465             return records[0];
466         });
467     },
468     /**
469      * Reads default values for the current model
470      *
471      * @param {Array} [fields] fields to get default values for, by default all defaults are read
472      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
473      * @returns {$.Deferred}
474      */
475     default_get: function(fields, options) {
476         options = options || {};
477         return this._model.call('default_get',
478             [fields], {context: this.get_context(options.context)});
479     },
480     /**
481      * Creates a new record in db
482      *
483      * @param {Object} data field values to set on the new record
484      * @param {Object} options Dictionary that can contain the following keys:
485      *   - readonly_fields: Values from readonly fields that were updated by
486      *     on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
487      * @returns {$.Deferred}
488      */
489     create: function(data, options) {
490         return this._model.call('create', [data], {context: this.get_context()});
491     },
492     /**
493      * Saves the provided data in an existing db record
494      *
495      * @param {Number|String} id identifier for the record to alter
496      * @param {Object} data field values to write into the record
497      * @param {Object} options Dictionary that can contain the following keys:
498      *   - context: The context to use in the server-side call.
499      *   - readonly_fields: Values from readonly fields that were updated by
500      *     on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
501      * @returns {$.Deferred}
502      */
503     write: function (id, data, options) {
504         options = options || {};
505         return this._model.call('write', [[id], data], {context: this.get_context(options.context)}).done(this.trigger('dataset_changed', id, data, options));
506     },
507     /**
508      * Deletes an existing record from the database
509      *
510      * @param {Number|String} ids identifier of the record to delete
511      */
512     unlink: function(ids) {
513         return this._model.call('unlink', [ids], {context: this.get_context()}).done(this.trigger('dataset_changed', ids));
514     },
515     /**
516      * Calls an arbitrary RPC method
517      *
518      * @param {String} method name of the method (on the current model) to call
519      * @param {Array} [args] arguments to pass to the method
520      * @param {Function} callback
521      * @param {Function} error_callback
522      * @returns {$.Deferred}
523      */
524     call: function (method, args) {
525         return this._model.call(method, args);
526     },
527     /**
528      * Calls a button method, usually returning some sort of action
529      *
530      * @param {String} method
531      * @param {Array} [args]
532      * @returns {$.Deferred}
533      */
534     call_button: function (method, args) {
535         return this._model.call_button(method, args);
536     },
537     /**
538      * Fetches the "readable name" for records, based on intrinsic rules
539      *
540      * @param {Array} ids
541      * @returns {$.Deferred}
542      */
543     name_get: function(ids) {
544         return this._model.call('name_get', [ids], {context: this.get_context()});
545     },
546     /**
547      * 
548      * @param {String} name name to perform a search for/on
549      * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
550      * @param {String} [operator='ilike'] matching operator to use with the provided name value
551      * @param {Number} [limit=0] maximum number of matches to return
552      * @param {Function} callback function to call with name_search result
553      * @returns {$.Deferred}
554      */
555     name_search: function (name, domain, operator, limit) {
556         return this._model.call('name_search', {
557             name: name || '',
558             args: domain || false,
559             operator: operator || 'ilike',
560             context: this._model.context(),
561             limit: limit || 0
562         });
563     },
564     /**
565      * @param name
566      */
567     name_create: function(name) {
568         return this._model.call('name_create', [name], {context: this.get_context()});
569     },
570     exec_workflow: function (id, signal) {
571         return this._model.exec_workflow(id, signal);
572     },
573     get_context: function(request_context) {
574         return this._model.context(request_context);
575     },
576     /**
577      * Reads or changes sort criteria on the dataset.
578      *
579      * If not provided with any argument, serializes the sort criteria to
580      * an SQL-like form usable by OpenERP's ORM.
581      *
582      * If given a field, will set that field as first sorting criteria or,
583      * if the field is already the first sorting criteria, will reverse it.
584      *
585      * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
586      * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
587      * @returns {String|undefined}
588      */
589     sort: function (field, force_reverse) {
590         if (!field) {
591             return instance.web.serialize_sort(this._sort);
592         }
593         var reverse = force_reverse || (this._sort[0] === field);
594         this._sort.splice.apply(
595             this._sort, [0, this._sort.length].concat(
596                 _.without(this._sort, field, '-' + field)));
597
598         this._sort.unshift((reverse ? '-' : '') + field);
599         return undefined;
600     },
601     size: function () {
602         return this.ids.length;
603     },
604     alter_ids: function(n_ids) {
605         this.ids = n_ids;
606     },
607     /**
608      * Resequence records.
609      *
610      * @param {Array} ids identifiers of the records to resequence
611      * @returns {$.Deferred}
612      */
613     resequence: function (ids, options) {
614         options = options || {};
615         return instance.session.rpc('/web/dataset/resequence', {
616             model: this.model,
617             ids: ids,
618             context: instance.web.pyeval.eval(
619                 'context', this.get_context(options.context)),
620         }).then(function (results) {
621             return results;
622         });
623     },
624 });
625
626 instance.web.DataSetStatic =  instance.web.DataSet.extend({
627     init: function(parent, model, context, ids) {
628         var self = this;
629         this._super(parent, model, context);
630         // all local records
631         this.ids = ids || [];
632     },
633     read_slice: function (fields, options) {
634         options = options || {};
635         fields = fields || {};
636         var offset = options.offset || 0,
637             limit = options.limit || false;
638         var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
639         return this.read_ids(this.ids.slice(offset, end_pos), fields);
640     },
641     set_ids: function (ids) {
642         this.ids = ids;
643         if (ids.length === 0) {
644             this.index = null;
645         } else if (this.index >= ids.length - 1) {
646             this.index = ids.length - 1;
647         }
648     },
649     unlink: function(ids) {
650         this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
651         this.trigger('unlink', ids);
652         return $.Deferred().resolve({result: true});
653     },
654 });
655
656 instance.web.DataSetSearch =  instance.web.DataSet.extend({
657     /**
658      * @constructs instance.web.DataSetSearch
659      * @extends instance.web.DataSet
660      *
661      * @param {Object} parent
662      * @param {String} model
663      * @param {Object} context
664      * @param {Array} domain
665      */
666     init: function(parent, model, context, domain) {
667         this._super(parent, model, context);
668         this.domain = domain || [];
669         this._length = null;
670         this.ids = [];
671         this._model = new instance.web.Model(model, context, domain);
672     },
673     /**
674      * Read a slice of the records represented by this DataSet, based on its
675      * domain and context.
676      *
677      * @params {Object} options
678      * @param {Array} [options.fields] fields to read and return, by default all fields are returned
679      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
680      * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
681      * @param {Number} [options.offset=0] The index from which selected records should be returned
682      * @param {Number} [options.limit=null] The maximum number of records to return
683      * @returns {$.Deferred}
684      */
685     read_slice: function (fields, options) {
686         options = options || {};
687         var self = this;
688         var q = this._model.query(fields || false)
689             .filter(options.domain)
690             .context(options.context)
691             .offset(options.offset || 0)
692             .limit(options.limit || false);
693         q = q.order_by.apply(q, this._sort);
694
695         return q.all().done(function (records) {
696             // FIXME: not sure about that one, *could* have discarded count
697             q.count().done(function (count) { self._length = count; });
698             self.ids = _(records).pluck('id');
699         });
700     },
701     get_domain: function (other_domain) {
702         this._model.domain(other_domain);
703     },
704     unlink: function(ids, callback, error_callback) {
705         var self = this;
706         return this._super(ids).done(function(result) {
707             self.ids = _(self.ids).difference(ids);
708             if (self._length) {
709                 self._length -= 1;
710             }
711             if (self.index !== null) {
712                 self.index = self.index <= self.ids.length - 1 ?
713                     self.index : (self.ids.length > 0 ? self.ids.length -1 : 0);
714             }
715             self.trigger("dataset_changed", ids, callback, error_callback);
716         });
717     },
718     size: function () {
719         if (this._length != null) {
720             return this._length;
721         }
722         return this._super();
723     }
724 });
725
726 instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
727     virtual_id_prefix: "one2many_v_id_",
728     debug_mode: true,
729     init: function() {
730         this._super.apply(this, arguments);
731         this.reset_ids([]);
732         this.last_default_get = {};
733     },
734     default_get: function(fields, options) {
735         var self = this;
736         return this._super(fields, options).done(function(res) {
737             self.last_default_get = res;
738         });
739     },
740     create: function(data, options) {
741         var cached = {
742             id:_.uniqueId(this.virtual_id_prefix),
743             values: _.extend({}, data, (options || {}).readonly_fields || {}),
744             defaults: this.last_default_get
745         };
746         this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
747         this.cache.push(cached);
748         return $.Deferred().resolve(cached.id).promise();
749     },
750     write: function (id, data, options) {
751         var self = this;
752         var record = _.detect(this.to_create, function(x) {return x.id === id;});
753         record = record || _.detect(this.to_write, function(x) {return x.id === id;});
754         var dirty = false;
755         if (record) {
756             for (var k in data) {
757                 if (record.values[k] === undefined || record.values[k] !== data[k]) {
758                     dirty = true;
759                     break;
760                 }
761             }
762             $.extend(record.values, data);
763         } else {
764             dirty = true;
765             record = {id: id, values: data};
766             self.to_write.push(record);
767         }
768         var cached = _.detect(this.cache, function(x) {return x.id === id;});
769         if (!cached) {
770             cached = {id: id, values: {}};
771             this.cache.push(cached);
772         }
773         $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
774         if (dirty)
775             this.trigger("dataset_changed", id, data, options);
776         return $.Deferred().resolve(true).promise();
777     },
778     unlink: function(ids, callback, error_callback) {
779         var self = this;
780         _.each(ids, function(id) {
781             if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
782                 self.to_delete.push({id: id})
783             }
784         });
785         this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
786         this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
787         this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
788         this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
789         this.trigger("dataset_changed", ids, callback, error_callback);
790         return $.async_when({result: true}).done(callback);
791     },
792     reset_ids: function(ids) {
793         this.set_ids(ids);
794         this.to_delete = [];
795         this.to_create = [];
796         this.to_write = [];
797         this.cache = [];
798         this.delete_all = false;
799     },
800     read_ids: function (ids, fields, options) {
801         var self = this;
802         var to_get = [];
803         _.each(ids, function(id) {
804             var cached = _.detect(self.cache, function(x) {return x.id === id;});
805             var created = _.detect(self.to_create, function(x) {return x.id === id;});
806             if (created) {
807                 _.each(fields, function(x) {if (cached.values[x] === undefined)
808                     cached.values[x] = created.defaults[x] || false;});
809             } else {
810                 if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
811                     to_get.push(id);
812             }
813         });
814         var completion = $.Deferred();
815         var return_records = function() {
816             var records = _.map(ids, function(id) {
817                 return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
818             });
819             if (self.debug_mode) {
820                 if (_.include(records, undefined)) {
821                     throw "Record not correctly loaded";
822                 }
823             }
824             var sort_fields = self._sort,
825                     compare = function (v1, v2) {
826                         return (v1 < v2) ? -1
827                              : (v1 > v2) ? 1
828                              : 0;
829                     };
830             // Array.sort is not necessarily stable. We must be careful with this because
831             // sorting an array where all items are considered equal is a worst-case that
832             // will randomize the array with an unstable sort! Therefore we must avoid
833             // sorting if there are no sort_fields (i.e. all items are considered equal)
834             // See also: http://ecma262-5.com/ELS5_Section_15.htm#Section_15.4.4.11 
835             //           http://code.google.com/p/v8/issues/detail?id=90
836             if (sort_fields.length) {
837                 records.sort(function (a, b) {
838                     return _.reduce(sort_fields, function (acc, field) {
839                         if (acc) { return acc; }
840                         var sign = 1;
841                         if (field[0] === '-') {
842                             sign = -1;
843                             field = field.slice(1);
844                         }
845                         return sign * compare(a[field], b[field]);
846                     }, 0);
847                 });
848             }
849             completion.resolve(records);
850         };
851         if(to_get.length > 0) {
852             var rpc_promise = this._super(to_get, fields, options).done(function(records) {
853                 _.each(records, function(record, index) {
854                     var id = to_get[index];
855                     var cached = _.detect(self.cache, function(x) {return x.id === id;});
856                     if (!cached) {
857                         self.cache.push({id: id, values: record});
858                     } else {
859                         // I assume cache value is prioritary
860                         cached.values = _.defaults(_.clone(cached.values), record);
861                     }
862                 });
863                 return_records();
864             });
865             $.when(rpc_promise).fail(function() {completion.reject();});
866         } else {
867             return_records();
868         }
869         return completion.promise();
870     },
871     /**
872      * Invalidates caching of a record in the dataset to ensure the next read
873      * of that record will hit the server.
874      *
875      * Of use when an action is going to remote-alter a record which will then
876      * need to be reloaded, e.g. action button.
877      *
878      * @param {Object} id record to remove from the BDS's cache
879      */
880     evict_record: function (id) {
881         for(var i=0, len=this.cache.length; i<len; ++i) {
882             var record = this.cache[i];
883             // if record we call the button upon is in the cache
884             if (record.id === id) {
885                 // evict it so it gets reloaded from server
886                 this.cache.splice(i, 1);
887                 break;
888             }
889         }
890     },
891     call_button: function (method, args) {
892         this.evict_record(args[0][0]);
893         return this._super(method, args);
894     },
895     exec_workflow: function (id, signal) {
896         this.evict_record(id);
897         return this._super(id, signal);
898     },
899     alter_ids: function(n_ids) {
900         this._super(n_ids);
901         this.trigger("dataset_changed", n_ids);
902     },
903 });
904 instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
905
906 instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
907     init: function() {
908         this._super.apply(this, arguments);
909         this.create_function = null;
910         this.write_function = null;
911         this.read_function = null;
912         this.default_get_function = null;
913         this.unlink_function = null;
914     },
915     read_ids: function (ids, fields, options) {
916         if (this.read_function) {
917             return this.read_function(ids, fields, options, this._super);
918         } else {
919             return this._super.apply(this, arguments);
920         }
921     },
922     default_get: function(fields, options) {
923         if (this.default_get_function) {
924             return this.default_get_function(fields, options, this._super);
925         } else {
926             return this._super.apply(this, arguments);
927         }
928     },
929     create: function(data, options) {
930         if (this.create_function) {
931             return this.create_function(data, options, this._super);
932         } else {
933             return this._super.apply(this, arguments);
934         }
935     },
936     write: function (id, data, options) {
937         if (this.write_function) {
938             return this.write_function(id, data, options, this._super);
939         } else {
940             return this._super.apply(this, arguments);
941         }
942     },
943     unlink: function(ids) {
944         if (this.unlink_function) {
945             return this.unlink_function(ids, this._super);
946         } else {
947             return this._super.apply(this, arguments);
948         }
949     },
950 });
951
952 instance.web.CompoundContext = instance.web.Class.extend({
953     init: function () {
954         this.__ref = "compound_context";
955         this.__contexts = [];
956         this.__eval_context = null;
957         var self = this;
958         _.each(arguments, function(x) {
959             self.add(x);
960         });
961     },
962     add: function (context) {
963         this.__contexts.push(context);
964         return this;
965     },
966     set_eval_context: function (eval_context) {
967         this.__eval_context = eval_context;
968         return this;
969     },
970     get_eval_context: function () {
971         return this.__eval_context;
972     },
973     eval: function() {
974         return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true});
975     },
976 });
977
978 instance.web.CompoundDomain = instance.web.Class.extend({
979     init: function () {
980         this.__ref = "compound_domain";
981         this.__domains = [];
982         this.__eval_context = null;
983         var self = this;
984         _.each(arguments, function(x) {
985             self.add(x);
986         });
987     },
988     add: function(domain) {
989         this.__domains.push(domain);
990         return this;
991     },
992     set_eval_context: function(eval_context) {
993         this.__eval_context = eval_context;
994         return this;
995     },
996     get_eval_context: function() {
997         return this.__eval_context;
998     },
999     eval: function() {
1000         return instance.web.pyeval.eval('domain', this);
1001     },
1002 });
1003
1004 instance.web.DropMisordered = instance.web.Class.extend({
1005     /**
1006      * @constructs instance.web.DropMisordered
1007      * @extends instance.web.Class
1008      *
1009      * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored
1010      */
1011     init: function (failMisordered) {
1012         // local sequence number, for requests sent
1013         this.lsn = 0;
1014         // remote sequence number, seqnum of last received request
1015         this.rsn = -1;
1016         this.failMisordered = failMisordered || false;
1017     },
1018     /**
1019      * Adds a deferred (usually an async request) to the sequencer
1020      *
1021      * @param {$.Deferred} deferred to ensure add
1022      * @returns {$.Deferred}
1023      */
1024     add: function (deferred) {
1025         var res = $.Deferred();
1026
1027         var self = this, seq = this.lsn++;
1028         deferred.done(function () {
1029             if (seq > self.rsn) {
1030                 self.rsn = seq;
1031                 res.resolve.apply(res, arguments);
1032             } else if (self.failMisordered) {
1033                 res.reject();
1034             }
1035         }).fail(function () {
1036             res.reject.apply(res, arguments);
1037         });
1038
1039         return res.promise();
1040     }
1041 });
1042
1043 };
1044
1045 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: