[MERGE] forward port of branch 8.0 up to 2b192be
[odoo/odoo.git] / addons / web / static / src / js / openerpframework.js
1 /*
2  * Copyright (c) 2012, OpenERP S.A.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice, this
9  *    list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright notice,
11  *    this list of conditions and the following disclaimer in the documentation
12  *    and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18  * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26  /*
27     The only dependencies of this file are underscore >= 1.3.1, jQuery >= 1.8.3 and
28     QWeb >= 1.0.0 . No dependencies shall be added.
29
30     This file must compile in EcmaScript 3 and work in IE7.
31  */
32
33 (function() {
34 /* jshint es3: true */
35 "use strict";
36
37 function declare($, _, QWeb2) {
38 var openerp = {};
39
40 /**
41  * Improved John Resig's inheritance, based on:
42  *
43  * Simple JavaScript Inheritance
44  * By John Resig http://ejohn.org/
45  * MIT Licensed.
46  *
47  * Adds "include()"
48  *
49  * Defines The Class object. That object can be used to define and inherit classes using
50  * the extend() method.
51  *
52  * Example:
53  *
54  * var Person = openerp.Class.extend({
55  *  init: function(isDancing){
56  *     this.dancing = isDancing;
57  *   },
58  *   dance: function(){
59  *     return this.dancing;
60  *   }
61  * });
62  *
63  * The init() method act as a constructor. This class can be instancied this way:
64  *
65  * var person = new Person(true);
66  * person.dance();
67  *
68  * The Person class can also be extended again:
69  *
70  * var Ninja = Person.extend({
71  *   init: function(){
72  *     this._super( false );
73  *   },
74  *   dance: function(){
75  *     // Call the inherited version of dance()
76  *     return this._super();
77  *   },
78  *   swingSword: function(){
79  *     return true;
80  *   }
81  * });
82  *
83  * When extending a class, each re-defined method can use this._super() to call the previous
84  * implementation of that method.
85  */
86 (function() {
87     var initializing = false,
88         fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
89     // The web Class implementation (does nothing)
90     openerp.Class = function(){};
91
92     /**
93      * Subclass an existing class
94      *
95      * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
96      */
97     openerp.Class.extend = function() {
98         var _super = this.prototype;
99         // Support mixins arguments
100         var args = _.toArray(arguments);
101         args.unshift({});
102         var prop = _.extend.apply(_,args);
103
104         // Instantiate a web class (but only create the instance,
105         // don't run the init constructor)
106         initializing = true;
107         var This = this;
108         var prototype = new This();
109         initializing = false;
110
111         // Copy the properties over onto the new prototype
112         _.each(prop, function(val, name) {
113             // Check if we're overwriting an existing function
114             prototype[name] = typeof prop[name] == "function" &&
115                               fnTest.test(prop[name]) ?
116                     (function(name, fn) {
117                         return function() {
118                             var tmp = this._super;
119
120                             // Add a new ._super() method that is the same
121                             // method but on the super-class
122                             this._super = _super[name];
123
124                             // The method only need to be bound temporarily, so
125                             // we remove it when we're done executing
126                             var ret = fn.apply(this, arguments);
127                             this._super = tmp;
128
129                             return ret;
130                         };
131                     })(name, prop[name]) :
132                     prop[name];
133         });
134
135         // The dummy class constructor
136         function Class() {
137             if(this.constructor !== openerp.Class){
138                 throw new Error("You can only instanciate objects with the 'new' operator");
139             }
140             // All construction is actually done in the init method
141             this._super = null;
142             if (!initializing && this.init) {
143                 var ret = this.init.apply(this, arguments);
144                 if (ret) { return ret; }
145             }
146             return this;
147         }
148         Class.include = function (properties) {
149             _.each(properties, function(val, name) {
150                 if (typeof properties[name] !== 'function'
151                         || !fnTest.test(properties[name])) {
152                     prototype[name] = properties[name];
153                 } else if (typeof prototype[name] === 'function'
154                            && prototype.hasOwnProperty(name)) {
155                     prototype[name] = (function (name, fn, previous) {
156                         return function () {
157                             var tmp = this._super;
158                             this._super = previous;
159                             var ret = fn.apply(this, arguments);
160                             this._super = tmp;
161                             return ret;
162                         };
163                     })(name, properties[name], prototype[name]);
164                 } else if (typeof _super[name] === 'function') {
165                     prototype[name] = (function (name, fn) {
166                         return function () {
167                             var tmp = this._super;
168                             this._super = _super[name];
169                             var ret = fn.apply(this, arguments);
170                             this._super = tmp;
171                             return ret;
172                         };
173                     })(name, properties[name]);
174                 }
175             });
176         };
177
178         // Populate our constructed prototype object
179         Class.prototype = prototype;
180
181         // Enforce the constructor to be what we expect
182         Class.constructor = Class;
183
184         // And make this class extendable
185         Class.extend = this.extend;
186
187         return Class;
188     };
189 })();
190
191 // Mixins
192
193 /**
194  * Mixin to structure objects' life-cycles folowing a parent-children
195  * relationship. Each object can a have a parent and multiple children.
196  * When an object is destroyed, all its children are destroyed too releasing
197  * any resource they could have reserved before.
198  */
199 openerp.ParentedMixin = {
200     __parentedMixin : true,
201     init: function() {
202         this.__parentedDestroyed = false;
203         this.__parentedChildren = [];
204         this.__parentedParent = null;
205     },
206     /**
207      * Set the parent of the current object. When calling this method, the
208      * parent will also be informed and will return the current object
209      * when its getChildren() method is called. If the current object did
210      * already have a parent, it is unregistered before, which means the
211      * previous parent will not return the current object anymore when its
212      * getChildren() method is called.
213      */
214     setParent : function(parent) {
215         if (this.getParent()) {
216             if (this.getParent().__parentedMixin) {
217                 this.getParent().__parentedChildren = _.without(this
218                         .getParent().getChildren(), this);
219             }
220         }
221         this.__parentedParent = parent;
222         if (parent && parent.__parentedMixin) {
223             parent.__parentedChildren.push(this);
224         }
225     },
226     /**
227      * Return the current parent of the object (or null).
228      */
229     getParent : function() {
230         return this.__parentedParent;
231     },
232     /**
233      * Return a list of the children of the current object.
234      */
235     getChildren : function() {
236         return _.clone(this.__parentedChildren);
237     },
238     /**
239      * Returns true if destroy() was called on the current object.
240      */
241     isDestroyed : function() {
242         return this.__parentedDestroyed;
243     },
244     /**
245         Utility method to only execute asynchronous actions if the current
246         object has not been destroyed.
247
248         @param {$.Deferred} promise The promise representing the asynchronous
249                                     action.
250         @param {bool} [reject=false] If true, the returned promise will be
251                                      rejected with no arguments if the current
252                                      object is destroyed. If false, the
253                                      returned promise will never be resolved
254                                      or rejected.
255         @returns {$.Deferred} A promise that will mirror the given promise if
256                               everything goes fine but will either be rejected
257                               with no arguments or never resolved if the
258                               current object is destroyed.
259     */
260     alive: function(promise, reject) {
261         var self = this;
262         return $.Deferred(function (def) {
263             promise.then(function () {
264                 if (!self.isDestroyed()) {
265                     def.resolve.apply(def, arguments);
266                 }
267             }, function () {
268                 if (!self.isDestroyed()) {
269                     def.reject.apply(def, arguments);
270                 }
271             }).always(function () {
272                 if (reject) {
273                     // noop if def already resolved or rejected
274                     def.reject();
275                 }
276                 // otherwise leave promise in limbo
277             });
278         }).promise();
279     },
280     /**
281      * Inform the object it should destroy itself, releasing any
282      * resource it could have reserved.
283      */
284     destroy : function() {
285         _.each(this.getChildren(), function(el) {
286             el.destroy();
287         });
288         this.setParent(undefined);
289         this.__parentedDestroyed = true;
290     }
291 };
292
293 /**
294  * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
295  *
296  * This class just handle the dispatching of events, it is not meant to be extended,
297  * nor used directly. All integration with parenting and automatic unregistration of
298  * events is done in EventDispatcherMixin.
299  *
300  * Copyright notice for the following Class:
301  *
302  * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
303  * Backbone may be freely distributed under the MIT license.
304  * For all details and documentation:
305  * http://backbonejs.org
306  *
307  */
308 var Events = openerp.Class.extend({
309     on : function(events, callback, context) {
310         var ev;
311         events = events.split(/\s+/);
312         var calls = this._callbacks || (this._callbacks = {});
313         while ((ev = events.shift())) {
314             var list = calls[ev] || (calls[ev] = {});
315             var tail = list.tail || (list.tail = list.next = {});
316             tail.callback = callback;
317             tail.context = context;
318             list.tail = tail.next = {};
319         }
320         return this;
321     },
322
323     off : function(events, callback, context) {
324         var ev, calls, node;
325         if (!events) {
326             delete this._callbacks;
327         } else if ((calls = this._callbacks)) {
328             events = events.split(/\s+/);
329             while ((ev = events.shift())) {
330                 node = calls[ev];
331                 delete calls[ev];
332                 if (!callback || !node)
333                     continue;
334                 while ((node = node.next) && node.next) {
335                     if (node.callback === callback
336                             && (!context || node.context === context))
337                         continue;
338                     this.on(ev, node.callback, node.context);
339                 }
340             }
341         }
342         return this;
343     },
344
345     callbackList: function() {
346         var lst = [];
347         _.each(this._callbacks || {}, function(el, eventName) {
348             var node = el;
349             while ((node = node.next) && node.next) {
350                 lst.push([eventName, node.callback, node.context]);
351             }
352         });
353         return lst;
354     },
355
356     trigger : function(events) {
357         var event, node, calls, tail, args, all, rest;
358         if (!(calls = this._callbacks))
359             return this;
360         all = calls['all'];
361         (events = events.split(/\s+/)).push(null);
362         // Save references to the current heads & tails.
363         while ((event = events.shift())) {
364             if (all)
365                 events.push({
366                     next : all.next,
367                     tail : all.tail,
368                     event : event
369                 });
370             if (!(node = calls[event]))
371                 continue;
372             events.push({
373                 next : node.next,
374                 tail : node.tail
375             });
376         }
377         rest = Array.prototype.slice.call(arguments, 1);
378         while ((node = events.pop())) {
379             tail = node.tail;
380             args = node.event ? [ node.event ].concat(rest) : rest;
381             while ((node = node.next) !== tail) {
382                 node.callback.apply(node.context || this, args);
383             }
384         }
385         return this;
386     }
387 });
388
389 /**
390     Mixin containing an event system. Events are also registered by specifying the target object
391     (the object which will receive the event when it is raised). Both the event-emitting object
392     and the target object store or reference to each other. This is used to correctly remove all
393     reference to the event handler when any of the object is destroyed (when the destroy() method
394     from ParentedMixin is called). Removing those references is necessary to avoid memory leak
395     and phantom events (events which are raised and sent to a previously destroyed object).
396 */
397 openerp.EventDispatcherMixin = _.extend({}, openerp.ParentedMixin, {
398     __eventDispatcherMixin: true,
399     init: function() {
400         openerp.ParentedMixin.init.call(this);
401         this.__edispatcherEvents = new Events();
402         this.__edispatcherRegisteredEvents = [];
403     },
404     on: function(events, dest, func) {
405         var self = this;
406         if (!(func instanceof Function)) {
407             throw new Error("Event handler must be a function.");
408         }
409         events = events.split(/\s+/);
410         _.each(events, function(eventName) {
411             self.__edispatcherEvents.on(eventName, func, dest);
412             if (dest && dest.__eventDispatcherMixin) {
413                 dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
414             }
415         });
416         return this;
417     },
418     off: function(events, dest, func) {
419         var self = this;
420         events = events.split(/\s+/);
421         _.each(events, function(eventName) {
422             self.__edispatcherEvents.off(eventName, func, dest);
423             if (dest && dest.__eventDispatcherMixin) {
424                 dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
425                     return !(el.name === eventName && el.func === func && el.source === self);
426                 });
427             }
428         });
429         return this;
430     },
431     trigger: function(events) {
432         this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
433         return this;
434     },
435     destroy: function() {
436         var self = this;
437         _.each(this.__edispatcherRegisteredEvents, function(event) {
438             event.source.__edispatcherEvents.off(event.name, event.func, self);
439         });
440         this.__edispatcherRegisteredEvents = [];
441         _.each(this.__edispatcherEvents.callbackList(), function(cal) {
442             this.off(cal[0], cal[2], cal[1]);
443         }, this);
444         this.__edispatcherEvents.off();
445         openerp.ParentedMixin.destroy.call(this);
446     }
447 });
448
449 openerp.PropertiesMixin = _.extend({}, openerp.EventDispatcherMixin, {
450     init: function() {
451         openerp.EventDispatcherMixin.init.call(this);
452         this.__getterSetterInternalMap = {};
453     },
454     set: function(arg1, arg2, arg3) {
455         var map;
456         var options;
457         if (typeof arg1 === "string") {
458             map = {};
459             map[arg1] = arg2;
460             options = arg3 || {};
461         } else {
462             map = arg1;
463             options = arg2 || {};
464         }
465         var self = this;
466         var changed = false;
467         _.each(map, function(val, key) {
468             var tmp = self.__getterSetterInternalMap[key];
469             if (tmp === val)
470                 return;
471             changed = true;
472             self.__getterSetterInternalMap[key] = val;
473             if (! options.silent)
474                 self.trigger("change:" + key, self, {
475                     oldValue: tmp,
476                     newValue: val
477                 });
478         });
479         if (changed)
480             self.trigger("change", self);
481     },
482     get: function(key) {
483         return this.__getterSetterInternalMap[key];
484     }
485 });
486
487 /**
488  * Base class for all visual components. Provides a lot of functionalities helpful
489  * for the management of a part of the DOM.
490  *
491  * Widget handles:
492  * - Rendering with QWeb.
493  * - Life-cycle management and parenting (when a parent is destroyed, all its children are
494  *     destroyed too).
495  * - Insertion in DOM.
496  *
497  * Guide to create implementations of the Widget class:
498  * ==============================================
499  *
500  * Here is a sample child class:
501  *
502  * MyWidget = openerp.base.Widget.extend({
503  *     // the name of the QWeb template to use for rendering
504  *     template: "MyQWebTemplate",
505  *
506  *     init: function(parent) {
507  *         this._super(parent);
508  *         // stuff that you want to init before the rendering
509  *     },
510  *     start: function() {
511  *         // stuff you want to make after the rendering, `this.$el` holds a correct value
512  *         this.$el.find(".my_button").click(/* an example of event binding * /);
513  *
514  *         // if you have some asynchronous operations, it's a good idea to return
515  *         // a promise in start()
516  *         var promise = this.rpc(...);
517  *         return promise;
518  *     }
519  * });
520  *
521  * Now this class can simply be used with the following syntax:
522  *
523  * var my_widget = new MyWidget(this);
524  * my_widget.appendTo($(".some-div"));
525  *
526  * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
527  * DOM inside the ".some-div" div and its events were binded.
528  *
529  * And of course, when you don't need that widget anymore, just do:
530  *
531  * my_widget.destroy();
532  *
533  * That will kill the widget in a clean way and erase its content from the dom.
534  */
535 openerp.Widget = openerp.Class.extend(openerp.PropertiesMixin, {
536     // Backbone-ish API
537     tagName: 'div',
538     id: null,
539     className: null,
540     attributes: {},
541     events: {},
542     /**
543      * The name of the QWeb template that will be used for rendering. Must be
544      * redefined in subclasses or the default render() method can not be used.
545      *
546      * @type string
547      */
548     template: null,
549     /**
550      * Constructs the widget and sets its parent if a parent is given.
551      *
552      * @constructs openerp.Widget
553      *
554      * @param {openerp.Widget} parent Binds the current instance to the given Widget instance.
555      * When that widget is destroyed by calling destroy(), the current instance will be
556      * destroyed too. Can be null.
557      */
558     init: function(parent) {
559         openerp.PropertiesMixin.init.call(this);
560         this.setParent(parent);
561         // Bind on_/do_* methods to this
562         // We might remove this automatic binding in the future
563         for (var name in this) {
564             if(typeof(this[name]) == "function") {
565                 if((/^on_|^do_/).test(name)) {
566                     this[name] = _.bind(this[name], this);
567                 }
568             }
569         }
570         // FIXME: this should not be
571         this.setElement(this._make_descriptive());
572     },
573     /**
574      * Destroys the current widget, also destroys all its children before destroying itself.
575      */
576     destroy: function() {
577         _.each(this.getChildren(), function(el) {
578             el.destroy();
579         });
580         if(this.$el) {
581             this.$el.remove();
582         }
583         openerp.PropertiesMixin.destroy.call(this);
584     },
585     /**
586      * Renders the current widget and appends it to the given jQuery object or Widget.
587      *
588      * @param target A jQuery object or a Widget instance.
589      */
590     appendTo: function(target) {
591         var self = this;
592         return this.__widgetRenderAndInsert(function(t) {
593             self.$el.appendTo(t);
594         }, target);
595     },
596     /**
597      * Renders the current widget and prepends it to the given jQuery object or Widget.
598      *
599      * @param target A jQuery object or a Widget instance.
600      */
601     prependTo: function(target) {
602         var self = this;
603         return this.__widgetRenderAndInsert(function(t) {
604             self.$el.prependTo(t);
605         }, target);
606     },
607     /**
608      * Renders the current widget and inserts it after to the given jQuery object or Widget.
609      *
610      * @param target A jQuery object or a Widget instance.
611      */
612     insertAfter: function(target) {
613         var self = this;
614         return this.__widgetRenderAndInsert(function(t) {
615             self.$el.insertAfter(t);
616         }, target);
617     },
618     /**
619      * Renders the current widget and inserts it before to the given jQuery object or Widget.
620      *
621      * @param target A jQuery object or a Widget instance.
622      */
623     insertBefore: function(target) {
624         var self = this;
625         return this.__widgetRenderAndInsert(function(t) {
626             self.$el.insertBefore(t);
627         }, target);
628     },
629     /**
630      * Renders the current widget and replaces the given jQuery object.
631      *
632      * @param target A jQuery object or a Widget instance.
633      */
634     replace: function(target) {
635         return this.__widgetRenderAndInsert(_.bind(function(t) {
636             this.$el.replaceAll(t);
637         }, this), target);
638     },
639     __widgetRenderAndInsert: function(insertion, target) {
640         this.renderElement();
641         insertion(target);
642         return this.start();
643     },
644     /**
645      * Method called after rendering. Mostly used to bind actions, perform asynchronous
646      * calls, etc...
647      *
648      * By convention, this method should return an object that can be passed to $.when() 
649      * to inform the caller when this widget has been initialized.
650      *
651      * @returns {jQuery.Deferred or any}
652      */
653     start: function() {
654         return $.when();
655     },
656     /**
657      * Renders the element. The default implementation renders the widget using QWeb,
658      * `this.template` must be defined. The context given to QWeb contains the "widget"
659      * key that references `this`.
660      */
661     renderElement: function() {
662         var $el;
663         if (this.template) {
664             $el = $(openerp.qweb.render(this.template, {widget: this}).trim());
665         } else {
666             $el = this._make_descriptive();
667         }
668         this.replaceElement($el);
669     },
670     /**
671      * Re-sets the widget's root element and replaces the old root element
672      * (if any) by the new one in the DOM.
673      *
674      * @param {HTMLElement | jQuery} $el
675      * @returns {*} this
676      */
677     replaceElement: function ($el) {
678         var $oldel = this.$el;
679         this.setElement($el);
680         if ($oldel && !$oldel.is(this.$el)) {
681             $oldel.replaceWith(this.$el);
682         }
683         return this;
684     },
685     /**
686      * Re-sets the widget's root element (el/$el/$el).
687      *
688      * Includes:
689      * * re-delegating events
690      * * re-binding sub-elements
691      * * if the widget already had a root element, replacing the pre-existing
692      *   element in the DOM
693      *
694      * @param {HTMLElement | jQuery} element new root element for the widget
695      * @return {*} this
696      */
697     setElement: function (element) {
698         // NB: completely useless, as WidgetMixin#init creates a $el
699         // always
700         if (this.$el) {
701             this.undelegateEvents();
702         }
703
704         this.$el = (element instanceof $) ? element : $(element);
705         this.el = this.$el[0];
706
707         this.delegateEvents();
708
709         return this;
710     },
711     /**
712      * Utility function to build small DOM elements.
713      *
714      * @param {String} tagName name of the DOM element to create
715      * @param {Object} [attributes] map of DOM attributes to set on the element
716      * @param {String} [content] HTML content to set on the element
717      * @return {Element}
718      */
719     make: function (tagName, attributes, content) {
720         var el = document.createElement(tagName);
721         if (!_.isEmpty(attributes)) {
722             $(el).attr(attributes);
723         }
724         if (content) {
725             $(el).html(content);
726         }
727         return el;
728     },
729     /**
730      * Makes a potential root element from the declarative builder of the
731      * widget
732      *
733      * @return {jQuery}
734      * @private
735      */
736     _make_descriptive: function () {
737         var attrs = _.extend({}, this.attributes || {});
738         if (this.id) { attrs.id = this.id; }
739         if (this.className) { attrs['class'] = this.className; }
740         return $(this.make(this.tagName, attrs));
741     },
742     delegateEvents: function () {
743         var events = this.events;
744         if (_.isEmpty(events)) { return; }
745
746         for(var key in events) {
747             if (!events.hasOwnProperty(key)) { continue; }
748
749             var method = this.proxy(events[key]);
750
751             var match = /^(\S+)(\s+(.*))?$/.exec(key);
752             var event = match[1];
753             var selector = match[3];
754
755             event += '.widget_events';
756             if (!selector) {
757                 this.$el.on(event, method);
758             } else {
759                 this.$el.on(event, selector, method);
760             }
761         }
762     },
763     undelegateEvents: function () {
764         this.$el.off('.widget_events');
765     },
766     /**
767      * Shortcut for ``this.$el.find(selector)``
768      *
769      * @param {String} selector CSS selector, rooted in $el
770      * @returns {jQuery} selector match
771      */
772     $: function(selector) {
773         if (selector === undefined)
774             return this.$el;
775         return this.$el.find(selector);
776     },
777     /**
778      * Proxies a method of the object, in order to keep the right ``this`` on
779      * method invocations.
780      *
781      * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
782      * even more so to ``jQuery.proxy`` with a fundamental difference: its
783      * resolution of the method being called is lazy, meaning it will use the
784      * method as it is when the proxy is called, not when the proxy is created.
785      *
786      * Other methods will fix the bound method to what it is when creating the
787      * binding/proxy, which is fine in most javascript code but problematic in
788      * OpenERP Web where developers may want to replace existing callbacks with
789      * theirs.
790      *
791      * The semantics of this precisely replace closing over the method call.
792      *
793      * @param {String|Function} method function or name of the method to invoke
794      * @returns {Function} proxied method
795      */
796     proxy: function (method) {
797         var self = this;
798         return function () {
799             var fn = (typeof method === 'string') ? self[method] : method;
800             return fn.apply(self, arguments);
801         };
802     }
803 });
804
805 var genericJsonRpc = function(fct_name, params, fct) {
806     var data = {
807         jsonrpc: "2.0",
808         method: fct_name,
809         params: params,
810         id: Math.floor(Math.random() * 1000 * 1000 * 1000)
811     };
812     var xhr = fct(data);
813     var result = xhr.pipe(function(result) {
814         if (result.error !== undefined) {
815             console.error("Server application error", result.error);
816             return $.Deferred().reject("server", result.error);
817         } else {
818             return result.result;
819         }
820     }, function() {
821         //console.error("JsonRPC communication error", _.toArray(arguments));
822         var def = $.Deferred();
823         return def.reject.apply(def, ["communication"].concat(_.toArray(arguments)));
824     });
825     // FIXME: jsonp?
826     result.abort = function () { xhr.abort && xhr.abort(); };
827     return result;
828 };
829
830 /**
831  * Replacer function for JSON.stringify, serializes Date objects to UTC
832  * datetime in the OpenERP Server format.
833  *
834  * However, if a serialized value has a toJSON method that method is called
835  * *before* the replacer is invoked. Date#toJSON exists, and thus the value
836  * passed to the replacer is a string, the original Date has to be fetched
837  * on the parent object (which is provided as the replacer's context).
838  *
839  * @param {String} k
840  * @param {Object} v
841  * @returns {Object}
842  */
843 function date_to_utc(k, v) {
844     var value = this[k];
845     if (!(value instanceof Date)) { return v; }
846
847     return openerp.datetime_to_str(value);
848 }
849
850 openerp.jsonRpc = function(url, fct_name, params, settings) {
851     return genericJsonRpc(fct_name, params, function(data) {
852         return $.ajax(url, _.extend({}, settings, {
853             url: url,
854             dataType: 'json',
855             type: 'POST',
856             data: JSON.stringify(data, date_to_utc),
857             contentType: 'application/json'
858         }));
859     });
860 };
861
862 openerp.jsonpRpc = function(url, fct_name, params, settings) {
863     settings = settings || {};
864     return genericJsonRpc(fct_name, params, function(data) {
865         var payload_str = JSON.stringify(data, date_to_utc);
866         var payload_url = $.param({r:payload_str});
867         var force2step = settings.force2step || false;
868         delete settings.force2step;
869         var session_id = settings.session_id || null;
870         delete settings.session_id;
871         if (payload_url.length < 2000 && ! force2step) {
872             return $.ajax(url, _.extend({}, settings, {
873                 url: url,
874                 dataType: 'jsonp',
875                 jsonp: 'jsonp',
876                 type: 'GET',
877                 cache: false,
878                 data: {r: payload_str, session_id: session_id}
879             }));
880         } else {
881             var args = {session_id: session_id, id: data.id};
882             var ifid = _.uniqueId('oe_rpc_iframe');
883             var html = "<iframe src='javascript:false;' name='" + ifid + "' id='" + ifid + "' style='display:none'></iframe>";
884             var $iframe = $(html);
885             var nurl = 'jsonp=1&' + $.param(args);
886             nurl = url.indexOf("?") !== -1 ? url + "&" + nurl : url + "?" + nurl;
887             var $form = $('<form>')
888                         .attr('method', 'POST')
889                         .attr('target', ifid)
890                         .attr('enctype', "multipart/form-data")
891                         .attr('action', nurl)
892                         .append($('<input type="hidden" name="r" />').attr('value', payload_str))
893                         .hide()
894                         .appendTo($('body'));
895             var cleanUp = function() {
896                 if ($iframe) {
897                     $iframe.unbind("load").remove();
898                 }
899                 $form.remove();
900             };
901             var deferred = $.Deferred();
902             // the first bind is fired up when the iframe is added to the DOM
903             $iframe.bind('load', function() {
904                 // the second bind is fired up when the result of the form submission is received
905                 $iframe.unbind('load').bind('load', function() {
906                     $.ajax({
907                         url: url,
908                         dataType: 'jsonp',
909                         jsonp: 'jsonp',
910                         type: 'GET',
911                         cache: false,
912                         data: {session_id: session_id, id: data.id}
913                     }).always(function() {
914                         cleanUp();
915                     }).done(function() {
916                         deferred.resolve.apply(deferred, arguments);
917                     }).fail(function() {
918                         deferred.reject.apply(deferred, arguments);
919                     });
920                 });
921                 // now that the iframe can receive data, we fill and submit the form
922                 $form.submit();
923             });
924             // append the iframe to the DOM (will trigger the first load)
925             $form.after($iframe);
926             if (settings.timeout) {
927                 realSetTimeout(function() {
928                     deferred.reject({});
929                 }, settings.timeout);
930             }
931             return deferred;
932         }
933     });
934 };
935
936 openerp.loadCSS = function (url) {
937     if (!$('link[href="' + url + '"]').length) {
938         $('head').append($('<link>', {
939             'href': url,
940             'rel': 'stylesheet',
941             'type': 'text/css'
942         }));
943     }
944 };
945 openerp.loadJS = function (url) {
946     var def = $.Deferred();
947     if ($('script[src="' + url + '"]').length) {
948         def.resolve();
949     } else {
950         var script = document.createElement('script');
951         script.type = 'text/javascript';
952         script.src = url;
953         script.onload = script.onreadystatechange = function() {
954             if ((script.readyState && script.readyState != "loaded" && script.readyState != "complete") || script.onload_done) {
955                 return;
956             }
957             script.onload_done = true;
958             def.resolve(url);
959         };
960         script.onerror = function () {
961             console.error("Error loading file", script.src);
962             def.reject(url);
963         };
964         var head = document.head || document.getElementsByTagName('head')[0];
965         head.appendChild(script);
966     }
967     return def;
968 };
969 openerp.loadBundle = function (name) {
970     return $.when(
971         openerp.loadCSS('/web/css/' + name),
972         openerp.loadJS('/web/js/' + name)
973     );
974 };
975
976 var realSetTimeout = function(fct, millis) {
977     var finished = new Date().getTime() + millis;
978     var wait = function() {
979         var current = new Date().getTime();
980         if (current < finished) {
981             setTimeout(wait, finished - current);
982         } else {
983             fct();
984         }
985     };
986     setTimeout(wait, millis);
987 };
988
989 openerp.Session = openerp.Class.extend(openerp.PropertiesMixin, {
990     triggers: {
991         'request': 'Request sent',
992         'response': 'Response received',
993         'response_failed': 'HTTP Error response or timeout received',
994         'error': 'The received response is an JSON-RPC error'
995     },
996     /**
997     @constructs openerp.Session
998     
999     @param parent The parent of the newly created object.
1000     @param {String} origin Url of the OpenERP server to contact with this session object
1001     or `null` if the server to contact is the origin server.
1002     @param {Dict} options A dictionary that can contain the following options:
1003         
1004         * "override_session": Default to false. If true, the current session object will
1005           not try to re-use a previously created session id stored in a cookie.
1006         * "session_id": Default to null. If specified, the specified session_id will be used
1007           by this session object. Specifying this option automatically implies that the option
1008           "override_session" is set to true.
1009      */
1010     init: function(parent, origin, options) {
1011         openerp.PropertiesMixin.init.call(this, parent);
1012         options = options || {};
1013         this.server = null;
1014         this.session_id = options.session_id || null;
1015         this.override_session = options.override_session || !!options.session_id || false;
1016         this.avoid_recursion = false;
1017         this.use_cors = options.use_cors || false;
1018         this.setup(origin);
1019     },
1020     setup: function(origin) {
1021         // must be able to customize server
1022         var window_origin = location.protocol + "//" + location.host;
1023         origin = origin ? origin.replace( /\/+$/, '') : window_origin;
1024         if (!_.isUndefined(this.origin) && this.origin !== origin)
1025             throw new Error('Session already bound to ' + this.origin);
1026         else
1027             this.origin = origin;
1028         this.prefix = this.origin;
1029         this.server = this.origin; // keep chs happy
1030         this.origin_server = this.origin === window_origin;
1031     },
1032     /**
1033      * (re)loads the content of a session: db name, username, user id, session
1034      * context and status of the support contract
1035      *
1036      * @returns {$.Deferred} deferred indicating the session is done reloading
1037      */
1038     session_reload: function () {
1039         var self = this;
1040         return self.rpc("/web/session/get_session_info", {}).then(function(result) {
1041             delete result.session_id;
1042             _.extend(self, result);
1043         });
1044     },
1045     /**
1046      * The session is validated either by login or by restoration of a previous session
1047      */
1048     session_authenticate: function(db, login, password) {
1049         var self = this;
1050         var params = {db: db, login: login, password: password};
1051         return this.rpc("/web/session/authenticate", params).then(function(result) {
1052             if (!result.uid) {
1053                 return $.Deferred().reject();
1054             }
1055             delete result.session_id;
1056             _.extend(self, result);
1057         });
1058     },
1059     check_session_id: function() {
1060         var self = this;
1061         if (this.avoid_recursion || self.use_cors)
1062             return $.when();
1063         if (this.session_id)
1064             return $.when(); // we already have the session id
1065         if (this.override_session || ! this.origin_server) {
1066             // If we don't use the origin server we consider we should always create a new session.
1067             // Even if some browsers could support cookies when using jsonp that behavior is
1068             // not consistent and the browser creators are tending to removing that feature.
1069             this.avoid_recursion = true;
1070             return this.rpc("/gen_session_id", {}).then(function(result) {
1071                 self.session_id = result;
1072             }).always(function() {
1073                 self.avoid_recursion = false;
1074             });
1075         } else {
1076             // normal use case, just use the cookie
1077             self.session_id = openerp.get_cookie("session_id");
1078             return $.when();
1079         }
1080     },
1081     /**
1082      * Executes an RPC call, registering the provided callbacks.
1083      *
1084      * Registers a default error callback if none is provided, and handles
1085      * setting the correct session id and session context in the parameter
1086      * objects
1087      *
1088      * @param {String} url RPC endpoint
1089      * @param {Object} params call parameters
1090      * @param {Object} options additional options for rpc call
1091      * @param {Function} success_callback function to execute on RPC call success
1092      * @param {Function} error_callback function to execute on RPC call failure
1093      * @returns {jQuery.Deferred} jquery-provided ajax deferred
1094      */
1095     rpc: function(url, params, options) {
1096         var self = this;
1097         options = _.clone(options || {});
1098         var shadow = options.shadow || false;
1099         delete options.shadow;
1100
1101         return self.check_session_id().then(function() {
1102             // TODO: remove
1103             if (! _.isString(url)) {
1104                 _.extend(options, url);
1105                 url = url.url;
1106             }
1107             // TODO correct handling of timeouts
1108             if (! shadow)
1109                 self.trigger('request');
1110             var fct;
1111             if (self.origin_server) {
1112                 fct = openerp.jsonRpc;
1113                 if (self.override_session) {
1114                     options.headers = _.extend({}, options.headers, {
1115                         "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
1116                     });
1117                 }
1118             } else if (self.use_cors) {
1119                 fct = openerp.jsonRpc;
1120                 url = self.url(url, null);
1121                 options.session_id = self.session_id || '';
1122                 if (self.override_session) {
1123                     options.headers = _.extend({}, options.headers, {
1124                         "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
1125                     });
1126                 }
1127             } else {
1128                 fct = openerp.jsonpRpc;
1129                 url = self.url(url, null);
1130                 options.session_id = self.session_id || '';
1131             }
1132             var p = fct(url, "call", params, options);
1133             p = p.then(function (result) {
1134                 if (! shadow)
1135                     self.trigger('response');
1136                 return result;
1137             }, function(type, error, textStatus, errorThrown) {
1138                 if (type === "server") {
1139                     if (! shadow)
1140                         self.trigger('response');
1141                     if (error.code === 100) {
1142                         self.uid = false;
1143                     }
1144                     return $.Deferred().reject(error, $.Event());
1145                 } else {
1146                     if (! shadow)
1147                         self.trigger('response_failed');
1148                     var nerror = {
1149                         code: -32098,
1150                         message: "XmlHttpRequestError " + errorThrown,
1151                         data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
1152                     };
1153                     return $.Deferred().reject(nerror, $.Event());
1154                 }
1155             });
1156             return p.fail(function() { // Allow deferred user to disable rpc_error call in fail
1157                 p.fail(function(error, event) {
1158                     if (!event.isDefaultPrevented()) {
1159                         self.trigger('error', error, event);
1160                     }
1161                 });
1162             });
1163         });
1164     },
1165     url: function(path, params) {
1166         params = _.extend(params || {});
1167         if (this.override_session || (! this.origin_server))
1168             params.session_id = this.session_id;
1169         var qs = $.param(params);
1170         if (qs.length > 0)
1171             qs = "?" + qs;
1172         var prefix = _.any(['http://', 'https://', '//'], function(el) {
1173             return path.length >= el.length && path.slice(0, el.length) === el;
1174         }) ? '' : this.prefix; 
1175         return prefix + path + qs;
1176     },
1177     model: function(model_name) {
1178         return new openerp.Model(this, model_name);
1179     }
1180 });
1181
1182 openerp.Model = openerp.Class.extend({
1183     /**
1184     new openerp.Model([session,] model_name)
1185
1186     @constructs instance.Model
1187     @extends instance.Class
1188     
1189     @param {openerp.Session} [session] The session object used to communicate with
1190     the server.
1191     @param {String} model_name name of the OpenERP model this object is bound to
1192     @param {Object} [context]
1193     @param {Array} [domain]
1194     */
1195     init: function () {
1196         var session, model_name;
1197         var args = _.toArray(arguments);
1198         args.reverse();
1199         session = args.pop();
1200         if (session && ! (session instanceof openerp.Session)) {
1201             model_name = session;
1202             session = null;
1203         } else {
1204             model_name = args.pop();
1205         }
1206
1207         this.name = model_name;
1208         this._session = session;
1209     },
1210     session: function() {
1211         if (! this._session)
1212             throw new Error("Not session specified");
1213         return this._session;
1214     },
1215     /**
1216      * Call a method (over RPC) on the bound OpenERP model.
1217      *
1218      * @param {String} method name of the method to call
1219      * @param {Array} [args] positional arguments
1220      * @param {Object} [kwargs] keyword arguments
1221      * @param {Object} [options] additional options for the rpc() method
1222      * @returns {jQuery.Deferred<>} call result
1223      */
1224     call: function (method, args, kwargs, options) {
1225         args = args || [];
1226         kwargs = kwargs || {};
1227         if (!_.isArray(args)) {
1228             // call(method, kwargs)
1229             kwargs = args;
1230             args = [];
1231         }
1232         var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
1233         return this.session().rpc(call_kw, {
1234             model: this.name,
1235             method: method,
1236             args: args,
1237             kwargs: kwargs
1238         }, options);
1239     }
1240 });
1241
1242 /** OpenERP Translations */
1243 openerp.TranslationDataBase = openerp.Class.extend(/** @lends instance.TranslationDataBase# */{
1244     /**
1245      * @constructs instance.TranslationDataBase
1246      * @extends instance.Class
1247      */
1248     init: function() {
1249         this.db = {};
1250         this.parameters = {"direction": 'ltr',
1251                         "date_format": '%m/%d/%Y',
1252                         "time_format": '%H:%M:%S',
1253                         "grouping": [],
1254                         "decimal_point": ".",
1255                         "thousands_sep": ","};
1256     },
1257     set_bundle: function(translation_bundle) {
1258         var self = this;
1259         this.db = {};
1260         var modules = _.keys(translation_bundle.modules);
1261         modules.sort();
1262         if (_.include(modules, "web")) {
1263             modules = ["web"].concat(_.without(modules, "web"));
1264         }
1265         _.each(modules, function(name) {
1266             self.add_module_translation(translation_bundle.modules[name]);
1267         });
1268         if (translation_bundle.lang_parameters) {
1269             this.parameters = translation_bundle.lang_parameters;
1270         }
1271     },
1272     add_module_translation: function(mod) {
1273         var self = this;
1274         _.each(mod.messages, function(message) {
1275             self.db[message.id] = message.string;
1276         });
1277     },
1278     build_translation_function: function() {
1279         var self = this;
1280         var fcnt = function(str) {
1281             var tmp = self.get(str);
1282             return tmp === undefined ? str : tmp;
1283         };
1284         fcnt.database = this;
1285         return fcnt;
1286     },
1287     get: function(key) {
1288         return this.db[key];
1289     },
1290     /**
1291         Loads the translations from an OpenERP server.
1292
1293         @param {openerp.Session} session The session object to contact the server.
1294         @param {Array} [modules] The list of modules to load the translation. If not specified,
1295         it will default to all the modules installed in the current database.
1296         @param {Object} [lang] lang The language. If not specified it will default to the language
1297         of the current user.
1298         @returns {jQuery.Deferred}
1299     */
1300     load_translations: function(session, modules, lang) {
1301         var self = this;
1302         return session.rpc('/web/webclient/translations', {
1303             "mods": modules || null,
1304             "lang": lang || null
1305         }).done(function(trans) {
1306             self.set_bundle(trans);
1307         });
1308     }
1309 });
1310
1311 openerp._t = new openerp.TranslationDataBase().build_translation_function();
1312
1313 openerp.get_cookie = function(c_name) {
1314     var cookies = document.cookie ? document.cookie.split('; ') : [];
1315     for (var i = 0, l = cookies.length; i < l; i++) {
1316         var parts = cookies[i].split('=');
1317         var name = parts.shift();
1318         var cookie = parts.join('=');
1319
1320         if (c_name && c_name === name) {
1321             return cookie;
1322         }
1323     }
1324     return "";
1325 };
1326
1327 openerp.qweb = new QWeb2.Engine();
1328
1329 openerp.qweb.default_dict = {
1330     '_' : _,
1331     'JSON': JSON,
1332     '_t' : openerp._t
1333 };
1334
1335 openerp.Mutex = openerp.Class.extend({
1336     init: function() {
1337         this.def = $.Deferred().resolve();
1338     },
1339     exec: function(action) {
1340         var current = this.def;
1341         var next = this.def = $.Deferred();
1342         return current.then(function() {
1343             return $.when(action()).always(function() {
1344                 next.resolve();
1345             });
1346         });
1347     }
1348 });
1349
1350 /**
1351  * Converts a string to a Date javascript object using OpenERP's
1352  * datetime string format (exemple: '2011-12-01 15:12:35.832').
1353  * 
1354  * The time zone is assumed to be UTC (standard for OpenERP 6.1)
1355  * and will be converted to the browser's time zone.
1356  * 
1357  * @param {String} str A string representing a datetime.
1358  * @returns {Date}
1359  */
1360 openerp.str_to_datetime = function(str) {
1361     if(!str) {
1362         return str;
1363     }
1364     var regex = /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
1365     var res = regex.exec(str);
1366     if ( !res ) {
1367         throw new Error("'" + str + "' is not a valid datetime");
1368     }
1369     var tmp = new Date(2000,0,1);
1370     tmp.setUTCMonth(1970);
1371     tmp.setUTCMonth(0);
1372     tmp.setUTCDate(1);
1373     tmp.setUTCFullYear(parseFloat(res[1]));
1374     tmp.setUTCMonth(parseFloat(res[2]) - 1);
1375     tmp.setUTCDate(parseFloat(res[3]));
1376     tmp.setUTCHours(parseFloat(res[4]));
1377     tmp.setUTCMinutes(parseFloat(res[5]));
1378     tmp.setUTCSeconds(parseFloat(res[6]));
1379     tmp.setUTCSeconds(parseFloat(res[6]));
1380     tmp.setUTCMilliseconds(parseFloat(rpad((res[7] || "").slice(0, 3), 3)));
1381     return tmp;
1382 };
1383
1384 /**
1385  * Converts a string to a Date javascript object using OpenERP's
1386  * date string format (exemple: '2011-12-01').
1387  * 
1388  * As a date is not subject to time zones, we assume it should be
1389  * represented as a Date javascript object at 00:00:00 in the
1390  * time zone of the browser.
1391  * 
1392  * @param {String} str A string representing a date.
1393  * @returns {Date}
1394  */
1395 openerp.str_to_date = function(str) {
1396     if(!str) {
1397         return str;
1398     }
1399     var regex = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
1400     var res = regex.exec(str);
1401     if ( !res ) {
1402         throw new Error("'" + str + "' is not a valid date");
1403     }
1404     var tmp = new Date(2000,0,1);
1405     tmp.setFullYear(parseFloat(res[1]));
1406     tmp.setMonth(parseFloat(res[2]) - 1);
1407     tmp.setDate(parseFloat(res[3]));
1408     tmp.setHours(0);
1409     tmp.setMinutes(0);
1410     tmp.setSeconds(0);
1411     return tmp;
1412 };
1413
1414 /**
1415  * Converts a string to a Date javascript object using OpenERP's
1416  * time string format (exemple: '15:12:35').
1417  * 
1418  * The OpenERP times are supposed to always be naive times. We assume it is
1419  * represented using a javascript Date with a date 1 of January 1970 and a
1420  * time corresponding to the meant time in the browser's time zone.
1421  * 
1422  * @param {String} str A string representing a time.
1423  * @returns {Date}
1424  */
1425 openerp.str_to_time = function(str) {
1426     if(!str) {
1427         return str;
1428     }
1429     var regex = /^(\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
1430     var res = regex.exec(str);
1431     if ( !res ) {
1432         throw new Error("'" + str + "' is not a valid time");
1433     }
1434     var tmp = new Date();
1435     tmp.setFullYear(1970);
1436     tmp.setMonth(0);
1437     tmp.setDate(1);
1438     tmp.setHours(parseFloat(res[1]));
1439     tmp.setMinutes(parseFloat(res[2]));
1440     tmp.setSeconds(parseFloat(res[3]));
1441     tmp.setMilliseconds(parseFloat(rpad((res[4] || "").slice(0, 3), 3)));
1442     return tmp;
1443 };
1444
1445 /*
1446  * Left-pad provided arg 1 with zeroes until reaching size provided by second
1447  * argument.
1448  *
1449  * @param {Number|String} str value to pad
1450  * @param {Number} size size to reach on the final padded value
1451  * @returns {String} padded string
1452  */
1453 var lpad = function(str, size) {
1454     str = "" + str;
1455     return new Array(size - str.length + 1).join('0') + str;
1456 };
1457
1458 var rpad = function(str, size) {
1459     str = "" + str;
1460     return str + new Array(size - str.length + 1).join('0');
1461 };
1462
1463 /**
1464  * Converts a Date javascript object to a string using OpenERP's
1465  * datetime string format (exemple: '2011-12-01 15:12:35').
1466  * 
1467  * The time zone of the Date object is assumed to be the one of the
1468  * browser and it will be converted to UTC (standard for OpenERP 6.1).
1469  * 
1470  * @param {Date} obj
1471  * @returns {String} A string representing a datetime.
1472  */
1473 openerp.datetime_to_str = function(obj) {
1474     if (!obj) {
1475         return false;
1476     }
1477     return lpad(obj.getUTCFullYear(),4) + "-" + lpad(obj.getUTCMonth() + 1,2) + "-"
1478          + lpad(obj.getUTCDate(),2) + " " + lpad(obj.getUTCHours(),2) + ":"
1479          + lpad(obj.getUTCMinutes(),2) + ":" + lpad(obj.getUTCSeconds(),2);
1480 };
1481
1482 /**
1483  * Converts a Date javascript object to a string using OpenERP's
1484  * date string format (exemple: '2011-12-01').
1485  * 
1486  * As a date is not subject to time zones, we assume it should be
1487  * represented as a Date javascript object at 00:00:00 in the
1488  * time zone of the browser.
1489  * 
1490  * @param {Date} obj
1491  * @returns {String} A string representing a date.
1492  */
1493 openerp.date_to_str = function(obj) {
1494     if (!obj) {
1495         return false;
1496     }
1497     return lpad(obj.getFullYear(),4) + "-" + lpad(obj.getMonth() + 1,2) + "-"
1498          + lpad(obj.getDate(),2);
1499 };
1500
1501 /**
1502  * Converts a Date javascript object to a string using OpenERP's
1503  * time string format (exemple: '15:12:35').
1504  * 
1505  * The OpenERP times are supposed to always be naive times. We assume it is
1506  * represented using a javascript Date with a date 1 of January 1970 and a
1507  * time corresponding to the meant time in the browser's time zone.
1508  * 
1509  * @param {Date} obj
1510  * @returns {String} A string representing a time.
1511  */
1512 openerp.time_to_str = function(obj) {
1513     if (!obj) {
1514         return false;
1515     }
1516     return lpad(obj.getHours(),2) + ":" + lpad(obj.getMinutes(),2) + ":"
1517          + lpad(obj.getSeconds(),2);
1518 };
1519
1520 // jQuery custom plugins
1521 jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
1522     return function( elem ) {
1523         return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
1524     };
1525 });
1526
1527 openerp.declare = declare;
1528
1529 return openerp;
1530 }
1531
1532 if (typeof(define) !== "undefined") { // amd
1533     define(["jquery", "underscore", "qweb2"], declare);
1534 } else {
1535     window.openerp = declare($, _, QWeb2);
1536 }
1537
1538 })();