[FIX] OPW 578531 Correctly invalidate BufferedDatasetCache when triggering an action...
[odoo/odoo.git] / addons / web / static / src / js / data.js
1
2 openerp.web.data = function(openerp) {
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 openerp.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 openerp.web.DataGroup =  openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{
22     /**
23      * Management interface between views and grouped collections of OpenERP
24      * records.
25      *
26      * The root DataGroup is instantiated with the relevant information
27      * (a session, a model, a domain, a context and a group_by sequence), the
28      * domain and context may be empty. It is then interacted with via
29      * :js:func:`~openerp.web.DataGroup.list`, which is used to read the
30      * content of the current grouping level.
31      *
32      * @constructs openerp.web.DataGroup
33      * @extends openerp.web.OldWidget
34      *
35      * @param {openerp.web.OldWidget} parent widget
36      * @param {String} model name of the model managed by this DataGroup
37      * @param {Array} domain search domain for this DataGroup
38      * @param {Object} context context of the DataGroup's searches
39      * @param {Array} group_by sequence of fields by which to group
40      * @param {Number} [level=0] nesting level of the group
41      */
42     init: function(parent, model, domain, context, group_by, level) {
43         this._super(parent, null);
44         if (group_by) {
45             if (group_by.length || context['group_by_no_leaf']) {
46                 return new openerp.web.ContainerDataGroup( this, model, domain, context, group_by, level);
47             } else {
48                 return new openerp.web.GrouplessDataGroup( this, model, domain, context, level);
49             }
50         }
51
52         this.model = model;
53         this.context = context;
54         this.domain = domain;
55
56         this.level = level || 0;
57     },
58     cls: 'DataGroup'
59 });
60 openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.ContainerDataGroup# */ {
61     /**
62      *
63      * @constructs openerp.web.ContainerDataGroup
64      * @extends openerp.web.DataGroup
65      *
66      * @param session
67      * @param model
68      * @param domain
69      * @param context
70      * @param group_by
71      * @param level
72      */
73     init: function (parent, model, domain, context, group_by, level) {
74         this._super(parent, model, domain, context, null, level);
75
76         this.group_by = group_by;
77     },
78     /**
79      * The format returned by ``read_group`` is absolutely dreadful:
80      *
81      * * A ``__context`` key provides future grouping levels
82      * * A ``__domain`` key provides the domain for the next search
83      * * The current grouping value is provided through the name of the
84      *   current grouping name e.g. if currently grouping on ``user_id``, then
85      *   the ``user_id`` value for this group will be provided through the
86      *   ``user_id`` key.
87      * * Similarly, the number of items in the group (not necessarily direct)
88      *   is provided via ``${current_field}_count``
89      * * Other aggregate fields are just dumped there
90      *
91      * This function slightly improves the grouping records by:
92      *
93      * * Adding a ``grouped_on`` property providing the current grouping field
94      * * Adding a ``value`` and a ``length`` properties which replace the
95      *   ``$current_field`` and ``${current_field}_count`` ones
96      * * Moving aggregate values into an ``aggregates`` property object
97      *
98      * Context and domain keys remain as-is, they should not be used externally
99      * but in case they're needed...
100      *
101      * @param {Object} group ``read_group`` record
102      */
103     transform_group: function (group) {
104         var field_name = this.group_by[0];
105         // In cases where group_by_no_leaf and no group_by, the result of
106         // read_group has aggregate fields but no __context or __domain.
107         // Create default (empty) values for those so that things don't break
108         var fixed_group = _.extend(
109                 {__context: {group_by: []}, __domain: []},
110                 group);
111
112         var aggregates = {};
113         _(fixed_group).each(function (value, key) {
114             if (key.indexOf('__') === 0
115                     || key === field_name
116                     || key === field_name + '_count') {
117                 return;
118             }
119             aggregates[key] = value || 0;
120         });
121
122         var group_size = fixed_group[field_name + '_count'] || fixed_group.__count || 0;
123         var leaf_group = fixed_group.__context.group_by.length === 0;
124         return {
125             __context: fixed_group.__context,
126             __domain: fixed_group.__domain,
127
128             grouped_on: field_name,
129             // if terminal group (or no group) and group_by_no_leaf => use group.__count
130             length: group_size,
131             value: fixed_group[field_name],
132             // A group is openable if it's not a leaf in group_by_no_leaf mode
133             openable: !(leaf_group && this.context['group_by_no_leaf']),
134
135             aggregates: aggregates
136         };
137     },
138     fetch: function (fields) {
139         // internal method
140         var d = new $.Deferred();
141         var self = this;
142
143         this.rpc('/web/group/read', {
144             model: this.model,
145             context: this.context,
146             domain: this.domain,
147             fields: _.uniq(this.group_by.concat(fields)),
148             group_by_fields: this.group_by,
149             sort: openerp.web.serialize_sort(this.sort)
150         }, function () { }).then(function (response) {
151             var data_groups = _(response).map(
152                     _.bind(self.transform_group, self));
153             self.groups = data_groups;
154             d.resolveWith(self, [data_groups]);
155         }, function () {
156             d.rejectWith.apply(d, [self, arguments]);
157         });
158         return d.promise();
159     },
160     /**
161      * The items of a list have the following properties:
162      *
163      * ``length``
164      *     the number of records contained in the group (and all of its
165      *     sub-groups). This does *not* provide the size of the "next level"
166      *     of the group, unless the group is terminal (no more groups within
167      *     it).
168      * ``grouped_on``
169      *     the name of the field this level was grouped on, this is mostly
170      *     used for display purposes, in order to know the name of the current
171      *     level of grouping. The ``grouped_on`` should be the same for all
172      *     objects of the list.
173      * ``value``
174      *     the value which led to this group (this is the value all contained
175      *     records have for the current ``grouped_on`` field name).
176      * ``aggregates``
177      *     a mapping of other aggregation fields provided by ``read_group``
178      *
179      * @param {Array} fields the list of fields to aggregate in each group, can be empty
180      * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters.
181      * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter
182      */
183     list: function (fields, ifGroups, ifRecords) {
184         var self = this;
185         this.fetch(fields).then(function (group_records) {
186             ifGroups(_(group_records).map(function (group) {
187                 var child_context = _.extend({}, self.context, group.__context);
188                 return _.extend(
189                     new openerp.web.DataGroup(
190                         self, self.model, group.__domain,
191                         child_context, child_context.group_by,
192                         self.level + 1),
193                     group, {sort: self.sort});
194             }));
195         });
196     }
197 });
198 openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.GrouplessDataGroup# */ {
199     /**
200      *
201      * @constructs openerp.web.GrouplessDataGroup
202      * @extends openerp.web.DataGroup
203      *
204      * @param session
205      * @param model
206      * @param domain
207      * @param context
208      * @param level
209      */
210     init: function (parent, model, domain, context, level) {
211         this._super(parent, model, domain, context, null, level);
212     },
213     list: function (fields, ifGroups, ifRecords) {
214         ifRecords(_.extend(
215             new openerp.web.DataSetSearch(this, this.model),
216             {domain: this.domain, context: this.context, _sort: this.sort}));
217     }
218 });
219 openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ {
220     /**
221      * A specialization of groupless data groups, relying on a single static
222      * dataset as its records provider.
223      *
224      * @constructs openerp.web.StaticDataGroup
225      * @extends openerp.web.GrouplessDataGroup
226      * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups
227      */
228     init: function (dataset) {
229         this.dataset = dataset;
230     },
231     list: function (fields, ifGroups, ifRecords) {
232         ifRecords(this.dataset);
233     }
234 });
235
236 openerp.web.DataSet =  openerp.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{
237     /**
238      * DateaManagement interface between views and the collection of selected
239      * OpenERP records (represents the view's state?)
240      *
241      * @constructs openerp.web.DataSet
242      * @extends openerp.web.OldWidget
243      *
244      * @param {String} model the OpenERP model this dataset will manage
245      */
246     init: function(parent, model, context) {
247         this._super(parent);
248         this.model = model;
249         this.context = context || {};
250         this.index = null;
251         this._sort = [];
252     },
253     previous: function () {
254         this.index -= 1;
255         if (!this.ids.length) {
256             this.index = null;
257         } else if (this.index < 0) {
258             this.index = this.ids.length - 1;
259         }
260         return this;
261     },
262     next: function () {
263         this.index += 1;
264         if (!this.ids.length) {
265             this.index = null;
266         } else if (this.index >= this.ids.length) {
267             this.index = 0;
268         }
269         return this;
270     },
271     select_id: function(id) {
272         var idx = this.get_id_index(id);
273         if (idx === null) {
274             return false;
275         } else {
276             this.index = idx;
277             return true;
278         }
279     },
280     get_id_index: function(id) {
281         for (var i=0, ii=this.ids.length; i<ii; i++) {
282             // Here we use type coercion because of the mess potentially caused by
283             // OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
284             // OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
285             if (id == this.ids[i]) {
286                 return i;
287             }
288         }
289         return null;
290     },
291     /**
292      * Read records.
293      *
294      * @param {Array} ids identifiers of the records to read
295      * @param {Array} fields fields to read and return, by default all fields are returned
296      * @returns {$.Deferred}
297      */
298     read_ids: function (ids, fields, options) {
299         var options = options || {};
300         return this.rpc('/web/dataset/get', {
301             model: this.model,
302             ids: ids,
303             fields: fields,
304             context: this.get_context(options.context)
305         });
306     },
307     /**
308      * Read a slice of the records represented by this DataSet, based on its
309      * domain and context.
310      *
311      * @param {Array} [fields] fields to read and return, by default all fields are returned
312      * @params {Object} [options]
313      * @param {Number} [options.offset=0] The index from which selected records should be returned
314      * @param {Number} [options.limit=null] The maximum number of records to return
315      * @returns {$.Deferred}
316      */
317     read_slice: function (fields, options) {
318         return null;
319     },
320     /**
321      * Reads the current dataset record (from its index)
322      *
323      * @params {Array} [fields] fields to read and return, by default all fields are returned
324      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
325      * @returns {$.Deferred}
326      */
327     read_index: function (fields, options) {
328         var def = $.Deferred();
329         if (_.isEmpty(this.ids)) {
330             def.reject();
331         } else {
332             fields = fields || false;
333             this.read_ids([this.ids[this.index]], fields, options).then(function(records) {
334                 def.resolve(records[0]);
335             }, function() {
336                 def.reject.apply(def, arguments);
337             });
338         }
339         return def.promise();
340     },
341     /**
342      * Reads default values for the current model
343      *
344      * @param {Array} [fields] fields to get default values for, by default all defaults are read
345      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
346      * @returns {$.Deferred}
347      */
348     default_get: function(fields, options) {
349         var options = options || {};
350         return this.rpc('/web/dataset/default_get', {
351             model: this.model,
352             fields: fields,
353             context: this.get_context(options.context)
354         });
355     },
356     /**
357      * Creates a new record in db
358      *
359      * @param {Object} data field values to set on the new record
360      * @param {Function} callback function called with operation result
361      * @param {Function} error_callback function called in case of creation error
362      * @returns {$.Deferred}
363      */
364     create: function(data, callback, error_callback) {
365         return this.rpc('/web/dataset/create', {
366             model: this.model,
367             data: data,
368             context: this.get_context()
369         }, callback, error_callback);
370     },
371     /**
372      * Saves the provided data in an existing db record
373      *
374      * @param {Number|String} id identifier for the record to alter
375      * @param {Object} data field values to write into the record
376      * @param {Function} callback function called with operation result
377      * @param {Function} error_callback function called in case of write error
378      * @returns {$.Deferred}
379      */
380     write: function (id, data, options, callback, error_callback) {
381         options = options || {};
382         return this.rpc('/web/dataset/save', {
383             model: this.model,
384             id: id,
385             data: data,
386             context: this.get_context(options.context)
387         }, callback, error_callback);
388     },
389     /**
390      * Deletes an existing record from the database
391      *
392      * @param {Number|String} ids identifier of the record to delete
393      * @param {Function} callback function called with operation result
394      * @param {Function} error_callback function called in case of deletion error
395      */
396     unlink: function(ids, callback, error_callback) {
397         var self = this;
398         return this.call_and_eval("unlink", [ids, this.get_context()], null, 1,
399             callback, error_callback);
400     },
401     /**
402      * Calls an arbitrary RPC method
403      *
404      * @param {String} method name of the method (on the current model) to call
405      * @param {Array} [args] arguments to pass to the method
406      * @param {Function} callback
407      * @param {Function} error_callback
408      * @returns {$.Deferred}
409      */
410     call: function (method, args, callback, error_callback) {
411         return this.rpc('/web/dataset/call', {
412             model: this.model,
413             method: method,
414             args: args || []
415         }, callback, error_callback);
416     },
417     /**
418      * Calls an arbitrary method, with more crazy
419      *
420      * @param {String} method
421      * @param {Array} [args]
422      * @param {Number} [domain_index] index of a domain to evaluate in the args array
423      * @param {Number} [context_index] index of a context to evaluate in the args array
424      * @param {Function} callback
425      * @param {Function} error_callback
426      * @returns {$.Deferred}
427      */
428     call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) {
429         return this.rpc('/web/dataset/call', {
430             model: this.model,
431             method: method,
432             domain_id: domain_index == undefined ? null : domain_index,
433             context_id: context_index == undefined ? null : context_index,
434             args: args || [],
435             // FIXME: API which does not suck for aborting requests in-flight
436             aborter: this
437         }, callback, error_callback);
438     },
439     /**
440      * Calls a button method, usually returning some sort of action
441      *
442      * @param {String} method
443      * @param {Array} [args]
444      * @param {Function} callback
445      * @param {Function} error_callback
446      * @returns {$.Deferred}
447      */
448     call_button: function (method, args, callback, error_callback) {
449         return this.rpc('/web/dataset/call_button', {
450             model: this.model,
451             method: method,
452             domain_id: null,
453             context_id: args.length - 1,
454             args: args || []
455         }, callback, error_callback);
456     },
457     /**
458      * Fetches the "readable name" for records, based on intrinsic rules
459      *
460      * @param {Array} ids
461      * @param {Function} callback
462      * @returns {$.Deferred}
463      */
464     name_get: function(ids, callback) {
465         return this.call_and_eval('name_get', [ids, this.get_context()], null, 1, callback);
466     },
467     /**
468      * 
469      * @param {String} name name to perform a search for/on
470      * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
471      * @param {String} [operator='ilike'] matching operator to use with the provided name value
472      * @param {Number} [limit=0] maximum number of matches to return
473      * @param {Function} callback function to call with name_search result
474      * @returns {$.Deferred}
475      */
476     name_search: function (name, domain, operator, limit, callback) {
477         return this.call_and_eval('name_search',
478             [name || '', domain || false, operator || 'ilike', this.get_context(), limit || 0],
479             1, 3, callback);
480     },
481     /**
482      * @param name
483      * @param callback
484      */
485     name_create: function(name, callback) {
486         return this.call_and_eval('name_create', [name, this.get_context()], null, 1, callback);
487     },
488     exec_workflow: function (id, signal, callback) {
489         return this.rpc('/web/dataset/exec_workflow', {
490             model: this.model,
491             id: id,
492             signal: signal
493         }, callback);
494     },
495     get_context: function(request_context) {
496         if (request_context) {
497             return new openerp.web.CompoundContext(this.context, request_context);
498         }
499         return this.context;
500     },
501     /**
502      * Reads or changes sort criteria on the dataset.
503      *
504      * If not provided with any argument, serializes the sort criteria to
505      * an SQL-like form usable by OpenERP's ORM.
506      *
507      * If given a field, will set that field as first sorting criteria or,
508      * if the field is already the first sorting criteria, will reverse it.
509      *
510      * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
511      * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
512      * @returns {String|undefined}
513      */
514     sort: function (field, force_reverse) {
515         if (!field) {
516             return openerp.web.serialize_sort(this._sort);
517         }
518         var reverse = force_reverse || (this._sort[0] === field);
519         this._sort.splice.apply(
520             this._sort, [0, this._sort.length].concat(
521                 _.without(this._sort, field, '-' + field)));
522
523         this._sort.unshift((reverse ? '-' : '') + field);
524         return undefined;
525     },
526     size: function () {
527         return this.ids.length;
528     },
529     alter_ids: function(n_ids) {
530         this.ids = n_ids;
531     },
532 });
533 openerp.web.DataSetStatic =  openerp.web.DataSet.extend({
534     init: function(parent, model, context, ids) {
535         this._super(parent, model, context);
536         // all local records
537         this.ids = ids || [];
538     },
539     read_slice: function (fields, options) {
540         options = options || {};
541         fields = fields || {};
542         var offset = options.offset || 0,
543             limit = options.limit || false;
544         var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
545         return this.read_ids(this.ids.slice(offset, end_pos), fields);
546     },
547     set_ids: function (ids) {
548         this.ids = ids;
549         if (ids.length === 0) {
550             this.index = null;
551         } else if (this.index >= ids.length - 1) {
552             this.index = ids.length - 1;
553         }
554     },
555     unlink: function(ids) {
556         this.on_unlink(ids);
557         return $.Deferred().resolve({result: true});
558     },
559     on_unlink: function(ids) {
560         this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
561     }
562 });
563 openerp.web.DataSetSearch =  openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{
564     /**
565      * @constructs openerp.web.DataSetSearch
566      * @extends openerp.web.DataSet
567      *
568      * @param {Object} parent
569      * @param {String} model
570      * @param {Object} context
571      * @param {Array} domain
572      */
573     init: function(parent, model, context, domain) {
574         this._super(parent, model, context);
575         this.domain = domain || [];
576         this.offset = 0;
577         this._length;
578         // subset records[offset:offset+limit]
579         // is it necessary ?
580         this.ids = [];
581     },
582     /**
583      * Read a slice of the records represented by this DataSet, based on its
584      * domain and context.
585      *
586      * @params {Object} options
587      * @param {Array} [options.fields] fields to read and return, by default all fields are returned
588      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
589      * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
590      * @param {Number} [options.offset=0] The index from which selected records should be returned
591      * @param {Number} [options.limit=null] The maximum number of records to return
592      * @returns {$.Deferred}
593      */
594     read_slice: function (fields, options) {
595         options = options || {};
596         var self = this;
597         var offset = options.offset || 0;
598         return this.rpc('/web/dataset/search_read', {
599             model: this.model,
600             fields: fields || false,
601             domain: this.get_domain(options.domain),
602             context: this.get_context(options.context),
603             sort: this.sort(),
604             offset: offset,
605             limit: options.limit || false
606         }).pipe(function (result) {
607             self.ids = result.ids;
608             self.offset = offset;
609             self._length = result.length;
610             return result.records;
611         });
612     },
613     get_domain: function (other_domain) {
614         if (other_domain) {
615             return new openerp.web.CompoundDomain(this.domain, other_domain);
616         }
617         return this.domain;
618     },
619     unlink: function(ids, callback, error_callback) {
620         var self = this;
621         return this._super(ids, function(result) {
622             self.ids = _.without.apply(_, [self.ids].concat(ids));
623             if (this.index !== null) {
624                 self.index = self.index <= self.ids.length - 1 ?
625                     self.index : (self.ids.length > 0 ? self.ids.length -1 : 0);
626             }
627             if (callback)
628                 callback(result);
629         }, error_callback);
630     },
631     size: function () {
632         if (this._length !== undefined) {
633             return this._length;
634         }
635         return this._super();
636     }
637 });
638 openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
639     virtual_id_prefix: "one2many_v_id_",
640     debug_mode: true,
641     init: function() {
642         this._super.apply(this, arguments);
643         this.reset_ids([]);
644         this.last_default_get = {};
645     },
646     default_get: function(fields, options) {
647         return this._super(fields, options).then(this.on_default_get);
648     },
649     on_default_get: function(res) {
650         this.last_default_get = res;
651     },
652     create: function(data, callback, error_callback) {
653         var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
654             defaults: this.last_default_get};
655         this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
656         this.cache.push(cached);
657         var prom = $.Deferred().then(callback);
658         prom.resolve({result: cached.id});
659         return prom.promise();
660     },
661     write: function (id, data, options, callback) {
662         var self = this;
663         var record = _.detect(this.to_create, function(x) {return x.id === id;});
664         record = record || _.detect(this.to_write, function(x) {return x.id === id;});
665         var dirty = false;
666         if (record) {
667             for (var k in data) {
668                 if (record.values[k] === undefined || record.values[k] !== data[k]) {
669                     dirty = true;
670                     break;
671                 }
672             }
673             $.extend(record.values, data);
674         } else {
675             dirty = true;
676             record = {id: id, values: data};
677             self.to_write.push(record);
678         }
679         var cached = _.detect(this.cache, function(x) {return x.id === id;});
680         if (!cached) {
681             cached = {id: id, values: {}};
682             this.cache.push(cached);
683         }
684         $.extend(cached.values, record.values);
685         if (dirty)
686             this.on_change();
687         var to_return = $.Deferred().then(callback);
688         to_return.resolve({result: true});
689         return to_return.promise();
690     },
691     unlink: function(ids, callback, error_callback) {
692         var self = this;
693         _.each(ids, function(id) {
694             if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
695                 self.to_delete.push({id: id})
696             }
697         });
698         this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
699         this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
700         this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
701         this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
702         this.on_change();
703         return $.async_when({result: true}).then(callback);
704     },
705     reset_ids: function(ids) {
706         this.set_ids(ids);
707         this.to_delete = [];
708         this.to_create = [];
709         this.to_write = [];
710         this.cache = [];
711         this.delete_all = false;
712     },
713     on_change: function() {},
714     read_ids: function (ids, fields, options) {
715         var self = this;
716         var to_get = [];
717         _.each(ids, function(id) {
718             var cached = _.detect(self.cache, function(x) {return x.id === id;});
719             var created = _.detect(self.to_create, function(x) {return x.id === id;});
720             if (created) {
721                 _.each(fields, function(x) {if (cached.values[x] === undefined)
722                     cached.values[x] = created.defaults[x] || false;});
723             } else {
724                 if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
725                     to_get.push(id);
726             }
727         });
728         var completion = $.Deferred();
729         var return_records = function() {
730             var records = _.map(ids, function(id) {
731                 return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
732             });
733             if (self.debug_mode) {
734                 if (_.include(records, undefined)) {
735                     throw "Record not correctly loaded";
736                 }
737             }
738             var sort_fields = self._sort,
739                     compare = function (v1, v2) {
740                         return (v1 < v2) ? -1
741                              : (v1 > v2) ? 1
742                              : 0;
743                     };
744             // Array.sort is not necessarily stable. We must be careful with this because
745             // sorting an array where all items are considered equal is a worst-case that
746             // will randomize the array with an unstable sort! Therefore we must avoid
747             // sorting if there are no sort_fields (i.e. all items are considered equal)
748             // See also: http://ecma262-5.com/ELS5_Section_15.htm#Section_15.4.4.11 
749             //           http://code.google.com/p/v8/issues/detail?id=90
750             if (sort_fields.length) {
751                 records.sort(function (a, b) {
752                     return _.reduce(sort_fields, function (acc, field) {
753                         if (acc) { return acc; }
754                         var sign = 1;
755                         if (field[0] === '-') {
756                             sign = -1;
757                             field = field.slice(1);
758                         }
759                         return sign * compare(a[field], b[field]);
760                     }, 0);
761                 });
762             }
763             completion.resolve(records);
764         };
765         if(to_get.length > 0) {
766             var rpc_promise = this._super(to_get, fields, options).then(function(records) {
767                 _.each(records, function(record, index) {
768                     var id = to_get[index];
769                     var cached = _.detect(self.cache, function(x) {return x.id === id;});
770                     if (!cached) {
771                         self.cache.push({id: id, values: record});
772                     } else {
773                         // I assume cache value is prioritary
774                         cached.values = _.defaults(_.clone(cached.values), record);
775                     }
776                 });
777                 return_records();
778             });
779             $.when(rpc_promise).fail(function() {completion.reject();});
780         } else {
781             return_records();
782         }
783         return completion.promise();
784     },
785     evict_from_cache: function (id) {
786         for (var i = 0, len = this.cache.length; i < len; ++i) {
787             var record = this.cache[i];
788             // if record we call the button upon is in the cache
789             if (record.id === id) {
790                 // evict it so it gets reloaded from server
791                 this.cache.splice(i, 1);
792                 break;
793             }
794         }
795     },
796     call_button: function (method, args, callback, error_callback) {
797         this.evict_from_cache(args[0][0]);
798         return this._super(method, args, callback, error_callback);
799     },
800     alter_ids: function(n_ids) {
801         this._super(n_ids);
802         this.on_change();
803     },
804 });
805 openerp.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
806
807 openerp.web.ProxyDataSet = openerp.web.DataSetSearch.extend({
808     init: function() {
809         this._super.apply(this, arguments);
810         this.create_function = null;
811         this.write_function = null;
812         this.read_function = null;
813     },
814     read_ids: function () {
815         if (this.read_function) {
816             return this.read_function.apply(null, arguments);
817         } else {
818             return this._super.apply(this, arguments);
819         }
820     },
821     default_get: function(fields, options) {
822         return this._super(fields, options).then(this.on_default_get);
823     },
824     on_default_get: function(result) {},
825     create: function(data, callback, error_callback) {
826         this.on_create(data);
827         if (this.create_function) {
828             return this.create_function(data, callback, error_callback);
829         } else {
830             console.warn("trying to create a record using default proxy dataset behavior");
831             return $.async_when({"result": undefined}).then(callback);
832         }
833     },
834     on_create: function(data) {},
835     write: function (id, data, options, callback) {
836         options = options || {};
837         this.on_write(id, data, options);
838         if (this.write_function) {
839             return this.write_function(id, data, options, callback);
840         } else {
841             console.warn("trying to write a record using default proxy dataset behavior");
842             return $.async_when({"result": true}).then(callback);
843         }
844     },
845     on_write: function(id, data) {},
846     unlink: function(ids, callback, error_callback) {
847         this.on_unlink(ids);
848         console.warn("trying to unlink a record using default proxy dataset behavior");
849         return $.async_when({"result": true}).then(callback);
850     },
851     on_unlink: function(ids) {}
852 });
853
854 openerp.web.Model = openerp.web.CallbackEnabled.extend({
855     init: function(model_name) {
856         this._super();
857         this.model_name = model_name;
858     },
859     rpc: function() {
860         var c = openerp.connection;
861         return c.rpc.apply(c, arguments);
862     },
863     /*
864      * deprecated because it does not allow to specify kwargs, directly use call() instead
865      */
866     get_func: function(method_name) {
867         var self = this;
868         return function() {
869             return self.call(method_name, _.toArray(arguments), {});
870         };
871     },
872     call: function (method, args, kwargs) {
873         return this.rpc('/web/dataset/call_kw', {
874             model: this.model_name,
875             method: method,
876             args: args,
877             kwargs: kwargs
878         });
879     }
880 });
881
882 openerp.web.CompoundContext = openerp.web.Class.extend({
883     init: function () {
884         this.__ref = "compound_context";
885         this.__contexts = [];
886         this.__eval_context = null;
887         var self = this;
888         _.each(arguments, function(x) {
889             self.add(x);
890         });
891     },
892     add: function (context) {
893         this.__contexts.push(context);
894         return this;
895     },
896     set_eval_context: function (eval_context) {
897         this.__eval_context = eval_context;
898         return this;
899     },
900     get_eval_context: function () {
901         return this.__eval_context;
902     }
903 });
904
905 openerp.web.CompoundDomain = openerp.web.Class.extend({
906     init: function () {
907         this.__ref = "compound_domain";
908         this.__domains = [];
909         this.__eval_context = null;
910         var self = this;
911         _.each(arguments, function(x) {
912             self.add(x);
913         });
914     },
915     add: function(domain) {
916         this.__domains.push(domain);
917         return this;
918     },
919     set_eval_context: function(eval_context) {
920         this.__eval_context = eval_context;
921         return this;
922     },
923     get_eval_context: function() {
924         return this.__eval_context;
925     }
926 });
927 };
928
929 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: