[IMP] Connection: new method to bind to a server. Must be called before login
[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.Widget.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.Widget
34      *
35      * @param {openerp.web.Widget} 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.Widget.extend( /** @lends openerp.web.DataSet# */{
237     identifier_prefix: "dataset",
238     /**
239      * DateaManagement interface between views and the collection of selected
240      * OpenERP records (represents the view's state?)
241      *
242      * @constructs openerp.web.DataSet
243      * @extends openerp.web.Widget
244      *
245      * @param {String} model the OpenERP model this dataset will manage
246      */
247     init: function(parent, model, context) {
248         this._super(parent);
249         this.model = model;
250         this.context = context || {};
251         this.index = null;
252     },
253     previous: function () {
254         this.index -= 1;
255         if (this.index < 0) {
256             this.index = this.ids.length - 1;
257         }
258         return this;
259     },
260     next: function () {
261         this.index += 1;
262         if (this.index >= this.ids.length) {
263             this.index = 0;
264         }
265         return this;
266     },
267     select_id: function(id) {
268         var idx = _.indexOf(this.ids, id);
269         if (idx === -1) {
270             return false;
271         } else {
272             this.index = idx;
273             return true;
274         }
275     },
276     /**
277      * Read records.
278      *
279      * @param {Array} ids identifiers of the records to read
280      * @param {Array} fields fields to read and return, by default all fields are returned
281      * @param {Function} callback function called with read result
282      * @returns {$.Deferred}
283      */
284     read_ids: function (ids, fields, callback) {
285         return this.rpc('/web/dataset/get', {
286             model: this.model,
287             ids: ids,
288             fields: fields,
289             context: this.get_context()
290         }, callback);
291     },
292     /**
293      * Read a slice of the records represented by this DataSet, based on its
294      * domain and context.
295      *
296      * @param {Array} [fields] fields to read and return, by default all fields are returned
297      * @params {Object} options
298      * @param {Number} [options.offset=0] The index from which selected records should be returned
299      * @param {Number} [options.limit=null] The maximum number of records to return
300      * @param {Function} callback function called with read_slice result
301      * @returns {$.Deferred}
302      */
303     read_slice: function (fields, options, callback) { 
304         return null; 
305     },
306     /**
307      * Reads the current dataset record (from its index)
308      *
309      * @params {Array} [fields] fields to read and return, by default all fields are returned
310      * @params {Function} callback function called with read_index result
311      * @returns {$.Deferred}
312      */
313     read_index: function (fields, callback) {
314         var def = $.Deferred().then(callback);
315         if (_.isEmpty(this.ids)) {
316             def.reject();
317         } else {
318             fields = fields || false;
319             return this.read_ids([this.ids[this.index]], fields).then(function(records) {
320                 def.resolve(records[0]);
321             }, function() {
322                 def.reject.apply(def, arguments);
323             });
324         }
325         return def.promise();
326     },
327     /**
328      * Reads default values for the current model
329      *
330      * @param {Array} [fields] fields to get default values for, by default all defaults are read
331      * @param {Function} callback function called with default_get result
332      * @returns {$.Deferred}
333      */
334     default_get: function(fields, callback) {
335         return this.rpc('/web/dataset/default_get', {
336             model: this.model,
337             fields: fields,
338             context: this.get_context()
339         }, callback);
340     },
341     /**
342      * Creates a new record in db
343      *
344      * @param {Object} data field values to set on the new record
345      * @param {Function} callback function called with operation result
346      * @param {Function} error_callback function called in case of creation error
347      * @returns {$.Deferred}
348      */
349     create: function(data, callback, error_callback) {
350         return this.rpc('/web/dataset/create', {
351             model: this.model,
352             data: data,
353             context: this.get_context()
354         }, callback, error_callback);
355     },
356     /**
357      * Saves the provided data in an existing db record
358      *
359      * @param {Number|String} id identifier for the record to alter
360      * @param {Object} data field values to write into the record
361      * @param {Function} callback function called with operation result
362      * @param {Function} error_callback function called in case of write error
363      * @returns {$.Deferred}
364      */
365     write: function (id, data, options, callback, error_callback) {
366         options = options || {};
367         return this.rpc('/web/dataset/save', {
368             model: this.model,
369             id: id,
370             data: data,
371             context: this.get_context(options.context)
372         }, callback, error_callback);
373     },
374     /**
375      * Deletes an existing record from the database
376      *
377      * @param {Number|String} ids identifier of the record to delete
378      * @param {Function} callback function called with operation result
379      * @param {Function} error_callback function called in case of deletion error
380      */
381     unlink: function(ids, callback, error_callback) {
382         var self = this;
383         return this.call_and_eval("unlink", [ids, this.get_context()], null, 1,
384             callback, error_callback);
385     },
386     /**
387      * Calls an arbitrary RPC method
388      *
389      * @param {String} method name of the method (on the current model) to call
390      * @param {Array} [args] arguments to pass to the method
391      * @param {Function} callback
392      * @param {Function} error_callback
393      * @returns {$.Deferred}
394      */
395     call: function (method, args, callback, error_callback) {
396         return this.rpc('/web/dataset/call', {
397             model: this.model,
398             method: method,
399             args: args || []
400         }, callback, error_callback);
401     },
402     /**
403      * Calls an arbitrary method, with more crazy
404      *
405      * @param {String} method
406      * @param {Array} [args]
407      * @param {Number} [domain_index] index of a domain to evaluate in the args array
408      * @param {Number} [context_index] index of a context to evaluate in the args array
409      * @param {Function} callback
410      * @param {Function }error_callback
411      * @returns {$.Deferred}
412      */
413     call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) {
414         return this.rpc('/web/dataset/call', {
415             model: this.model,
416             method: method,
417             domain_id: domain_index || null,
418             context_id: context_index || null,
419             args: args || []
420         }, callback, error_callback);
421     },
422     /**
423      * Calls a button method, usually returning some sort of action
424      *
425      * @param {String} method
426      * @param {Array} [args]
427      * @param {Function} callback
428      * @param {Function} error_callback
429      * @returns {$.Deferred}
430      */
431     call_button: function (method, args, callback, error_callback) {
432         return this.rpc('/web/dataset/call_button', {
433             model: this.model,
434             method: method,
435             domain_id: null,
436             context_id: 1,
437             args: args || []
438         }, callback, error_callback);
439     },
440     /**
441      * Fetches the "readable name" for records, based on intrinsic rules
442      *
443      * @param {Array} ids
444      * @param {Function} callback
445      * @returns {$.Deferred}
446      */
447     name_get: function(ids, callback) {
448         return this.call_and_eval('name_get', [ids, this.get_context()], null, 1, callback);
449     },
450     /**
451      * 
452      * @param {String} name name to perform a search for/on
453      * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
454      * @param {String} [operator='ilike'] matching operator to use with the provided name value
455      * @param {Number} [limit=100] maximum number of matches to return
456      * @param {Function} callback function to call with name_search result
457      * @returns {$.Deferred}
458      */
459     name_search: function (name, domain, operator, limit, callback) {
460         return this.call_and_eval('name_search',
461             [name || '', domain || false, operator || 'ilike', this.get_context(), limit || 100],
462             1, 3, callback);
463     },
464     /**
465      * @param name
466      * @param callback
467      */
468     name_create: function(name, callback) {
469         return this.call_and_eval('name_create', [name, this.get_context()], null, 1, callback);
470     },
471     exec_workflow: function (id, signal, callback) {
472         return this.rpc('/web/dataset/exec_workflow', {
473             model: this.model,
474             id: id,
475             signal: signal
476         }, callback);
477     },
478     get_context: function(request_context) {
479         if (request_context) {
480             return new openerp.web.CompoundContext(this.context, request_context);
481         }
482         return this.context;
483     }
484 });
485 openerp.web.DataSetStatic =  openerp.web.DataSet.extend({
486     init: function(parent, model, context, ids) {
487         this._super(parent, model, context);
488         // all local records
489         this.ids = ids || [];
490     },
491     read_slice: function (fields, options, callback) {
492         // TODO remove fields from options
493         var self = this,
494             offset = options.offset || 0,
495             limit = options.limit || false,
496             fields = fields || false;
497         var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
498         return this.read_ids(this.ids.slice(offset, end_pos), fields, callback);
499     },
500     set_ids: function (ids) {
501         this.ids = ids;
502         if (this.index !== null) {
503             this.index = this.index <= this.ids.length - 1 ?
504                 this.index : (this.ids.length > 0 ? this.length - 1 : 0);
505         }
506     },
507     unlink: function(ids) {
508         this.on_unlink(ids);
509         return $.Deferred().resolve({result: true});
510     },
511     on_unlink: function(ids) {
512         this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
513     }
514 });
515 openerp.web.DataSetSearch =  openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{
516     /**
517      * @constructs openerp.web.DataSetSearch
518      * @extends openerp.web.DataSet
519      *
520      * @param {Object} parent
521      * @param {String} model
522      * @param {Object} context
523      * @param {Array} domain
524      */
525     init: function(parent, model, context, domain) {
526         this._super(parent, model, context);
527         this.domain = domain || [];
528         this._sort = [];
529         this.offset = 0;
530         // subset records[offset:offset+limit]
531         // is it necessary ?
532         this.ids = [];
533     },
534     /**
535      * Read a slice of the records represented by this DataSet, based on its
536      * domain and context.
537      *
538      * @params {Object} options
539      * @param {Array} [options.fields] fields to read and return, by default all fields are returned
540      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
541      * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
542      * @param {Number} [options.offset=0] The index from which selected records should be returned
543      * @param {Number} [options.limit=null] The maximum number of records to return
544      * @param {Function} callback function called with read_slice result
545      * @returns {$.Deferred}
546      */
547     read_slice: function (fields, options, callback) {
548         var self = this;
549         var options = options || {};
550         var offset = options.offset || 0;
551         return this.rpc('/web/dataset/search_read', {
552             model: this.model,
553             fields: fields || false,
554             domain: this.get_domain(options.domain),
555             context: this.get_context(options.context),
556             sort: this.sort(),
557             offset: offset,
558             limit: options.limit || false
559         }).pipe(function (result) {
560             self.ids = result.ids;
561             self.offset = offset;
562             return result.records;
563         }).then(callback);
564     },
565     get_domain: function (other_domain) {
566         if (other_domain) {
567             return new openerp.web.CompoundDomain(this.domain, other_domain);
568         }
569         return this.domain;
570     },
571     /**
572      * Reads or changes sort criteria on the dataset.
573      *
574      * If not provided with any argument, serializes the sort criteria to
575      * an SQL-like form usable by OpenERP's ORM.
576      *
577      * If given a field, will set that field as first sorting criteria or,
578      * if the field is already the first sorting criteria, will reverse it.
579      *
580      * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
581      * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
582      * @returns {String|undefined}
583      */
584     sort: function (field, force_reverse) {
585         if (!field) {
586             return openerp.web.serialize_sort(this._sort);
587         }
588         var reverse = force_reverse || (this._sort[0] === field);
589         this._sort.splice.apply(
590             this._sort, [0, this._sort.length].concat(
591                 _.without(this._sort, field, '-' + field)));
592
593         this._sort.unshift((reverse ? '-' : '') + field);
594         return undefined;
595     },
596     unlink: function(ids, callback, error_callback) {
597         var self = this;
598         return this._super(ids, function(result) {
599             self.ids = _.without.apply(_, [self.ids].concat(ids));
600             if (this.index !== null) {
601                 self.index = self.index <= self.ids.length - 1 ?
602                     self.index : (self.ids.length > 0 ? self.ids.length -1 : 0);
603             }
604             if (callback)
605                 callback(result);
606         }, error_callback);
607     }
608 });
609 openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
610     virtual_id_prefix: "one2many_v_id_",
611     debug_mode: true,
612     init: function() {
613         this._super.apply(this, arguments);
614         this.reset_ids([]);
615         this.last_default_get = {};
616     },
617     default_get: function(fields, callback) {
618         return this._super(fields).then(this.on_default_get).then(callback);
619     },
620     on_default_get: function(res) {
621         this.last_default_get = res;
622     },
623     create: function(data, callback, error_callback) {
624         var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
625             defaults: this.last_default_get};
626         this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
627         this.cache.push(cached);
628         this.on_change();
629         var prom = $.Deferred().then(callback);
630         prom.resolve({result: cached.id});
631         return prom.promise();
632     },
633     write: function (id, data, options, callback) {
634         var self = this;
635         var record = _.detect(this.to_create, function(x) {return x.id === id;});
636         record = record || _.detect(this.to_write, function(x) {return x.id === id;});
637         var dirty = false;
638         if (record) {
639             for (var k in data) {
640                 if (record.values[k] === undefined || record.values[k] !== data[k]) {
641                     dirty = true;
642                     break;
643                 }
644             }
645             $.extend(record.values, data);
646         } else {
647             dirty = true;
648             record = {id: id, values: data};
649             self.to_write.push(record);
650         }
651         var cached = _.detect(this.cache, function(x) {return x.id === id;});
652         if (!cached) {
653             cached = {id: id, values: {}};
654             this.cache.push(cached);
655         }
656         $.extend(cached.values, record.values);
657         if (dirty)
658             this.on_change();
659         var to_return = $.Deferred().then(callback);
660         to_return.resolve({result: true});
661         return to_return.promise();
662     },
663     unlink: function(ids, callback, error_callback) {
664         var self = this;
665         _.each(ids, function(id) {
666             if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
667                 self.to_delete.push({id: id})
668             }
669         });
670         this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
671         this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
672         this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
673         this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
674         this.on_change();
675         var to_return = $.Deferred().then(callback);
676         setTimeout(function () {to_return.resolve({result: true});}, 0);
677         return to_return.promise();
678     },
679     reset_ids: function(ids) {
680         this.set_ids(ids);
681         this.to_delete = [];
682         this.to_create = [];
683         this.to_write = [];
684         this.cache = [];
685         this.delete_all = false;
686     },
687     on_change: function() {},
688     read_ids: function (ids, fields, callback) {
689         var self = this;
690         var to_get = [];
691         _.each(ids, function(id) {
692             var cached = _.detect(self.cache, function(x) {return x.id === id;});
693             var created = _.detect(self.to_create, function(x) {return x.id === id;});
694             if (created) {
695                 _.each(fields, function(x) {if (cached.values[x] === undefined)
696                     cached.values[x] = created.defaults[x] || false;});
697             } else {
698                 if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
699                     to_get.push(id);
700             }
701         });
702         var completion = $.Deferred().then(callback);
703         var return_records = function() {
704             var records = _.map(ids, function(id) {
705                 return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
706             });
707             if (self.debug_mode) {
708                 if (_.include(records, undefined)) {
709                     throw "Record not correctly loaded";
710                 }
711             }
712             completion.resolve(records);
713         };
714         if(to_get.length > 0) {
715             var rpc_promise = this._super(to_get, fields, function(records) {
716                 _.each(records, function(record, index) {
717                     var id = to_get[index];
718                     var cached = _.detect(self.cache, function(x) {return x.id === id;});
719                     if (!cached) {
720                         self.cache.push({id: id, values: record});
721                     } else {
722                         // I assume cache value is prioritary
723                         _.defaults(cached.values, record);
724                     }
725                 });
726                 return_records();
727             });
728             $.when(rpc_promise).fail(function() {completion.reject();});
729         } else {
730             return_records();
731         }
732         return completion.promise();
733     }
734 });
735 openerp.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
736
737 openerp.web.ProxyDataSet = openerp.web.DataSetSearch.extend({
738     init: function() {
739         this._super.apply(this, arguments);
740         this.create_function = null;
741         this.write_function = null;
742         this.read_function = null;
743     },
744     read_ids: function () {
745         if (this.read_function) {
746             return this.read_function.apply(null, arguments);
747         } else {
748             return this._super.apply(this, arguments);
749         }
750     },
751     default_get: function(fields, callback) {
752         return this._super(fields, callback).then(this.on_default_get);
753     },
754     on_default_get: function(result) {},
755     create: function(data, callback, error_callback) {
756         this.on_create(data);
757         if (this.create_function) {
758             return this.create_function(data, callback, error_callback);
759         } else {
760             console.warn("trying to create a record using default proxy dataset behavior");
761             var to_return = $.Deferred().then(callback);
762             setTimeout(function () {to_return.resolve({"result": undefined});}, 0);
763             return to_return.promise();
764         }
765     },
766     on_create: function(data) {},
767     write: function (id, data, options, callback) {
768         this.on_write(id, data);
769         if (this.write_function) {
770             return this.write_function(id, data, options, callback);
771         } else {
772             console.warn("trying to write a record using default proxy dataset behavior");
773             var to_return = $.Deferred().then(callback);
774             setTimeout(function () {to_return.resolve({"result": true});}, 0);
775             return to_return.promise();
776         }
777     },
778     on_write: function(id, data) {},
779     unlink: function(ids, callback, error_callback) {
780         this.on_unlink(ids);
781         console.warn("trying to unlink a record using default proxy dataset behavior");
782         var to_return = $.Deferred().then(callback);
783         setTimeout(function () {to_return.resolve({"result": true});}, 0);
784         return to_return.promise();
785     },
786     on_unlink: function(ids) {}
787 });
788
789 openerp.web.Model = openerp.web.CallbackEnabled.extend({
790     init: function(_, model_name) {
791         this._super();
792         this.model_name = model_name;
793     },
794     rpc: function() {
795         var c = openerp.connection;
796         return c.rpc.apply(c, arguments);
797     },
798     get_func: function(method_name) {
799         var self = this;
800         return function() {
801             if (method_name == "search_read")
802                 return self._search_read.apply(self, arguments);
803             return self._call(method_name, _.toArray(arguments));
804         };
805     },
806     _call: function (method, args) {
807         return this.rpc('/web/dataset/call', {
808             model: this.model_name,
809             method: method,
810             args: args
811         }).pipe(function(result) {
812             if (method == "read" && result instanceof Array && result.length > 0 && result[0]["id"]) {
813                 var index = {};
814                 _.each(_.range(result.length), function(i) {
815                     index[result[i]["id"]] = result[i];
816                 })
817                 result = _.map(args[0], function(x) {return index[x];});
818             }
819             return result;
820         });
821     },
822     _search_read: function(domain, fields, offset, limit, order, context) {
823         return this.rpc('/web/dataset/search_read', {
824             model: this.model_name,
825             fields: fields,
826             offset: offset,
827             limit: limit,
828             domain: domain,
829             sort: order,
830             context: context
831         }).pipe(function(result) {
832             return result.records;
833         });;
834     }
835 });
836
837 openerp.web.CompoundContext = openerp.web.Class.extend({
838     init: function () {
839         this.__ref = "compound_context";
840         this.__contexts = [];
841         this.__eval_context = null;
842         var self = this;
843         _.each(arguments, function(x) {
844             self.add(x);
845         });
846     },
847     add: function (context) {
848         this.__contexts.push(context);
849         return this;
850     },
851     set_eval_context: function (eval_context) {
852         this.__eval_context = eval_context;
853         return this;
854     },
855     get_eval_context: function () {
856         return this.__eval_context;
857     }
858 });
859
860 openerp.web.CompoundDomain = openerp.web.Class.extend({
861     init: function () {
862         this.__ref = "compound_domain";
863         this.__domains = [];
864         this.__eval_context = null;
865         var self = this;
866         _.each(arguments, function(x) {
867             self.add(x);
868         });
869     },
870     add: function(domain) {
871         this.__domains.push(domain);
872         return this;
873     },
874     set_eval_context: function(eval_context) {
875         this.__eval_context = eval_context;
876         return this;
877     },
878     get_eval_context: function() {
879         return this.__eval_context;
880     }
881 });
882 };
883
884 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: