[MERGE] from trunk
authorXavier Morel <xmo@openerp.com>
Thu, 19 Apr 2012 08:12:13 +0000 (10:12 +0200)
committerXavier Morel <xmo@openerp.com>
Thu, 19 Apr 2012 08:12:13 +0000 (10:12 +0200)
bzr revid: xmo@openerp.com-20120419081213-m3fhofkgydj6gp1m

16 files changed:
1  2 
addons/web/controllers/main.py
addons/web/static/src/js/corelib.js
addons/web/static/src/js/data.js
addons/web/static/src/js/view_form.js
addons/web/static/test/class.js
addons/web/static/test/evals.js
addons/web/static/test/form.js
addons/web/static/test/formats.js
addons/web/static/test/fulltest.html
addons/web/static/test/list-utils.js
addons/web/static/test/onchange.js
addons/web/static/test/registry.js
addons/web/static/test/rpc.js
addons/web/static/test/test.html
addons/web_process/static/src/js/process.js
doc/index.rst

Simple merge
index 0000000,5475f4a..cbd28ac
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1772 +1,1773 @@@
+ /*
+  * Copyright (c) 2012, OpenERP S.A.
+  * All rights reserved.
+  * 
+  * Redistribution and use in source and binary forms, with or without
+  * modification, are permitted provided that the following conditions are met: 
+  * 
+  * 1. Redistributions of source code must retain the above copyright notice, this
+  *    list of conditions and the following disclaimer. 
+  * 2. Redistributions in binary form must reproduce the above copyright notice,
+  *    this list of conditions and the following disclaimer in the documentation
+  *    and/or other materials provided with the distribution. 
+  * 
+  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+  * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+  */
+ openerp.web.corelib = function(instance) {
+ /**
+  * Improved John Resig's inheritance, based on:
+  *
+  * Simple JavaScript Inheritance
+  * By John Resig http://ejohn.org/
+  * MIT Licensed.
+  *
+  * Adds "include()"
+  *
+  * Defines The Class object. That object can be used to define and inherit classes using
+  * the extend() method.
+  *
+  * Example:
+  *
+  * var Person = instance.web.Class.extend({
+  *  init: function(isDancing){
+  *     this.dancing = isDancing;
+  *   },
+  *   dance: function(){
+  *     return this.dancing;
+  *   }
+  * });
+  *
+  * The init() method act as a constructor. This class can be instancied this way:
+  *
+  * var person = new Person(true);
+  * person.dance();
+  *
+  * The Person class can also be extended again:
+  *
+  * var Ninja = Person.extend({
+  *   init: function(){
+  *     this._super( false );
+  *   },
+  *   dance: function(){
+  *     // Call the inherited version of dance()
+  *     return this._super();
+  *   },
+  *   swingSword: function(){
+  *     return true;
+  *   }
+  * });
+  *
+  * When extending a class, each re-defined method can use this._super() to call the previous
+  * implementation of that method.
+  */
+ (function() {
+     var initializing = false,
+         fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
+     // The web Class implementation (does nothing)
+     instance.web.Class = function(){};
+     /**
+      * Subclass an existing class
+      *
+      * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
+      */
+     instance.web.Class.extend = function() {
+         var _super = this.prototype;
+         // Support mixins arguments
+         var args = _.toArray(arguments);
+         args.unshift({});
+         var prop = _.extend.apply(_,args);
+         // Instantiate a web class (but only create the instance,
+         // don't run the init constructor)
+         initializing = true;
+         var prototype = new this();
+         initializing = false;
+         // Copy the properties over onto the new prototype
+         for (var name in prop) {
+             // Check if we're overwriting an existing function
+             prototype[name] = typeof prop[name] == "function" &&
+                               typeof _super[name] == "function" &&
+                               fnTest.test(prop[name]) ?
+                     (function(name, fn) {
+                         return function() {
+                             var tmp = this._super;
+                             // Add a new ._super() method that is the same
+                             // method but on the super-class
+                             this._super = _super[name];
+                             // The method only need to be bound temporarily, so
+                             // we remove it when we're done executing
+                             var ret = fn.apply(this, arguments);
+                             this._super = tmp;
+                             return ret;
+                         };
+                     })(name, prop[name]) :
+                     prop[name];
+         }
+         // The dummy class constructor
+         function Class() {
+             // All construction is actually done in the init method
+             if (!initializing && this.init) {
+                 var ret = this.init.apply(this, arguments);
+                 if (ret) { return ret; }
+             }
+             return this;
+         }
+         Class.include = function (properties) {
+             for (var name in properties) {
+                 if (typeof properties[name] !== 'function'
+                         || !fnTest.test(properties[name])) {
+                     prototype[name] = properties[name];
+                 } else if (typeof prototype[name] === 'function'
+                            && prototype.hasOwnProperty(name)) {
+                     prototype[name] = (function (name, fn, previous) {
+                         return function () {
+                             var tmp = this._super;
+                             this._super = previous;
+                             var ret = fn.apply(this, arguments);
+                             this._super = tmp;
+                             return ret;
+                         }
+                     })(name, properties[name], prototype[name]);
+                 } else if (typeof _super[name] === 'function') {
+                     prototype[name] = (function (name, fn) {
+                         return function () {
+                             var tmp = this._super;
+                             this._super = _super[name];
+                             var ret = fn.apply(this, arguments);
+                             this._super = tmp;
+                             return ret;
+                         }
+                     })(name, properties[name]);
+                 }
+             }
+         };
+         // Populate our constructed prototype object
+         Class.prototype = prototype;
+         // Enforce the constructor to be what we expect
+         Class.constructor = Class;
+         // And make this class extendable
+         Class.extend = arguments.callee;
+         return Class;
+     };
+ })();
+ // Mixins
+ /**
+  * Mixin to structure objects' life-cycles folowing a parent-children
+  * relationship. Each object can a have a parent and multiple children.
+  * When an object is destroyed, all its children are destroyed too releasing
+  * any resource they could have reserved before.
+  */
+ instance.web.ParentedMixin = {
+     __parentedMixin : true,
+     init: function() {
+         this.__parentedDestroyed = false;
+         this.__parentedChildren = [];
+         this.__parentedParent = null;
+     },
+     /**
+      * Set the parent of the current object. When calling this method, the
+      * parent will also be informed and will return the current object
+      * when its getChildren() method is called. If the current object did
+      * already have a parent, it is unregistered before, which means the
+      * previous parent will not return the current object anymore when its
+      * getChildren() method is called.
+      */
+     setParent : function(parent) {
+         if (this.getParent()) {
+             if (this.getParent().__parentedMixin) {
+                 this.getParent().__parentedChildren = _.without(this
+                         .getParent().getChildren(), this);
+             }
+         }
+         this.__parentedParent = parent;
+         if (parent && parent.__parentedMixin) {
+             parent.__parentedChildren.push(this);
+         }
+     },
+     /**
+      * Return the current parent of the object (or null).
+      */
+     getParent : function() {
+         return this.__parentedParent;
+     },
+     /**
+      * Return a list of the children of the current object.
+      */
+     getChildren : function() {
+         return _.clone(this.__parentedChildren);
+     },
+     /**
+      * Returns true if destroy() was called on the current object.
+      */
+     isDestroyed : function() {
+         return this.__parentedDestroyed;
+     },
+     /**
+      * Inform the object it should destroy itself, releasing any
+      * resource it could have reserved.
+      */
+     destroy : function() {
+         _.each(this.getChildren(), function(el) {
+             el.destroy();
+         });
+         this.setParent(undefined);
+         this.__parentedDestroyed = true;
+     }
+ };
+ /**
+  * TODO al: move into the the mixin
+  *
+  * Backbone's events
+  *
+  * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
+  * Backbone may be freely distributed under the MIT license.
+  * For all details and documentation:
+  * http://backbonejs.org
+  * 
+  * This class just handle the dispatching of events, it is not meant to be extended,
+  * nor used directly. All integration with parenting and automatic unregistration of
+  * events is done in EventDispatcherMixin.
+  *
+  */
+ instance.web.Events = instance.web.Class.extend({
+     on : function(events, callback, context) {
+         var ev;
+         events = events.split(/\s+/);
+         var calls = this._callbacks || (this._callbacks = {});
+         while (ev = events.shift()) {
+             var list = calls[ev] || (calls[ev] = {});
+             var tail = list.tail || (list.tail = list.next = {});
+             tail.callback = callback;
+             tail.context = context;
+             list.tail = tail.next = {};
+         }
+         return this;
+     },
+     off : function(events, callback, context) {
+         var ev, calls, node;
+         if (!events) {
+             delete this._callbacks;
+         } else if (calls = this._callbacks) {
+             events = events.split(/\s+/);
+             while (ev = events.shift()) {
+                 node = calls[ev];
+                 delete calls[ev];
+                 if (!callback || !node)
+                     continue;
+                 while ((node = node.next) && node.next) {
+                     if (node.callback === callback
+                             && (!context || node.context === context))
+                         continue;
+                     this.on(ev, node.callback, node.context);
+                 }
+             }
+         }
+         return this;
+     },
+     trigger : function(events) {
+         var event, node, calls, tail, args, all, rest;
+         if (!(calls = this._callbacks))
+             return this;
+         all = calls['all'];
+         (events = events.split(/\s+/)).push(null);
+         // Save references to the current heads & tails.
+         while (event = events.shift()) {
+             if (all)
+                 events.push({
+                     next : all.next,
+                     tail : all.tail,
+                     event : event
+                 });
+             if (!(node = calls[event]))
+                 continue;
+             events.push({
+                 next : node.next,
+                 tail : node.tail
+             });
+         }
+         rest = Array.prototype.slice.call(arguments, 1);
+         while (node = events.pop()) {
+             tail = node.tail;
+             args = node.event ? [ node.event ].concat(rest) : rest;
+             while ((node = node.next) !== tail) {
+                 node.callback.apply(node.context || this, args);
+             }
+         }
+         return this;
+     }
+ });
+ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, {
+     __eventDispatcherMixin: true,
+     init: function() {
+         instance.web.ParentedMixin.init.call(this);
+         this.__edispatcherEvents = new instance.web.Events();
+         this.__edispatcherRegisteredEvents = [];
+     },
+     on: function(events, dest, func) {
+         var self = this;
+         events = events.split(/\s+/);
+         _.each(events, function(eventName) {
+             self.__edispatcherEvents.on(eventName, func, dest);
+             if (dest && dest.__eventDispatcherMixin) {
+                 dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
+             }
+         });
+         return this;
+     },
+     off: function(events, dest, func) {
+         var self = this;
+         events = events.split(/\s+/);
+         _.each(events, function(eventName) {
+             self.__edispatcherEvents.off(eventName, func, dest);
+             if (dest && dest.__eventDispatcherMixin) {
+                 dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
+                     return !(el.name === eventName && el.func === func && el.source === self);
+                 });
+             }
+         });
+         return this;
+     },
+     trigger: function(events) {
+         this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
+         return this;
+     },
+     destroy: function() {
+         var self = this;
+         _.each(this.__edispatcherRegisteredEvents, function(event) {
+             event.source.__edispatcherEvents.off(event.name, event.func, self);
+         });
+         this.__edispatcherRegisteredEvents = [];
+         if(!this.__edispatcherEvents) {
+             debugger;
+         }
+         this.__edispatcherEvents.off();
+         instance.web.ParentedMixin.destroy.call(this);
+     }
+ });
+ instance.web.GetterSetterMixin = _.extend({}, instance.web.EventDispatcherMixin, {
+     init: function() {
+         instance.web.EventDispatcherMixin.init.call(this);
+         this.__getterSetterInternalMap = {};
+     },
+     set: function(map) {
+         var self = this;
+         var changed = false;
+         _.each(map, function(val, key) {
+             var tmp = self.__getterSetterInternalMap[key];
+             if (tmp === val)
+                 return;
+             changed = true;
+             self.__getterSetterInternalMap[key] = val;
+             self.trigger("change:" + key, self, {
+                 oldValue: tmp,
+                 newValue: val
+             });
+         });
+         if (changed)
+             self.trigger("change", self);
+     },
+     get: function(key) {
+         return this.__getterSetterInternalMap[key];
+     }
+ });
+ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.GetterSetterMixin, {
+     init: function() {
+         instance.web.GetterSetterMixin.init.call(this);
+         var self = this;
+         var callback_maker = function(obj, name, method) {
+             var callback = function() {
+                 var args = Array.prototype.slice.call(arguments);
+                 self.trigger.apply(self, [name].concat(args));
+                 var r;
+                 for(var i = 0; i < callback.callback_chain.length; i++)  {
+                     var c = callback.callback_chain[i];
+                     if(c.unique) {
+                         callback.callback_chain.splice(i, 1);
+                         i -= 1;
+                     }
+                     var result = c.callback.apply(c.self, c.args.concat(args));
+                     if (c.callback === method) {
+                         // return the result of the original method
+                         r = result;
+                     }
+                     // TODO special value to stop the chain
+                     // instance.web.callback_stop
+                 }
+                 return r;
+             };
+             callback.callback_chain = [];
+             callback.add = function(f) {
+                 if(typeof(f) == 'function') {
+                     f = { callback: f, args: Array.prototype.slice.call(arguments, 1) };
+                 }
+                 f.self = f.self || null;
+                 f.args = f.args || [];
+                 f.unique = !!f.unique;
+                 if(f.position == 'last') {
+                     callback.callback_chain.push(f);
+                 } else {
+                     callback.callback_chain.unshift(f);
+                 }
+                 return callback;
+             };
+             callback.add_first = function(f) {
+                 return callback.add.apply(null,arguments);
+             };
+             callback.add_last = function(f) {
+                 return callback.add({
+                     callback: f,
+                     args: Array.prototype.slice.call(arguments, 1),
+                     position: "last"
+                 });
+             };
+             callback.remove = function(f) {
+                 callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) {
+                     return el.callback === f;
+                 }));
+                 return callback;
+             };
+             return callback.add({
+                 callback: method,
+                 self:obj,
+                 args:Array.prototype.slice.call(arguments, 3)
+             });
+         };
+         // Transform on_/do_* methods into callbacks
+         for (var name in this) {
+             if(typeof(this[name]) == "function") {
+                 this[name].debug_name = name;
+                 if((/^on_|^do_/).test(name)) {
+                     this[name] = callback_maker(this, name, this[name]);
+                 }
+             }
+         }
+     },
+     /**
+      * Proxies a method of the object, in order to keep the right ``this`` on
+      * method invocations.
+      *
+      * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
+      * even more so to ``jQuery.proxy`` with a fundamental difference: its
+      * resolution of the method being called is lazy, meaning it will use the
+      * method as it is when the proxy is called, not when the proxy is created.
+      *
+      * Other methods will fix the bound method to what it is when creating the
+      * binding/proxy, which is fine in most javascript code but problematic in
+      * OpenERP Web where developers may want to replace existing callbacks with
+      * theirs.
+      *
+      * The semantics of this precisely replace closing over the method call.
+      *
+      * @param {String} method_name name of the method to invoke
+      * @returns {Function} proxied method
+      */
+     proxy: function (method_name) {
+         var self = this;
+         return function () {
+             return self[method_name].apply(self, arguments);
+         }
+     }
+ });
+ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
+     /**
+      * Tag name when creating a default $element.
+      * @type string
+      */
+     tagName: 'div',
+     /**
+      * Constructs the widget and sets its parent if a parent is given.
+      *
+      * @constructs instance.web.Widget
+      * @extends instance.web.CallbackEnabled
+      *
+      * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
+      * When that widget is destroyed by calling destroy(), the current instance will be
+      * destroyed too. Can be null.
+      * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
+      * to bind the current Widget to an already existing part of the DOM, which is not compatible
+      * with the DOM insertion methods provided by the current implementation of Widget. So
+      * for new components this argument should not be provided any more.
+      */
+     init: function(parent) {
+         instance.web.CallbackEnabledMixin.init.call(this);
+         this.$element = $(document.createElement(this.tagName));
+         this.setParent(parent);
+     },
+     /**
+      * Destroys the current widget, also destroys all its children before destroying itself.
+      */
+     destroy: function() {
+         _.each(this.getChildren(), function(el) {
+             el.destroy();
+         });
+         if(this.$element != null) {
+             this.$element.remove();
+         }
+         instance.web.GetterSetterMixin.destroy.call(this);
+     },
+     /**
+      * Renders the current widget and appends it to the given jQuery object or Widget.
+      *
+      * @param target A jQuery object or a Widget instance.
+      */
+     appendTo: function(target) {
+         var self = this;
+         return this.__widgetRenderAndInsert(function(t) {
+             self.$element.appendTo(t);
+         }, target);
+     },
+     /**
+      * Renders the current widget and prepends it to the given jQuery object or Widget.
+      *
+      * @param target A jQuery object or a Widget instance.
+      */
+     prependTo: function(target) {
+         var self = this;
+         return this.__widgetRenderAndInsert(function(t) {
+             self.$element.prependTo(t);
+         }, target);
+     },
+     /**
+      * Renders the current widget and inserts it after to the given jQuery object or Widget.
+      *
+      * @param target A jQuery object or a Widget instance.
+      */
+     insertAfter: function(target) {
+         var self = this;
+         return this.__widgetRenderAndInsert(function(t) {
+             self.$element.insertAfter(t);
+         }, target);
+     },
+     /**
+      * Renders the current widget and inserts it before to the given jQuery object or Widget.
+      *
+      * @param target A jQuery object or a Widget instance.
+      */
+     insertBefore: function(target) {
+         var self = this;
+         return this.__widgetRenderAndInsert(function(t) {
+             self.$element.insertBefore(t);
+         }, target);
+     },
+     /**
+      * Renders the current widget and replaces the given jQuery object.
+      *
+      * @param target A jQuery object or a Widget instance.
+      */
+     replace: function(target) {
+         return this.__widgetRenderAndInsert(_.bind(function(t) {
+             this.$element.replaceAll(t);
+         }, this), target);
+     },
+     __widgetRenderAndInsert: function(insertion, target) {
+         this.renderElement();
+         insertion(target);
+         return this.start();
+     },
+     /**
+      * This is the method to implement to render the Widget.
+      */
+     renderElement: function() {
+     },
+     /**
+      * Method called after rendering. Mostly used to bind actions, perform asynchronous
+      * calls, etc...
+      *
+      * By convention, the method should return a promise to inform the caller when
+      * this widget has been initialized.
+      *
+      * @returns {jQuery.Deferred}
+      */
+     start: function() {
+     }
+ });
+ // Classes
+ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEnabledMixin, {
+     init: function() {
+         instance.web.CallbackEnabledMixin.init.call(this);
+     }
+ });
+ /**
+  * Base class for all visual components. Provides a lot of functionalities helpful
+  * for the management of a part of the DOM.
+  *
+  * Widget handles:
+  * - Rendering with QWeb.
+  * - Life-cycle management and parenting (when a parent is destroyed, all its children are
+  *     destroyed too).
+  * - Insertion in DOM.
+  *
+  * Guide to create implementations of the Widget class:
+  * ==============================================
+  *
+  * Here is a sample child class:
+  *
+  * MyWidget = instance.base.Widget.extend({
+  *     // the name of the QWeb template to use for rendering
+  *     template: "MyQWebTemplate",
+  *
+  *     init: function(parent) {
+  *         this._super(parent);
+  *         // stuff that you want to init before the rendering
+  *     },
+  *     start: function() {
+  *         // stuff you want to make after the rendering, `this.$element` holds a correct value
+  *         this.$element.find(".my_button").click(/* an example of event binding * /);
+  *
+  *         // if you have some asynchronous operations, it's a good idea to return
+  *         // a promise in start()
+  *         var promise = this.rpc(...);
+  *         return promise;
+  *     }
+  * });
+  *
+  * Now this class can simply be used with the following syntax:
+  *
+  * var my_widget = new MyWidget(this);
+  * my_widget.appendTo($(".some-div"));
+  *
+  * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
+  * DOM inside the ".some-div" div and its events were binded.
+  *
+  * And of course, when you don't need that widget anymore, just do:
+  *
+  * my_widget.destroy();
+  *
+  * That will kill the widget in a clean way and erase its content from the dom.
+  */
+ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, {
+     /**
+      * The name of the QWeb template that will be used for rendering. Must be
+      * redefined in subclasses or the default render() method can not be used.
+      *
+      * @type string
+      */
+     template: null,
+     /**
+      * Constructs the widget and sets its parent if a parent is given.
+      *
+      * @constructs instance.web.Widget
+      * @extends instance.web.CallbackEnabled
+      *
+      * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
+      * When that widget is destroyed by calling destroy(), the current instance will be
+      * destroyed too. Can be null.
+      * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
+      * to bind the current Widget to an already existing part of the DOM, which is not compatible
+      * with the DOM insertion methods provided by the current implementation of Widget. So
+      * for new components this argument should not be provided any more.
+      */
+     init: function(parent) {
+         instance.web.WidgetMixin.init.call(this,parent);
+         this.session = instance.connection;
+     },
+     /**
+      * Renders the element. The default implementation renders the widget using QWeb,
+      * `this.template` must be defined. The context given to QWeb contains the "widget"
+      * key that references `this`.
+      */
+     renderElement: function() {
+         var rendered = null;
+         if (this.template)
+             rendered = instance.web.qweb.render(this.template, {widget: this});
+         if (_.str.trim(rendered)) {
+             var elem = $(rendered);
+             this.$element.replaceWith(elem);
+             this.$element = elem;
+         }
+     },
+     /**
+      * Informs the action manager to do an action. This supposes that
+      * the action manager can be found amongst the ancestors of the current widget.
+      * If that's not the case this method will simply return `false`.
+      */
+     do_action: function(action, on_finished) {
+         if (this.getParent()) {
+             return this.getParent().do_action(action, on_finished);
+         }
+         return false;
+     },
+     do_notify: function() {
+         if (this.getParent()) {
+             return this.getParent().do_notify.apply(this,arguments);
+         }
+         return false;
+     },
+     do_warn: function() {
+         if (this.getParent()) {
+             return this.getParent().do_warn.apply(this,arguments);
+         }
+         return false;
+     },
+     rpc: function(url, data, success, error) {
+         var def = $.Deferred().then(success, error);
+         var self = this;
+         instance.connection.rpc(url, data). then(function() {
+             if (!self.isDestroyed())
+                 def.resolve.apply(def, arguments);
+         }, function() {
+             if (!self.isDestroyed())
+                 def.reject.apply(def, arguments);
+         });
+         return def.promise();
+     }
+ });
+ instance.web.Registry = instance.web.Class.extend({
+     /**
+      * Stores a mapping of arbitrary key (strings) to object paths (as strings
+      * as well).
+      *
+      * Resolves those paths at query time in order to always fetch the correct
+      * object, even if those objects have been overloaded/replaced after the
+      * registry was created.
+      *
+      * An object path is simply a dotted name from the instance root to the
+      * object pointed to (e.g. ``"instance.web.Connection"`` for an OpenERP
+      * connection object).
+      *
+      * @constructs instance.web.Registry
+      * @param {Object} mapping a mapping of keys to object-paths
+      */
+     init: function (mapping) {
+         this.parent = null;
+         this.map = mapping || {};
+     },
+     /**
+      * Retrieves the object matching the provided key string.
+      *
+      * @param {String} key the key to fetch the object for
+      * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception
+      * @returns {Class} the stored class, to initialize or null if not found
+      */
+     get_object: function (key, silent_error) {
+         var path_string = this.map[key];
+         if (path_string === undefined) {
+             if (this.parent) {
+                 return this.parent.get_object(key, silent_error);
+             }
+             if (silent_error) { return void 'nooo'; }
+             return null;
+         }
+         var object_match = instance;
+         var path = path_string.split('.');
+         // ignore first section
+         for(var i=1; i<path.length; ++i) {
+             object_match = object_match[path[i]];
+             if (object_match === undefined) {
+                 if (silent_error) { return void 'noooooo'; }
+                 return null;
+             }
+         }
+         return object_match;
+     },
+     /**
+      * Checks if the registry contains an object mapping for this key.
+      *
+      * @param {String} key key to look for
+      */
+     contains: function (key) {
+         if (key === undefined) { return false; }
+         if (key in this.map) {
+             return true
+         }
+         if (this.parent) {
+             return this.parent.contains(key);
+         }
+         return false;
+     },
+     /**
+      * Tries a number of keys, and returns the first object matching one of
+      * the keys.
+      *
+      * @param {Array} keys a sequence of keys to fetch the object for
+      * @returns {Class} the first class found matching an object
+      */
+     get_any: function (keys) {
+         for (var i=0; i<keys.length; ++i) {
+             var key = keys[i];
+             if (!this.contains(key)) {
+                 continue;
+             }
+             return this.get_object(key);
+         }
+         return null;
+     },
+     /**
+      * Adds a new key and value to the registry.
+      *
+      * This method can be chained.
+      *
+      * @param {String} key
+      * @param {String} object_path fully qualified dotted object path
+      * @returns {instance.web.Registry} itself
+      */
+     add: function (key, object_path) {
+         this.map[key] = object_path;
+         return this;
+     },
+     /**
+      * Creates and returns a copy of the current mapping, with the provided
+      * mapping argument added in (replacing existing keys if needed)
+      *
+      * Parent and child remain linked, a new key in the parent (which is not
+      * overwritten by the child) will appear in the child.
+      *
+      * @param {Object} [mapping={}] a mapping of keys to object-paths
+      */
+     extend: function (mapping) {
+         var child = new instance.web.Registry(mapping);
+         child.parent = this;
+         return child;
+     },
+     /**
+      * @deprecated use Registry#extend
+      */
+     clone: function (mapping) {
+         console.warn('Registry#clone is deprecated, use Registry#extend');
+         return this.extend(mapping);
+     }
+ });
+ instance.web.Connection = instance.web.CallbackEnabled.extend( /** @lends instance.web.Connection# */{
+     /**
+      * @constructs instance.web.Connection
+      * @extends instance.web.CallbackEnabled
+      *
+      * @param {String} [server] JSON-RPC endpoint hostname
+      * @param {String} [port] JSON-RPC endpoint port
+      */
+     init: function() {
+         this._super();
+         this.server = null;
+         this.debug = ($.deparam($.param.querystring()).debug != undefined);
+         // TODO: session store in cookie should be optional
+         this.name = instance._session_id;
+         this.qweb_mutex = new $.Mutex();
+     },
+     session_bind: function(origin) {
+         var window_origin = location.protocol+"//"+location.host, self=this;
+         this.origin = origin ? _.str.rtrim(origin,'/') : window_origin;
+         this.prefix = this.origin;
+         this.server = this.origin; // keep chs happy
+         instance.web.qweb.default_dict['_s'] = this.origin;
+         this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
+         this.session_id = false;
+         this.uid = false;
+         this.username = false;
+         this.user_context= {};
+         this.db = false;
+         this.openerp_entreprise = false;
+         this.module_list = instance._modules.slice();
+         this.module_loaded = {};
+         _(this.module_list).each(function (mod) {
+             self.module_loaded[mod] = true;
+         });
+         this.context = {};
+         this.shortcuts = [];
+         this.active_id = null;
+         return this.session_init();
+     },
+     test_eval_get_context: function () {
+         var asJS = function (arg) {
+             if (arg instanceof py.object) {
+                 return arg.toJSON();
+             }
+             return arg;
+         };
+         var datetime = new py.object();
+         datetime.datetime = new py.type(function datetime() {
+             throw new Error('datetime.datetime not implemented');
+         });
+         var date = datetime.date = new py.type(function date(y, m, d) {
+             if (y instanceof Array) {
+                 d = y[2];
+                 m = y[1];
+                 y = y[0];
+             }
+             this.year = asJS(y);
+             this.month = asJS(m);
+             this.day = asJS(d);
+         }, py.object, {
+             strftime: function (args) {
+                 var f = asJS(args[0]), self = this;
+                 return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) {
+                     switch (c) {
+                     case 'Y': return self.year;
+                     case 'm': return _.str.sprintf('%02d', self.month);
+                     case 'd': return _.str.sprintf('%02d', self.day);
+                     }
+                     throw new Error('ValueError: No known conversion for ' + m);
+                 }));
+             }
+         });
+         date.__getattribute__ = function (name) {
+             if (name === 'today') {
+                 return date.today;
+             }
+             throw new Error("AttributeError: object 'date' has no attribute '" + name +"'");
+         };
+         date.today = new py.def(function () {
+             var d = new Date();
+             return new date(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
+         });
+         datetime.time = new py.type(function time() {
+             throw new Error('datetime.time not implemented');
+         });
+         var time = new py.object();
+         time.strftime = new py.def(function (args) {
+             return date.today.__call__().strftime(args);
+         });
+         var relativedelta = new py.type(function relativedelta(args, kwargs) {
+             if (!_.isEmpty(args)) {
+                 throw new Error('Extraction of relative deltas from existing datetimes not supported');
+             }
+             this.ops = kwargs;
+         }, py.object, {
+             __add__: function (other) {
+                 if (!(other instanceof datetime.date)) {
+                     return py.NotImplemented;
+                 }
+                 // TODO: test this whole mess
+                 var year = asJS(this.ops.year) || asJS(other.year);
+                 if (asJS(this.ops.years)) {
+                     year += asJS(this.ops.years);
+                 }
+                 var month = asJS(this.ops.month) || asJS(other.month);
+                 if (asJS(this.ops.months)) {
+                     month += asJS(this.ops.months);
+                     // FIXME: no divmod in JS?
+                     while (month < 1) {
+                         year -= 1;
+                         month += 12;
+                     }
+                     while (month > 12) {
+                         year += 1;
+                         month -= 12;
+                     }
+                 }
+                 var lastMonthDay = new Date(year, month, 0).getDate();
+                 var day = asJS(this.ops.day) || asJS(other.day);
+                 if (day > lastMonthDay) { day = lastMonthDay; }
+                 var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
+                 if (days_offset) {
+                     day = new Date(year, month-1, day + days_offset).getDate();
+                 }
+                 // TODO: leapdays?
+                 // TODO: hours, minutes, seconds? Not used in XML domains
+                 // TODO: weekday?
+                 return new datetime.date(year, month, day);
+             },
+             __radd__: function (other) {
+                 return this.__add__(other);
+             },
+             __sub__: function (other) {
+                 if (!(other instanceof datetime.date)) {
+                     return py.NotImplemented;
+                 }
+                 // TODO: test this whole mess
+                 var year = asJS(this.ops.year) || asJS(other.year);
+                 if (asJS(this.ops.years)) {
+                     year -= asJS(this.ops.years);
+                 }
+                 var month = asJS(this.ops.month) || asJS(other.month);
+                 if (asJS(this.ops.months)) {
+                     month -= asJS(this.ops.months);
+                     // FIXME: no divmod in JS?
+                     while (month < 1) {
+                         year -= 1;
+                         month += 12;
+                     }
+                     while (month > 12) {
+                         year += 1;
+                         month -= 12;
+                     }
+                 }
+                 var lastMonthDay = new Date(year, month, 0).getDate();
+                 var day = asJS(this.ops.day) || asJS(other.day);
+                 if (day > lastMonthDay) { day = lastMonthDay; }
+                 var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
+                 if (days_offset) {
+                     day = new Date(year, month-1, day - days_offset).getDate();
+                 }
+                 // TODO: leapdays?
+                 // TODO: hours, minutes, seconds? Not used in XML domains
+                 // TODO: weekday?
+                 return new datetime.date(year, month, day);
+             },
+             __rsub__: function (other) {
+                 return this.__sub__(other);
+             }
+         });
+         return {
+             uid: new py.float(this.uid),
+             datetime: datetime,
+             time: time,
+             relativedelta: relativedelta
+         };
+     },
+     /**
+      * FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching
+      */
+     test_eval: function (source, expected) {
+         var match_template = '<ul>' +
+                 '<li>Source: %(source)s</li>' +
+                 '<li>Local: %(local)s</li>' +
+                 '<li>Remote: %(remote)s</li>' +
+             '</ul>',
+             fail_template = '<ul>' +
+                 '<li>Error: %(error)s</li>' +
+                 '<li>Source: %(source)s</li>' +
+             '</ul>';
+         try {
+             var ctx = this.test_eval_contexts(source.contexts);
+             if (!_.isEqual(ctx, expected.context)) {
+                 instance.webclient.notification.warn('Context mismatch, report to xmo',
+                     _.str.sprintf(match_template, {
+                         source: JSON.stringify(source.contexts),
+                         local: JSON.stringify(ctx),
+                         remote: JSON.stringify(expected.context)
+                     }), true);
+             }
+         } catch (e) {
+             instance.webclient.notification.warn('Context fail, report to xmo',
+                 _.str.sprintf(fail_template, {
+                     error: e.message,
+                     source: JSON.stringify(source.contexts)
+                 }), true);
+         }
+         try {
+             var dom = this.test_eval_domains(source.domains, this.test_eval_get_context());
+             if (!_.isEqual(dom, expected.domain)) {
+                 instance.webclient.notification.warn('Domains mismatch, report to xmo',
+                     _.str.sprintf(match_template, {
+                         source: JSON.stringify(source.domains),
+                         local: JSON.stringify(dom),
+                         remote: JSON.stringify(expected.domain)
+                     }), true);
+             }
+         } catch (e) {
+             instance.webclient.notification.warn('Domain fail, report to xmo',
+                 _.str.sprintf(fail_template, {
+                     error: e.message,
+                     source: JSON.stringify(source.domains)
+                 }), true);
+         }
+         try {
+             var groups = this.test_eval_groupby(source.group_by_seq);
+             if (!_.isEqual(groups, expected.group_by)) {
+                 instance.webclient.notification.warn('GroupBy mismatch, report to xmo',
+                     _.str.sprintf(match_template, {
+                         source: JSON.stringify(source.group_by_seq),
+                         local: JSON.stringify(groups),
+                         remote: JSON.stringify(expected.group_by)
+                     }), true);
+             }
+         } catch (e) {
+             instance.webclient.notification.warn('GroupBy fail, report to xmo',
+                 _.str.sprintf(fail_template, {
+                     error: e.message,
+                     source: JSON.stringify(source.group_by_seq)
+                 }), true);
+         }
+     },
+     test_eval_contexts: function (contexts, evaluation_context) {
+         evaluation_context = evaluation_context || {};
+         var self = this;
+         return _(contexts).reduce(function (result_context, ctx) {
+             // __eval_context evaluations can lead to some of `contexts`'s
+             // values being null, skip them as well as empty contexts
+             if (_.isEmpty(ctx)) { return result_context; }
+             var evaluated = ctx;
+             switch(ctx.__ref) {
+             case 'context':
+                 evaluated = py.eval(ctx.__debug, evaluation_context);
+                 break;
+             case 'compound_context':
+                 var eval_context = self.test_eval_contexts([ctx.__eval_context]);
+                 evaluated = self.test_eval_contexts(
+                     ctx.__contexts, _.extend({}, evaluation_context, eval_context));
+                 break;
+             }
+             // add newly evaluated context to evaluation context for following
+             // siblings
+             _.extend(evaluation_context, evaluated);
+             return _.extend(result_context, evaluated);
+         }, _.extend({}, this.user_context));
+     },
+     test_eval_domains: function (domains, evaluation_context) {
+         var result_domain = [], self = this;
+         _(domains).each(function (dom) {
+             switch(dom.__ref) {
+             case 'domain':
+                 result_domain.push.apply(
+                     result_domain, py.eval(dom.__debug, evaluation_context));
+                 break;
+             case 'compound_domain':
+                 var eval_context = self.test_eval_contexts([dom.__eval_context]);
+                 result_domain.push.apply(
+                     result_domain, self.test_eval_domains(
+                         dom.__domains, _.extend(
+                             {}, evaluation_context, eval_context)));
+                 break;
+             default:
+                 result_domain.push.apply(
+                     result_domain, dom);
+             }
+         });
+         return result_domain;
+     },
+     test_eval_groupby: function (contexts) {
+         var result_group = [], self = this;
+         _(contexts).each(function (ctx) {
+             var group;
+             switch(ctx.__ref) {
+             case 'context':
+                 group = py.eval(ctx.__debug).group_by;
+                 break;
+             case 'compound_context':
+                 group = self.test_eval_contexts(
+                     ctx.__contexts, ctx.__eval_context).group_by;
+                 break;
+             default:
+                 group = ctx.group_by
+             }
+             if (!group) { return; }
+             if (typeof group === 'string') {
+                 result_group.push(group);
+             } else if (group instanceof Array) {
+                 result_group.push.apply(result_group, group);
+             } else {
+                 throw new Error('Got invalid groupby {{'
+                         + JSON.stringify(group) + '}}');
+             }
+         });
+         return result_group;
+     },
+     /**
+      * Executes an RPC call, registering the provided callbacks.
+      *
+      * Registers a default error callback if none is provided, and handles
+      * setting the correct session id and session context in the parameter
+      * objects
+      *
+      * @param {String} url RPC endpoint
+      * @param {Object} params call parameters
+      * @param {Function} success_callback function to execute on RPC call success
+      * @param {Function} error_callback function to execute on RPC call failure
+      * @returns {jQuery.Deferred} jquery-provided ajax deferred
+      */
+     rpc: function(url, params, success_callback, error_callback) {
+         var self = this;
+         // url can be an $.ajax option object
+         if (_.isString(url)) {
+             url = { url: url };
+         }
+         // Construct a JSON-RPC2 request, method is currently unused
+         params.session_id = this.session_id;
+         if (this.debug)
+             params.debug = 1;
+         var payload = {
+             jsonrpc: '2.0',
+             method: 'call',
+             params: params,
+             id: _.uniqueId('r')
+         };
+         var deferred = $.Deferred();
+         this.on_rpc_request();
+         var aborter = params.aborter;
+         delete params.aborter;
+         var request = this.rpc_function(url, payload).then(
+             function (response, textStatus, jqXHR) {
+                 self.on_rpc_response();
+                 if (!response.error) {
+                     if (url.url === '/web/session/eval_domain_and_context') {
+                         self.test_eval(params, response.result);
+                     }
+                     deferred.resolve(response["result"], textStatus, jqXHR);
+                 } else if (response.error.data.type === "session_invalid") {
+                     self.uid = false;
+                     // TODO deprecate or use a deferred on login.do_ask_login()
+                     self.on_session_invalid(function() {
+                         self.rpc(url, payload.params,
+                             function() { deferred.resolve.apply(deferred, arguments); },
+                             function() { deferred.reject.apply(deferred, arguments); });
+                     });
+                 } else {
+                     deferred.reject(response.error, $.Event());
+                 }
+             },
+             function(jqXHR, textStatus, errorThrown) {
+                 self.on_rpc_response();
+                 var error = {
+                     code: -32098,
+                     message: "XmlHttpRequestError " + errorThrown,
+                     data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] }
+                 };
+                 deferred.reject(error, $.Event());
+             });
+         if (aborter) {
+             aborter.abort_last = function () {
+                 if (!(request.isResolved() || request.isRejected())) {
+                     deferred.fail(function (error, event) {
+                         event.preventDefault();
+                     });
+                     request.abort();
+                 }
+             };
+         }
+         // Allow deferred user to disable on_rpc_error in fail
+         deferred.fail(function() {
+             deferred.fail(function(error, event) {
+                 if (!event.isDefaultPrevented()) {
+                     self.on_rpc_error(error, event);
+                 }
+             });
+         }).then(success_callback, error_callback).promise();
+         return deferred;
+     },
+     /**
+      * Raw JSON-RPC call
+      *
+      * @returns {jQuery.Deferred} ajax-webd deferred object
+      */
+     rpc_json: function(url, payload) {
+         var self = this;
+         var ajax = _.extend({
+             type: "POST",
+             dataType: 'json',
+             contentType: 'application/json',
+             data: JSON.stringify(payload),
+             processData: false
+         }, url);
+         if (this.synch)
+               ajax.async = false;
+         return $.ajax(ajax);
+     },
+     rpc_jsonp: function(url, payload) {
+         var self = this;
+         // extracted from payload to set on the url
+         var data = {
+             session_id: this.session_id,
+             id: payload.id
+         };
+         url.url = this.get_url(url.url);
+         var ajax = _.extend({
+             type: "GET",
+             dataType: 'jsonp', 
+             jsonp: 'jsonp',
+             cache: false,
+             data: data
+         }, url);
+         if (this.synch)
+               ajax.async = false;
+         var payload_str = JSON.stringify(payload);
+         var payload_url = $.param({r:payload_str});
+         if(payload_url.length < 2000) {
+             // Direct jsonp request
+             ajax.data.r = payload_str;
+             return $.ajax(ajax);
+         } else {
+             // Indirect jsonp request
+             var ifid = _.uniqueId('oe_rpc_iframe');
+             var display = options.openerp.debug ? 'block' : 'none';
+             var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display));
+             var $form = $('<form>')
+                         .attr('method', 'POST')
+                         .attr('target', ifid)
+                         .attr('enctype', "multipart/form-data")
+                         .attr('action', ajax.url + '?' + $.param(data))
+                         .append($('<input type="hidden" name="r" />').attr('value', payload_str))
+                         .hide()
+                         .appendTo($('body'));
+             var cleanUp = function() {
+                 if ($iframe) {
+                     $iframe.unbind("load").attr("src", "javascript:false;").remove();
+                 }
+                 $form.remove();
+             };
+             var deferred = $.Deferred();
+             // the first bind is fired up when the iframe is added to the DOM
+             $iframe.bind('load', function() {
+                 // the second bind is fired up when the result of the form submission is received
+                 $iframe.unbind('load').bind('load', function() {
+                     $.ajax(ajax).always(function() {
+                         cleanUp();
+                     }).then(
+                         function() { deferred.resolve.apply(deferred, arguments); },
+                         function() { deferred.reject.apply(deferred, arguments); }
+                     );
+                 });
+                 // now that the iframe can receive data, we fill and submit the form
+                 $form.submit();
+             });
+             // append the iframe to the DOM (will trigger the first load)
+             $form.after($iframe);
+             return deferred;
+         }
+     },
+     on_rpc_request: function() {
+     },
+     on_rpc_response: function() {
+     },
+     on_rpc_error: function(error) {
+     },
+     /**
+      * Init a session, reloads from cookie, if it exists
+      */
+     session_init: function () {
+         var self = this;
+         // TODO: session store in cookie should be optional
+         this.session_id = this.get_cookie('session_id');
+         return this.session_reload().pipe(function(result) {
+             var modules = instance._modules.join(',');
+             var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb);
+             if(self.session_is_valid()) {
+                 return deferred.pipe(function() { return self.load_modules(); });
+             }
+             return deferred;
+         });
+     },
+     /**
+      * (re)loads the content of a session: db name, username, user id, session
+      * context and status of the support contract
+      *
+      * @returns {$.Deferred} deferred indicating the session is done reloading
+      */
+     session_reload: function () {
+         var self = this;
+         return this.rpc("/web/session/get_session_info", {}).then(function(result) {
+             // If immediately follows a login (triggered by trying to restore
+             // an invalid session or no session at all), refresh session data
+             // (should not change, but just in case...)
+             _.extend(self, {
++                session_id: result.session_id,
+                 db: result.db,
+                 username: result.login,
+                 uid: result.uid,
+                 user_context: result.context,
+                 openerp_entreprise: result.openerp_entreprise
+             });
+         });
+     },
+     session_is_valid: function() {
+         return !!this.uid;
+     },
+     /**
+      * The session is validated either by login or by restoration of a previous session
+      */
+     session_authenticate: function(db, login, password, _volatile) {
+         var self = this;
+         var base_location = document.location.protocol + '//' + document.location.host;
+         var params = { db: db, login: login, password: password, base_location: base_location };
+         return this.rpc("/web/session/authenticate", params).pipe(function(result) {
+             _.extend(self, {
+                 session_id: result.session_id,
+                 db: result.db,
+                 username: result.login,
+                 uid: result.uid,
+                 user_context: result.context,
+                 openerp_entreprise: result.openerp_entreprise
+             });
+             if (!_volatile) {
+                 self.set_cookie('session_id', self.session_id);
+             }
+             return self.load_modules();
+         });
+     },
+     session_logout: function() {
+         this.set_cookie('session_id', '');
+         return this.rpc("/web/session/destroy", {});
+     },
+     on_session_valid: function() {
+     },
+     /**
+      * Called when a rpc call fail due to an invalid session.
+      * By default, it's a noop
+      */
+     on_session_invalid: function(retry_callback) {
+     },
+     /**
+      * Fetches a cookie stored by an openerp session
+      *
+      * @private
+      * @param name the cookie's name
+      */
+     get_cookie: function (name) {
+         if (!this.name) { return null; }
+         var nameEQ = this.name + '|' + name + '=';
+         var cookies = document.cookie.split(';');
+         for(var i=0; i<cookies.length; ++i) {
+             var cookie = cookies[i].replace(/^\s*/, '');
+             if(cookie.indexOf(nameEQ) === 0) {
+                 return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
+             }
+         }
+         return null;
+     },
+     /**
+      * Create a new cookie with the provided name and value
+      *
+      * @private
+      * @param name the cookie's name
+      * @param value the cookie's value
+      * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
+      */
+     set_cookie: function (name, value, ttl) {
+         if (!this.name) { return; }
+         ttl = ttl || 24*60*60*365;
+         document.cookie = [
+             this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
+             'path=/',
+             'max-age=' + ttl,
+             'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
+         ].join(';');
+     },
+     /**
+      * Load additional web addons of that instance and init them
+      *
+      * @param {Boolean} [no_session_valid_signal=false] prevents load_module from triggering ``on_session_valid``.
+      */
+     load_modules: function(no_session_valid_signal) {
+         var self = this;
+         return this.rpc('/web/session/modules', {}).pipe(function(result) {
+             var lang = self.user_context.lang,
+                 all_modules = _.uniq(self.module_list.concat(result));
+             var params = { mods: all_modules, lang: lang};
+             var to_load = _.difference(result, self.module_list).join(',');
+             self.module_list = all_modules;
+             var loaded = $.Deferred().resolve().promise();
+             if (to_load.length) {
+                 loaded = $.when(
+                     self.rpc('/web/webclient/csslist', {mods: to_load}, self.do_load_css),
+                     self.rpc('/web/webclient/qweblist', {mods: to_load}).pipe(self.do_load_qweb),
+                     self.rpc('/web/webclient/translations', params).pipe(function(trans) {
+                         instance.web._t.database.set_bundle(trans);
+                         var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"];
+                         return self.rpc('/web/webclient/jslist', {mods: to_load}).pipe(function(files) {
+                             return self.do_load_js(file_list.concat(files));
+                         }).then(function () {
+                             if (!Date.CultureInfo.pmDesignator) {
+                                 // If no am/pm designator is specified but the openerp
+                                 // datetime format uses %i, date.js won't be able to
+                                 // correctly format a date. See bug#938497.
+                                 Date.CultureInfo.amDesignator = 'AM';
+                                 Date.CultureInfo.pmDesignator = 'PM';
+                             }
+                         });
+                     }))
+             }
+             return loaded.then(function() {
+                 self.on_modules_loaded();
+                 if (!no_session_valid_signal) {
+                     self.on_session_valid();
+                 }
+             });
+         });
+     },
+     do_load_css: function (files) {
+         var self = this;
+         _.each(files, function (file) {
+             $('head').append($('<link>', {
+                 'href': self.get_url(file),
+                 'rel': 'stylesheet',
+                 'type': 'text/css'
+             }));
+         });
+     },
+     do_load_js: function(files) {
+         var self = this;
+         var d = $.Deferred();
+         if(files.length != 0) {
+             var file = files.shift();
+             var tag = document.createElement('script');
+             tag.type = 'text/javascript';
+             tag.src = self.get_url(file);
+             tag.onload = tag.onreadystatechange = function() {
+                 if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
+                     return;
+                 tag.onload_done = true;
+                 self.do_load_js(files).then(function () {
+                     d.resolve();
+                 });
+             };
+             var head = document.head || document.getElementsByTagName('head')[0];
+             head.appendChild(tag);
+         } else {
+             d.resolve();
+         }
+         return d;
+     },
+     do_load_qweb: function(files) {
+         var self = this;
+         _.each(files, function(file) {
+             self.qweb_mutex.exec(function() {
+                 return self.rpc('/web/proxy/load', {path: file}).pipe(function(xml) {
+                     if (!xml) { return; }
+                     instance.web.qweb.add_template(_.str.trim(xml));
+                 });
+             });
+         });
+         return self.qweb_mutex.def;
+     },
+     on_modules_loaded: function() {
+         for(var j=0; j<this.module_list.length; j++) {
+             var mod = this.module_list[j];
+             if(this.module_loaded[mod])
+                 continue;
+             instance[mod] = {};
+             // init module mod
+             if(instance._openerp[mod] != undefined) {
+                 instance._openerp[mod](instance);
+                 this.module_loaded[mod] = true;
+             }
+         }
+     },
+     get_url: function (file) {
+         return this.prefix + file;
+     },
+     /**
+      * Cooperative file download implementation, for ajaxy APIs.
+      *
+      * Requires that the server side implements an httprequest correctly
+      * setting the `fileToken` cookie to the value provided as the `token`
+      * parameter. The cookie *must* be set on the `/` path and *must not* be
+      * `httpOnly`.
+      *
+      * It would probably also be a good idea for the response to use a
+      * `Content-Disposition: attachment` header, especially if the MIME is a
+      * "known" type (e.g. text/plain, or for some browsers application/json
+      *
+      * @param {Object} options
+      * @param {String} [options.url] used to dynamically create a form
+      * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data
+      * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
+      * @param {Function} [options.success] callback in case of download success
+      * @param {Function} [options.error] callback in case of request error, provided with the error body
+      * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
+      */
+     get_file: function (options) {
+         // need to detect when the file is done downloading (not used
+         // yet, but we'll need it to fix the UI e.g. with a throbber
+         // while dump is being generated), iframe load event only fires
+         // when the iframe content loads, so we need to go smarter:
+         // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
+         var timer, token = new Date().getTime(),
+             cookie_name = 'fileToken', cookie_length = cookie_name.length,
+             CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
+             remove_form = false;
+         var $form, $form_data = $('<div>');
+         var complete = function () {
+             if (options.complete) { options.complete(); }
+             clearTimeout(timer);
+             $form_data.remove();
+             $target.remove();
+             if (remove_form && $form) { $form.remove(); }
+         };
+         var $target = $('<iframe style="display: none;">')
+             .attr({id: id, name: id})
+             .appendTo(document.body)
+             .load(function () {
+                 try {
+                     if (options.error) {
+                         options.error(JSON.parse(
+                             this.contentDocument.body.childNodes[1].textContent
+                         ));
+                     }
+                 } finally {
+                     complete();
+                 }
+             });
+         if (options.form) {
+             $form = $(options.form);
+         } else {
+             remove_form = true;
+             $form = $('<form>', {
+                 action: options.url,
+                 method: 'POST'
+             }).appendTo(document.body);
+         }
+         _(_.extend({}, options.data || {},
+                    {session_id: this.session_id, token: token}))
+             .each(function (value, key) {
+                 var $input = $form.find('[name=' + key +']');
+                 if (!$input.length) {
+                     $input = $('<input type="hidden" name="' + key + '">')
+                         .appendTo($form_data);
+                 }
+                 $input.val(value)
+             });
+         $form
+             .append($form_data)
+             .attr('target', id)
+             .get(0).submit();
+         var waitLoop = function () {
+             var cookies = document.cookie.split(';');
+             // setup next check
+             timer = setTimeout(waitLoop, CHECK_INTERVAL);
+             for (var i=0; i<cookies.length; ++i) {
+                 var cookie = cookies[i].replace(/^\s*/, '');
+                 if (!cookie.indexOf(cookie_name === 0)) { continue; }
+                 var cookie_val = cookie.substring(cookie_length + 1);
+                 if (parseInt(cookie_val, 10) !== token) { continue; }
+                 // clear cookie
+                 document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
+                     cookie_name, new Date().toGMTString());
+                 if (options.success) { options.success(); }
+                 complete();
+                 return;
+             }
+         };
+         timer = setTimeout(waitLoop, CHECK_INTERVAL);
+     },
+     synchronized_mode: function(to_execute) {
+       var synch = this.synch;
+       this.synch = true;
+       try {
+               return to_execute();
+       } finally {
+               this.synch = synch;
+       }
+     }
+ });
+ instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance.web.TranslationDataBase# */{
+     /**
+      * @constructs instance.web.TranslationDataBase
+      * @extends instance.web.Class
+      */
+     init: function() {
+         this.db = {};
+         this.parameters = {"direction": 'ltr',
+                         "date_format": '%m/%d/%Y',
+                         "time_format": '%H:%M:%S',
+                         "grouping": [],
+                         "decimal_point": ".",
+                         "thousands_sep": ","};
+     },
+     set_bundle: function(translation_bundle) {
+         var self = this;
+         this.db = {};
+         var modules = _.keys(translation_bundle.modules);
+         modules.sort();
+         if (_.include(modules, "web")) {
+             modules = ["web"].concat(_.without(modules, "web"));
+         }
+         _.each(modules, function(name) {
+             self.add_module_translation(translation_bundle.modules[name]);
+         });
+         if (translation_bundle.lang_parameters) {
+             this.parameters = translation_bundle.lang_parameters;
+             this.parameters.grouping = py.eval(
+                     this.parameters.grouping);
+         }
+     },
+     add_module_translation: function(mod) {
+         var self = this;
+         _.each(mod.messages, function(message) {
+             self.db[message.id] = message.string;
+         });
+     },
+     build_translation_function: function() {
+         var self = this;
+         var fcnt = function(str) {
+             var tmp = self.get(str);
+             return tmp === undefined ? str : tmp;
+         };
+         fcnt.database = this;
+         return fcnt;
+     },
+     get: function(key) {
+         if (this.db[key])
+             return this.db[key];
+         return undefined;
+     }
+ });
+ /**
+  * @deprecated use :class:`instance.web.Widget`
+  */
+ instance.web.OldWidget = instance.web.Widget.extend({
+     init: function(parent, element_id) {
+         this._super(parent);
+         this.element_id = element_id;
+         this.element_id = this.element_id || _.uniqueId('widget-');
+         var tmp = document.getElementById(this.element_id);
+         this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName));
+     },
+     renderElement: function() {
+         var rendered = this.render();
+         if (rendered) {
+             var elem = $(rendered);
+             this.$element.replaceWith(elem);
+             this.$element = elem;
+         }
+         return this;
+     },
+     render: function (additional) {
+         if (this.template)
+             return instance.web.qweb.render(this.template, _.extend({widget: this}, additional || {}));
+         return null;
+     }
+ });
+ }
+ // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
@@@ -18,496 -18,205 +18,496 @@@ instance.web.serialize_sort = function 
          }).join(', ');
  };
  
- openerp.web.Query = openerp.web.Class.extend({
 -instance.web.DataGroup =  instance.web.OldWidget.extend( /** @lends instance.web.DataGroup# */{
++instance.web.Query = instance.web.Class.extend({
 +    init: function (model, fields) {
 +        this._model = model;
 +        this._fields = fields;
 +        this._filter = [];
 +        this._context = {};
 +        this._limit = false;
 +        this._offset = 0;
 +        this._order_by = [];
 +    },
 +    clone: function (to_set) {
 +        to_set = to_set || {};
-         var q = new openerp.web.Query(this._model, this._fields);
++        var q = new instance.web.Query(this._model, this._fields);
 +        q._context = this._context;
 +        q._filter = this._filter;
 +        q._limit = this._limit;
 +        q._offset = this._offset;
 +        q._order_by = this._order_by;
 +
 +        for(var key in to_set) {
 +            if (!to_set.hasOwnProperty(key)) { continue; }
 +            switch(key) {
 +            case 'filter':
-                 q._filter = new openerp.web.CompoundDomain(
++                q._filter = new instance.web.CompoundDomain(
 +                        q._filter, to_set.filter);
 +                break;
 +            case 'context':
-                 q._context = new openerp.web.CompoundContext(
++                q._context = new instance.web.CompoundContext(
 +                        q._context, to_set.context);
 +                break;
 +            case 'limit':
 +            case 'offset':
 +            case 'order_by':
 +                q['_' + key] = to_set[key];
 +            }
 +        }
 +        return q;
 +    },
 +    _execute: function () {
 +        var self = this;
-         return openerp.connection.rpc('/web/dataset/search_read', {
++        return instance.connection.rpc('/web/dataset/search_read', {
 +            model: this._model.name,
 +            fields: this._fields || false,
 +            domain: this._model.domain(this._filter),
 +            context: this._model.context(this._context),
 +            offset: this._offset,
 +            limit: this._limit,
-             sort: openerp.web.serialize_sort(this._order_by)
++            sort: instance.web.serialize_sort(this._order_by)
 +        }).pipe(function (results) {
 +            self._count = results.length;
 +            return results.records;
 +        }, null);
 +    },
      /**
 -     * Management interface between views and grouped collections of OpenERP
 -     * records.
 +     * Fetches the first record matching the query, or null
       *
 -     * The root DataGroup is instantiated with the relevant information
 -     * (a session, a model, a domain, a context and a group_by sequence), the
 -     * domain and context may be empty. It is then interacted with via
 -     * :js:func:`~instance.web.DataGroup.list`, which is used to read the
 -     * content of the current grouping level.
 +     * @returns {jQuery.Deferred<Object|null>}
 +     */
 +    first: function () {
 +        var self = this;
 +        return this.clone({limit: 1})._execute().pipe(function (records) {
 +            delete self._count;
 +            if (records.length) { return records[0]; }
 +            return null;
 +        });
 +    },
 +    /**
 +     * Fetches all records matching the query
       *
 -     * @constructs instance.web.DataGroup
 -     * @extends instance.web.OldWidget
 +     * @returns {jQuery.Deferred<Array<>>}
 +     */
 +    all: function () {
 +        return this._execute();
 +    },
 +    /**
 +     * Fetches the number of records matching the query in the database
       *
 -     * @param {instance.web.OldWidget} parent widget
 -     * @param {String} model name of the model managed by this DataGroup
 -     * @param {Array} domain search domain for this DataGroup
 -     * @param {Object} context context of the DataGroup's searches
 -     * @param {Array} group_by sequence of fields by which to group
 -     * @param {Number} [level=0] nesting level of the group
 +     * @returns {jQuery.Deferred<Number>}
       */
 -    init: function(parent, model, domain, context, group_by, level) {
 -        this._super(parent, null);
 -        if (group_by) {
 -            if (group_by.length || context['group_by_no_leaf']) {
 -                return new instance.web.ContainerDataGroup( this, model, domain, context, group_by, level);
 -            } else {
 -                return new instance.web.GrouplessDataGroup( this, model, domain, context, level);
 -            }
 +    count: function () {
 +        if (this._count != undefined) { return $.when(this._count); }
 +        return this._model.call(
 +            'search_count', [this._filter], {
 +                context: this._model.context(this._context)});
 +    },
 +    /**
 +     * Performs a groups read according to the provided grouping criterion
 +     *
 +     * @param {String|Array<String>} grouping
 +     * @returns {jQuery.Deferred<Array<openerp.web.data.Group>> | null}
 +     */
 +    group_by: function (grouping) {
 +        if (grouping === undefined) {
 +            return null;
          }
  
 -        this.model = model;
 -        this.context = context;
 -        this.domain = domain;
 +        if (!(grouping instanceof Array)) {
 +            grouping = _.toArray(arguments);
 +        }
 +        if (_.isEmpty(grouping)) { return null; }
  
 -        this.level = level || 0;
 +        var self = this;
 +        return this._model.call('read_group', {
 +            groupby: grouping,
 +            fields: _.uniq(grouping.concat(this._fields || [])),
 +            domain: this._model.domain(this._filter),
 +            context: this._model.context(this._context),
 +            offset: this._offset,
 +            limit: this._limit,
-             orderby: openerp.web.serialize_sort(this._order_by) || false
++            orderby: instance.web.serialize_sort(this._order_by) || false
 +        }).pipe(function (results) {
 +            return _(results).map(function (result) {
-                 return new openerp.web.data.Group(
++                return new instance.web.data.Group(
 +                    self._model.name, grouping[0], result);
 +            });
 +        });
      },
 -    cls: 'DataGroup'
 -});
 -instance.web.ContainerDataGroup = instance.web.DataGroup.extend( /** @lends instance.web.ContainerDataGroup# */ {
      /**
 +     * Creates a new query with the union of the current query's context and
 +     * the new context.
       *
 -     * @constructs instance.web.ContainerDataGroup
 -     * @extends instance.web.DataGroup
 +     * @param context context data to add to the query
 +     * @returns {openerp.web.Query}
 +     */
 +    context: function (context) {
 +        if (!context) { return this; }
 +        return this.clone({context: context});
 +    },
 +    /**
 +     * Creates a new query with the union of the current query's filter and
 +     * the new domain.
 +     *
 +     * @param domain domain data to AND with the current query filter
 +     * @returns {openerp.web.Query}
 +     */
 +    filter: function (domain) {
 +        if (!domain) { return this; }
 +        return this.clone({filter: domain});
 +    },
 +    /**
 +     * Creates a new query with the provided limit replacing the current
 +     * query's own limit
 +     *
 +     * @param {Number} limit maximum number of records the query should retrieve
 +     * @returns {openerp.web.Query}
 +     */
 +    limit: function (limit) {
 +        return this.clone({limit: limit});
 +    },
 +    /**
 +     * Creates a new query with the provided offset replacing the current
 +     * query's own offset
 +     *
 +     * @param {Number} offset number of records the query should skip before starting its retrieval
 +     * @returns {openerp.web.Query}
 +     */
 +    offset: function (offset) {
 +        return this.clone({offset: offset});
 +    },
 +    /**
 +     * Creates a new query with the provided ordering parameters replacing
 +     * those of the current query
       *
 -     * @param session
 -     * @param model
 -     * @param domain
 -     * @param context
 -     * @param group_by
 -     * @param level
 +     * @param {String...} fields ordering clauses
 +     * @returns {openerp.web.Query}
       */
 -    init: function (parent, model, domain, context, group_by, level) {
 -        this._super(parent, model, domain, context, null, level);
 +    order_by: function (fields) {
 +        if (fields === undefined) { return this; }
 +        if (!(fields instanceof Array)) {
 +            fields = _.toArray(arguments);
 +        }
 +        if (_.isEmpty(fields)) { return this; }
 +        return this.clone({order_by: fields});
 +    }
 +});
  
- openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{
 -        this.group_by = group_by;
++instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{
 +    /**
-      * @constructs openerp.web.Model
-      * @extends openerp.web.Class
++     * @constructs instance.web.Model
++     * @extends instance.web.Class
 +     *
 +     * @param {String} model_name name of the OpenERP model this object is bound to
 +     * @param {Object} [context]
 +     * @param {Array} [domain]
 +     */
 +    init: function (model_name, context, domain) {
 +        this.name = model_name;
 +        this._context = context || {};
 +        this._domain = domain || [];
 +    },
 +    /**
 +     * @deprecated does not allow to specify kwargs, directly use call() instead
 +     */
 +    get_func: function (method_name) {
 +        var self = this;
 +        return function () {
 +            return self.call(method_name, _.toArray(arguments));
 +        };
      },
      /**
 -     * The format returned by ``read_group`` is absolutely dreadful:
 +     * Call a method (over RPC) on the bound OpenERP model.
       *
 -     * * A ``__context`` key provides future grouping levels
 -     * * A ``__domain`` key provides the domain for the next search
 -     * * The current grouping value is provided through the name of the
 -     *   current grouping name e.g. if currently grouping on ``user_id``, then
 -     *   the ``user_id`` value for this group will be provided through the
 -     *   ``user_id`` key.
 -     * * Similarly, the number of items in the group (not necessarily direct)
 -     *   is provided via ``${current_field}_count``
 -     * * Other aggregate fields are just dumped there
 +     * @param {String} method name of the method to call
 +     * @param {Array} [args] positional arguments
 +     * @param {Object} [kwargs] keyword arguments
 +     * @returns {jQuery.Deferred<>} call result
 +     */
 +    call: function (method, args, kwargs) {
 +        args = args || [];
 +        kwargs = kwargs || {};
 +        if (!_.isArray(args)) {
 +            // call(method, kwargs)
 +            kwargs = args;
 +            args = [];
 +        }
-         return openerp.connection.rpc('/web/dataset/call_kw', {
++        return instance.connection.rpc('/web/dataset/call_kw', {
 +            model: this.name,
 +            method: method,
 +            args: args,
 +            kwargs: kwargs
 +        });
 +    },
 +    /**
 +     * Fetches a Query instance bound to this model, for searching
       *
 -     * This function slightly improves the grouping records by:
 +     * @param {Array<String>} [fields] fields to ultimately fetch during the search
 +     * @returns {openerp.web.Query}
 +     */
 +    query: function (fields) {
-         return new openerp.web.Query(this, fields);
++        return new instance.web.Query(this, fields);
 +    },
 +    /**
 +     * Executes a signal on the designated workflow, on the bound OpenERP model
       *
 -     * * Adding a ``grouped_on`` property providing the current grouping field
 -     * * Adding a ``value`` and a ``length`` properties which replace the
 -     *   ``$current_field`` and ``${current_field}_count`` ones
 -     * * Moving aggregate values into an ``aggregates`` property object
 +     * @param {Number} id workflow identifier
 +     * @param {String} signal signal to trigger on the workflow
 +     */
 +    exec_workflow: function (id, signal) {
-         return openerp.connection.rpc('/web/dataset/exec_workflow', {
++        return instance.connection.rpc('/web/dataset/exec_workflow', {
 +            model: this.name,
 +            id: id,
 +            signal: signal
 +        });
 +    },
 +    /**
 +     * Fetches the model's domain, combined with the provided domain if any
       *
 -     * Context and domain keys remain as-is, they should not be used externally
 -     * but in case they're needed...
 +     * @param {Array} [domain] to combine with the model's internal domain
 +     * @returns The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
 +     */
 +    domain: function (domain) {
 +        if (!domain) { return this._domain; }
-         return new openerp.web.CompoundDomain(
++        return new instance.web.CompoundDomain(
 +            this._domain, domain);
 +    },
 +    /**
 +     * Fetches the combination of the user's context and the domain context,
 +     * combined with the provided context if any
       *
 -     * @param {Object} group ``read_group`` record
 +     * @param {Object} [context] to combine with the model's internal context
 +     * @returns The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
       */
 -    transform_group: function (group) {
 -        var field_name = this.group_by[0];
 -        // In cases where group_by_no_leaf and no group_by, the result of
 -        // read_group has aggregate fields but no __context or __domain.
 -        // Create default (empty) values for those so that things don't break
 -        var fixed_group = _.extend(
 -                {__context: {group_by: []}, __domain: []},
 -                group);
 -
 -        var aggregates = {};
 -        _(fixed_group).each(function (value, key) {
 -            if (key.indexOf('__') === 0
 -                    || key === field_name
 -                    || key === field_name + '_count') {
 -                return;
 -            }
 -            aggregates[key] = value || 0;
 +    context: function (context) {
-         return new openerp.web.CompoundContext(
-             openerp.connection.user_context, this._context, context || {});
++        return new instance.web.CompoundContext(
++            instance.connection.user_context, this._context, context || {});
 +    },
 +    /**
 +     * Button action caller, needs to perform cleanup if an action is returned
 +     * from the button (parsing of context and domain, and fixup of the views
 +     * collection for act_window actions)
 +     *
 +     * FIXME: remove when evaluator integrated
 +     */
 +    call_button: function (method, args) {
-         return openerp.connection.rpc('/web/dataset/call_button', {
++        return instance.connection.rpc('/web/dataset/call_button', {
 +            model: this.name,
 +            method: method,
 +            domain_id: null,
 +            context_id: args.length - 1,
 +            args: args || []
          });
 +    },
 +});
  
- openerp.web.Traverser = openerp.web.Class.extend(/** @lends openerp.web.Traverser# */{
 -        var group_size = fixed_group[field_name + '_count'] || fixed_group.__count || 0;
 -        var leaf_group = fixed_group.__context.group_by.length === 0;
 -        return {
 -            __context: fixed_group.__context,
 -            __domain: fixed_group.__domain,
 -
 -            grouped_on: field_name,
 -            // if terminal group (or no group) and group_by_no_leaf => use group.__count
 -            length: group_size,
 -            value: fixed_group[field_name],
 -            // A group is openable if it's not a leaf in group_by_no_leaf mode
 -            openable: !(leaf_group && this.context['group_by_no_leaf']),
++instance.web.Traverser = instance.web.Class.extend(/** @lends openerp.web.Traverser# */{
 +    /**
-      * @constructs openerp.web.Traverser
-      * @extends openerp.web.Class
++     * @constructs instance.web.Traverser
++     * @extends instance.web.Class
 +     *
-      * @param {openerp.web.Model} model instance this traverser is bound to
++     * @param {instance.web.Model} model instance this traverser is bound to
 +     */
 +    init: function (model) {
 +        this._model = model;
 +        this._index = 0;
 +    },
  
 -            aggregates: aggregates
 -        };
 +    /**
 +     * Gets and sets the current index
 +     *
 +     * @param {Number} [idx]
 +     * @returns {Number} current index
 +     */
 +    index: function (idx) {
 +        if (idx) { this._index = idx; }
 +        return this._index;
 +    },
 +    /**
 +     * Returns the model this traverser is currently bound to
 +     *
 +     * @returns {openerp.web.Model}
 +     */
 +    model: function () {
 +        return this._model;
 +    },
 +    /**
 +     * Fetches the size of the backing model's match
 +     *
 +     * @returns {Deferred<Number>} deferred count
 +     */
 +    size: function () {
 +        return this._model.query().count();
      },
 -    fetch: function (fields) {
 -        // internal method
 -        var d = new $.Deferred();
 -        var self = this;
  
 -        this.rpc('/web/group/read', {
 -            model: this.model,
 -            context: this.context,
 -            domain: this.domain,
 -            fields: _.uniq(this.group_by.concat(fields)),
 -            group_by_fields: this.group_by,
 -            sort: instance.web.serialize_sort(this.sort)
 -        }, function () { }).then(function (response) {
 -            var data_groups = _(response).map(
 -                    _.bind(self.transform_group, self));
 -            self.groups = data_groups;
 -            d.resolveWith(self, [data_groups]);
 -        }, function () {
 -            d.rejectWith.apply(d, [self, arguments]);
 -        });
 -        return d.promise();
 -    },
 -    /**
 -     * The items of a list have the following properties:
 -     *
 -     * ``length``
 -     *     the number of records contained in the group (and all of its
 -     *     sub-groups). This does *not* provide the size of the "next level"
 -     *     of the group, unless the group is terminal (no more groups within
 -     *     it).
 -     * ``grouped_on``
 -     *     the name of the field this level was grouped on, this is mostly
 -     *     used for display purposes, in order to know the name of the current
 -     *     level of grouping. The ``grouped_on`` should be the same for all
 -     *     objects of the list.
 -     * ``value``
 -     *     the value which led to this group (this is the value all contained
 -     *     records have for the current ``grouped_on`` field name).
 -     * ``aggregates``
 -     *     a mapping of other aggregation fields provided by ``read_group``
 -     *
 -     * @param {Array} fields the list of fields to aggregate in each group, can be empty
 -     * @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.
 -     * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter
 +    /**
 +     * Record at the current index for the collection, fails if there is no
 +     * record at the current index.
 +     *
 +     * @returns {Deferred<>}
       */
 -    list: function (fields, ifGroups, ifRecords) {
 +    current: function (fields) {
 +        return this._model.query(fields).first().pipe(function (record) {
 +            if (record == null) {
 +                return $.Deferred()
 +                    .reject('No record at index' + this._index)
 +                    .promise();
 +            }
 +            return record;
 +        });
 +    },
 +    next: function (fields) {
          var self = this;
 -        this.fetch(fields).then(function (group_records) {
 -            ifGroups(_(group_records).map(function (group) {
 -                var child_context = _.extend({}, self.context, group.__context);
 -                return _.extend(
 -                    new instance.web.DataGroup(
 -                        self, self.model, group.__domain,
 -                        child_context, child_context.group_by,
 -                        self.level + 1),
 -                    group, {sort: self.sort});
 -            }));
 +        this._index++;
 +        return this.size().pipe(function (s) {
 +            if (self._index >= s) {
 +                self._index = 0;
 +            }
 +            return self.current(fields);
          });
 +    },
 +    previous: function (fields) {
 +        var self = this;
 +        this._index--;
 +        if (this._index < 0) {
 +            return this.size().pipe(function (s) {
 +                self._index = s-1;
 +                return self.current(fields);
 +            });
 +        }
 +        return this.current(fields);
      }
 +
  });
 -instance.web.GrouplessDataGroup = instance.web.DataGroup.extend( /** @lends instance.web.GrouplessDataGroup# */ {
 +
 +/**
 + * Utility objects, should never need to be instantiated from outside of this
 + * module
 + *
 + * @namespace
 + */
- openerp.web.data = {
-     Group: openerp.web.Class.extend(/** @lends openerp.web.data.Group# */{
++instance.web.data = {
++    Group: instance.web.Class.extend(/** @lends openerp.web.data.Group# */{
 +        /**
-          * @constructs openerp.web.data.Group
-          * @extends openerp.web.Class
++         * @constructs instance.web.data.Group
++         * @extends instance.web.Class
 +         */
 +        init: function (model, grouping_field, read_group_group) {
 +            // In cases where group_by_no_leaf and no group_by, the result of
 +            // read_group has aggregate fields but no __context or __domain.
 +            // Create default (empty) values for those so that things don't break
 +            var fixed_group = _.extend(
 +                {__context: {group_by: []}, __domain: []},
 +                read_group_group);
 +
 +            var aggregates = {};
 +            _(fixed_group).each(function (value, key) {
 +                if (key.indexOf('__') === 0
 +                        || key === grouping_field
 +                        || key === grouping_field + '_count') {
 +                    return;
 +                }
 +                aggregates[key] = value || 0;
 +            });
 +
-             this.model = new openerp.web.Model(
++            this.model = new instance.web.Model(
 +                model, fixed_group.__context, fixed_group.__domain);
 +
 +            var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0;
 +            var leaf_group = fixed_group.__context.group_by.length === 0;
 +            this.attributes = {
 +                grouped_on: grouping_field,
 +                // if terminal group (or no group) and group_by_no_leaf => use group.__count
 +                length: group_size,
 +                value: fixed_group[grouping_field],
 +                // A group is open-able if it's not a leaf in group_by_no_leaf mode
 +                has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),
 +
 +                aggregates: aggregates
 +            };
 +        },
 +        get: function (key) {
 +            return this.attributes[key];
 +        },
 +        subgroups: function () {
 +            return this.model.query().group_by(this.model.context().group_by);
 +        },
 +        query: function () {
 +            return this.model.query.apply(this.model, arguments);
 +        }
 +    })
 +};
 +
- openerp.web.DataGroup =  openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{
++instance.web.DataGroup =  instance.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{
      /**
 +     * Management interface between views and grouped collections of OpenERP
 +     * records.
       *
 -     * @constructs instance.web.GrouplessDataGroup
 -     * @extends instance.web.DataGroup
 +     * The root DataGroup is instantiated with the relevant information
 +     * (a session, a model, a domain, a context and a group_by sequence), the
 +     * domain and context may be empty. It is then interacted with via
-      * :js:func:`~openerp.web.DataGroup.list`, which is used to read the
++     * :js:func:`~instance.web.DataGroup.list`, which is used to read the
 +     * content of the current grouping level.
 +     *
-      * @constructs openerp.web.DataGroup
-      * @extends openerp.web.OldWidget
++     * @constructs instance.web.DataGroup
++     * @extends instance.web.OldWidget
       *
-      * @param {openerp.web.OldWidget} parent widget
 -     * @param session
 -     * @param model
 -     * @param domain
 -     * @param context
 -     * @param level
++     * @param {instance.web.OldWidget} parent widget
 +     * @param {String} model name of the model managed by this DataGroup
 +     * @param {Array} domain search domain for this DataGroup
 +     * @param {Object} context context of the DataGroup's searches
 +     * @param {Array} group_by sequence of fields by which to group
 +     * @param {Number} [level=0] nesting level of the group
       */
 -    init: function (parent, model, domain, context, level) {
 -        this._super(parent, model, domain, context, null, level);
 +    init: function(parent, model, domain, context, group_by, level) {
 +        this._super(parent, null);
-         this.model = new openerp.web.Model(model, context, domain);
++        this.model = new instance.web.Model(model, context, domain);
 +        this.group_by = group_by;
 +        this.context = context;
 +        this.domain = domain;
 +
 +        this.level = level || 0;
      },
      list: function (fields, ifGroups, ifRecords) {
 -        ifRecords(_.extend(
 -            new instance.web.DataSetSearch(this, this.model),
 -            {domain: this.domain, context: this.context, _sort: this.sort}));
 +        var self = this;
 +        $.when(this.model.query(fields)
 +                    .order_by(this.sort)
 +                    .group_by(this.group_by)).then(function (groups) {
 +            if (!groups) {
 +                ifRecords(_.extend(
-                     new openerp.web.DataSetSearch(
++                    new instance.web.DataSetSearch(
 +                            self, self.model.name,
 +                            self.model.context(),
 +                            self.model.domain()),
 +                    {_sort: self.sort}));
 +                return;
 +            }
 +            ifGroups(_(groups).map(function (group) {
 +                var child_context = _.extend(
 +                    {}, self.model.context(), group.model.context());
 +                return _.extend(
-                     new openerp.web.DataGroup(
++                    new instance.web.DataGroup(
 +                        self, self.model.name, group.model.domain(),
 +                        child_context, group.model._context.group_by,
 +                        self.level + 1),
 +                    {
 +                        __context: child_context,
 +                        __domain: group.model.domain(),
 +                        grouped_on: group.get('grouped_on'),
 +                        length: group.get('length'),
 +                        value: group.get('value'),
 +                        openable: group.get('has_children'),
 +                        aggregates: group.get('aggregates')
 +                    }, {sort: self.sort});
 +            }));
 +        });
      }
  });
- openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend({ });
- openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend({ });
 -instance.web.StaticDataGroup = instance.web.GrouplessDataGroup.extend( /** @lends instance.web.StaticDataGroup# */ {
++instance.web.ContainerDataGroup = instance.web.DataGroup.extend({ });
++instance.web.GrouplessDataGroup = instance.web.DataGroup.extend({ });
 +
- openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ {
++instance.web.StaticDataGroup = instance.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ {
      /**
       * A specialization of groupless data groups, relying on a single static
       * dataset as its records provider.
      }
  });
  
- openerp.web.DataSet =  openerp.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{
 -instance.web.DataSet =  instance.web.OldWidget.extend( /** @lends instance.web.DataSet# */{
++instance.web.DataSet =  instance.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{
      /**
       * DateaManagement interface between views and the collection of selected
       * OpenERP records (represents the view's state?)
          this.context = context || {};
          this.index = null;
          this._sort = [];
-         this._model = new openerp.web.Model(model, context);
++        this._model = new instance.web.Model(model, context);
      },
      previous: function () {
          this.index -= 1;
@@@ -836,10 -560,10 +836,10 @@@ instance.web.DataSetStatic =  instance.
          this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
      }
  });
- openerp.web.DataSetSearch =  openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{
 -instance.web.DataSetSearch =  instance.web.DataSet.extend(/** @lends instance.web.DataSetSearch */{
++instance.web.DataSetSearch =  instance.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{
      /**
-      * @constructs openerp.web.DataSetSearch
-      * @extends openerp.web.DataSet
+      * @constructs instance.web.DataSetSearch
+      * @extends instance.web.DataSet
       *
       * @param {Object} parent
       * @param {String} model
      init: function(parent, model, context, domain) {
          this._super(parent, model, context);
          this.domain = domain || [];
 -        this.offset = 0;
 -        this._length;
 -        // subset records[offset:offset+limit]
 -        // is it necessary ?
 +        this._length = null;
          this.ids = [];
-         this._model = new openerp.web.Model(model, context, domain);
++        this._model = new instance.web.Model(model, context, domain);
      },
      /**
       * Read a slice of the records represented by this DataSet, based on its
@@@ -1119,7 -848,35 +1119,7 @@@ instance.web.ProxyDataSet = instance.we
      on_unlink: function(ids) {}
  });
  
- openerp.web.CompoundContext = openerp.web.Class.extend({
 -instance.web.Model = instance.web.CallbackEnabled.extend({
 -    init: function(model_name) {
 -        this._super();
 -        this.model_name = model_name;
 -    },
 -    rpc: function() {
 -        var c = instance.connection;
 -        return c.rpc.apply(c, arguments);
 -    },
 -    /*
 -     * deprecated because it does not allow to specify kwargs, directly use call() instead
 -     */
 -    get_func: function(method_name) {
 -        var self = this;
 -        return function() {
 -            return self.call(method_name, _.toArray(arguments), {});
 -        };
 -    },
 -    call: function (method, args, kwargs) {
 -        return this.rpc('/web/dataset/call_kw', {
 -            model: this.model_name,
 -            method: method,
 -            args: args,
 -            kwargs: kwargs
 -        });
 -    }
 -});
 -
+ instance.web.CompoundContext = instance.web.Class.extend({
      init: function () {
          this.__ref = "compound_context";
          this.__contexts = [];
@@@ -1164,46 -921,6 +1164,46 @@@ instance.web.CompoundDomain = instance.
          return this.__eval_context;
      }
  });
 +
- openerp.web.DropMisordered = openerp.web.Class.extend(/** @lends openerp.web.DropMisordered# */{
++instance.web.DropMisordered = instance.web.Class.extend(/** @lends openerp.web.DropMisordered# */{
 +    /**
-      * @constructs openerp.web.DropMisordered
-      * @extends openerp.web.Class
++     * @constructs instance.web.DropMisordered
++     * @extends instance.web.Class
 +     *
 +     * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored
 +     */
 +    init: function (failMisordered) {
 +        // local sequence number, for requests sent
 +        this.lsn = 0;
 +        // remote sequence number, seqnum of last received request
 +        this.rsn = -1;
 +        this.failMisordered = failMisordered || false;
 +    },
 +    /**
 +     * Adds a deferred (usually an async request) to the sequencer
 +     *
 +     * @param {$.Deferred} deferred to ensure add
 +     * @returns {$.Deferred}
 +     */
 +    add: function (deferred) {
 +        var res = $.Deferred();
 +
 +        var self = this, seq = this.lsn++;
 +        deferred.then(function () {
 +            if (seq > self.rsn) {
 +                self.rsn = seq;
 +                res.resolve.apply(res, arguments);
 +            } else if (self.failMisordered) {
 +                res.reject();
 +            }
 +        }, function () {
 +            res.reject.apply(res, arguments);
 +        });
 +
 +        return res.promise();
 +    }
 +});
 +
  };
  
  // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
@@@ -1956,16 -2281,16 +2281,17 @@@ instance.web.form.dialog = function(con
      return dialog.$element;
  };
  
- openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
-     form_template: 'FieldMany2One',
-     init: function(view, node) {
-         this._super(view, node);
+ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
+     template: "FieldMany2One",
+     init: function(field_manager, node) {
+         this._super(field_manager, node);
          this.limit = 7;
-         this.value = null;
-         this.cm_id = _.uniqueId('m2o_cm_');
+         this.set({'value': false});
+         this.display_value = {};
          this.last_search = [];
-         this.tmp_value = undefined;
+         this.floating = false;
+         this.inhibit_on_change = false;
 +        this.orderer = new openerp.web.DropMisordered();
      },
      start: function() {
          this._super();
          var search_val = request.term;
          var self = this;
  
-         var dataset = new openerp.web.DataSet(this, this.field.relation, self.build_context());
 -        if (this.abort_last) {
 -            this.abort_last();
 -            delete this.abort_last;
 -        }
 -        var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
++        var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
  
 -        dataset.name_search(search_val, self.build_domain(), 'ilike',
 -                this.limit + 1, function(data) {
 +        this.orderer.add(dataset.name_search(
 +                search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) {
              self.last_search = data;
              // possible selections for the m2o
              var values = _.map(data, function(x) {
              self._search_create_popup("form", undefined, {"default_name": name});
          };
          if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) {
-             new openerp.web.DataSet(this, this.field.relation, self.build_context())
 -            var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
 -            dataset.name_create(name, function(data) {
 -                self.display_value = {};
 -                self.display_value["" + data[0]] = data[1];
 -                self.set({value: data[0]});
 -            }).fail(function(error, event) {
 -                event.preventDefault();
 -                slow_create();
 -            });
++            new instance.web.DataSet(this, this.field.relation, self.build_context())
 +                .name_create(name, function(data) {
-                     self._change_int_ext_value(data);
++                    self.display_value = {};
++                    self.display_value["" + data[0]] = data[1];
++                    self.set({value: data[0]});
 +                }).fail(function(error, event) {
 +                    event.preventDefault();
 +                    slow_create();
 +                });
          } else
              slow_create();
      },
@@@ -2,8 -2,8 +2,8 @@@ $(document).ready(function () 
      var openerp;
      module('web-class', {
          setup: function () {
--            openerp = window.openerp.init();
--            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
          }
      });
      test('Basic class creation', function () {
index 0000000,9c1dcaf..290a009
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,135 +1,136 @@@
+ $(document).ready(function () {
+     var openerp;
+     module("eval.contexts", {
+         setup: function () {
 -            openerp = window.openerp.init();
 -            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
+         }
+     });
+     test('context_sequences', function () {
+         // Context n should have base evaluation context + all of contexts
+         // 0..n-1 in its own evaluation context
+         var active_id = 4;
+         var result = openerp.connection.test_eval_contexts([
+             {
+                 "__contexts": [
+                     {
+                         "department_id": false,
+                         "lang": "en_US",
+                         "project_id": false,
+                         "section_id": false,
+                         "tz": false,
+                         "uid": 1
+                     },
+                     { "search_default_create_uid": 1 },
+                     {}
+                 ],
+                 "__eval_context": null,
+                 "__ref": "compound_context"
+             },
+             {
+                 "active_id": active_id,
+                 "active_ids": [ active_id ],
+                 "active_model": "purchase.requisition"
+             },
+             {
+                 "__debug": "{'record_id' : active_id}",
+                 "__id": "63e8e9bff8a6",
+                 "__ref": "context"
+             }
+         ]);
+         deepEqual(result, {
+             department_id: false,
+             lang: 'en_US',
+             project_id: false,
+             section_id: false,
+             tz: false,
+             uid: 1,
+             search_default_create_uid: 1,
+             active_id: active_id,
+             active_ids: [active_id],
+             active_model: 'purchase.requisition',
+             record_id: active_id
+         });
+     });
+     test('non-literal_eval_contexts', function () {
+         var result = openerp.connection.test_eval_contexts([{
+             "__ref": "compound_context",
+             "__contexts": [
+                 {"__ref": "context", "__debug": "{'type':parent.type}",
+                  "__id": "462b9dbed42f"}
+             ],
+             "__eval_context": {
+                 "__ref": "compound_context",
+                 "__contexts": [{
+                         "__ref": "compound_context",
+                         "__contexts": [
+                             {"__ref": "context", "__debug": "{'type': type}",
+                              "__id": "16a04ed5a194"}
+                         ],
+                         "__eval_context": {
+                             "__ref": "compound_context",
+                             "__contexts": [
+                                 {"lang": "en_US", "tz": false, "uid": 1,
+                                  "journal_type": "sale", "section_id": false,
+                                  "default_type": "out_invoice",
+                                  "type": "out_invoice", "department_id": false},
+                                 {"id": false, "journal_id": 10,
+                                  "number": false, "type": "out_invoice",
+                                  "currency_id": 1, "partner_id": 4,
+                                  "fiscal_position": false,
+                                  "date_invoice": false, "period_id": false,
+                                  "payment_term": false, "reference_type": "none",
+                                  "reference": false, "account_id": 440,
+                                  "name": false, "invoice_line": [],
+                                  "tax_line": [], "amount_untaxed": 0,
+                                  "amount_tax": 0, "reconciled": false,
+                                  "amount_total": 0, "state": "draft",
+                                  "residual": 0, "company_id": 1,
+                                  "date_due": false, "user_id": 1,
+                                  "partner_bank_id": false, "origin": false,
+                                  "move_id": false, "comment": false,
+                                  "payment_ids": [[6, false, []]],
+                                  "active_id": false, "active_ids": [],
+                                  "active_model": "account.invoice",
+                                  "parent": {}}
+                     ], "__eval_context": null}
+                 }, {
+                     "id": false,
+                     "product_id": 4,
+                     "name": "[PC1] Basic PC",
+                     "quantity": 1,
+                     "uos_id": 1,
+                     "price_unit": 100,
+                     "account_id": 853,
+                     "discount": 0,
+                     "account_analytic_id": false,
+                     "company_id": false,
+                     "note": false,
+                     "invoice_line_tax_id": [[6, false, [1]]],
+                     "active_id": false,
+                     "active_ids": [],
+                     "active_model": "account.invoice.line",
+                     "parent": {
+                         "id": false, "journal_id": 10, "number": false,
+                         "type": "out_invoice", "currency_id": 1,
+                         "partner_id": 4, "fiscal_position": false,
+                         "date_invoice": false, "period_id": false,
+                         "payment_term": false, "reference_type": "none",
+                         "reference": false, "account_id": 440, "name": false,
+                         "tax_line": [], "amount_untaxed": 0, "amount_tax": 0,
+                         "reconciled": false, "amount_total": 0,
+                         "state": "draft", "residual": 0, "company_id": 1,
+                         "date_due": false, "user_id": 1,
+                         "partner_bank_id": false, "origin": false,
+                         "move_id": false, "comment": false,
+                         "payment_ids": [[6, false, []]]}
+                 }],
+                 "__eval_context": null
+             }
+         }]);
+         deepEqual(result, {type: 'out_invoice'});
+     });
+ });
@@@ -2,8 -2,8 +2,9 @@@ $(document).ready(function () 
      var openerp;
      module("form.widget", {
          setup: function () {
--            openerp = window.openerp.init(true);
--            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
              window.openerp.web.chrome(openerp);
              // views loader stuff
              window.openerp.web.data(openerp);
@@@ -3,8 -3,8 +3,9 @@@ $(document).ready(function () 
  
      module('server-formats', {
          setup: function () {
--            openerp = window.openerp.init();
--            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
              window.openerp.web.dates(openerp);
          }
      });
@@@ -40,8 -40,8 +41,9 @@@
  
      module('web-formats', {
          setup: function () {
--            openerp = window.openerp.init();
--            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
              window.openerp.web.dates(openerp);
              window.openerp.web.formats(openerp);
          }
      });
      module('custom-date-formats', {
          setup: function () {
--            openerp = window.openerp.init();
--            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
              window.openerp.web.dates(openerp);
              window.openerp.web.formats(openerp);
          }
index f27adfa,0000000..a031325
mode 100644,000000..100644
--- /dev/null
@@@ -1,49 -1,0 +1,50 @@@
 +<!DOCTYPE html>
 +<html style="height: 100%">
 +<head>
 +    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
 +    <title>OpenERP</title>
 +    <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
 +
 +    <link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
 +    <script src="/web/static/lib/qunit/qunit.js" type="text/javascript"></script>
 +
 +    <script src="/web/static/lib/underscore/underscore.js" type="text/javascript"></script>
 +    <script src="/web/static/lib/underscore/underscore.string.js" type="text/javascript"></script>
 +
 +    <!-- jquery -->
 +    <script src="/web/static/lib/jquery/jquery-1.6.4.js"></script>
 +    <script src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js"></script>
 +    <script src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
 +    
 +    <script src="/web/static/lib/datejs/globalization/en-US.js"></script>
 +    <script src="/web/static/lib/datejs/core.js"></script>
 +    <script src="/web/static/lib/datejs/parser.js"></script>
 +    <script src="/web/static/lib/datejs/sugarpak.js"></script>
 +    <script src="/web/static/lib/datejs/extras.js"></script>
 +
 +    <script src="/web/static/lib/qweb/qweb2.js"></script>
 +
-     <script src="/web/static/lib/py.parse/lib/py.js"></script>
++    <script src="/web/static/lib/py.js/lib/py.js"></script>
 +
 +    <script src="/web/static/src/js/boot.js"></script>
-     <script src="/web/static/src/js/core.js"></script>
++    <script src="/web/static/src/js/corelib.js"></script>
++    <script src="/web/static/src/js/coresetup.js"></script>
 +    <script src="/web/static/src/js/dates.js"></script>
 +    <script src="/web/static/src/js/formats.js"></script>
 +    <script src="/web/static/src/js/chrome.js"></script>
 +    <script src="/web/static/src/js/data.js"></script>
 +
 +    <script src="/web/static/src/js/test_support.js"></script>
 +</head>
 +    <body id="oe" class="openerp">
 +        <h1 id="qunit-header">
 +            OpenERP Web Test Suite: javascript to XML-RPC (excluded)
 +        </h1>
 +        <h2 id="qunit-banner"></h2>
 +        <div id="qunit-testrunner-toolbar"></div>
 +        <h2 id="qunit-userAgent"></h2>
 +        <ol id="qunit-tests"></ol>
 +        <div id="qunit-fixture"></div>
 +        <script type="text/javascript" src="/web/static/test/fulltest/dataset.js"></script>
 +    </body>
 +</html>
@@@ -10,7 -10,7 +10,13 @@@ $(document).ready(function () 
          };
      module('list-events', {
          setup: function () {
--            openerp = window.openerp.init();
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
++            window.openerp.web.chrome(openerp);
++            // views loader stuff
++            window.openerp.web.data(openerp);
++            window.openerp.web.views(openerp);
              window.openerp.web.list(openerp);
          }
      });
  
      module('list-records', {
          setup: function () {
--            openerp = window.openerp.init();
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
++            window.openerp.web.chrome(openerp);
++            // views loader stuff
++            window.openerp.web.data(openerp);
++            window.openerp.web.views(openerp);
              window.openerp.web.list(openerp);
          }
      });
  
      module('list-collections-degenerate', {
          setup: function () {
--            openerp = window.openerp.init();
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
++            window.openerp.web.chrome(openerp);
++            // views loader stuff
++            window.openerp.web.data(openerp);
++            window.openerp.web.views(openerp);
              window.openerp.web.list(openerp);
          }
      });
  
      module('list-hofs', {
          setup: function () {
--            openerp = window.openerp.init();
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
++            window.openerp.web.chrome(openerp);
++            // views loader stuff
++            window.openerp.web.data(openerp);
++            window.openerp.web.views(openerp);
              window.openerp.web.list(openerp);
          }
      });
diff --cc addons/web/static/test/onchange.js
index 8074c75,8074c75..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,74 -1,74 +1,0 @@@
--$(document).ready(function () {
--    var openerp,
--        make_form = function (default_values) {
--            var fields = {};
--            _(default_values).each(function (value, name) {
--                fields[name] = value instanceof Function ? value : {
--                        get_on_change_value: function () { return value; }
--                    };
--            });
--            return _.extend(new openerp.web.FormView(null, {}),
--                    {fields: fields});
--        };
--    module("form.onchange", {
--        setup: function () {
--            openerp = window.openerp.init(true);
--            window.openerp.web.core(openerp);
--            window.openerp.web.chrome(openerp);
--            // views loader stuff
--            window.openerp.web.data(openerp);
--            window.openerp.web.views(openerp);
--            window.openerp.web.list(openerp);
--            window.openerp.web.form(openerp);
--        }
--    });
--    test('Parse args-less onchange', function () {
--        var f = new openerp.web.FormView(null, {});
--        var result = f.parse_on_change('on_change_foo()', {});
--        equal(result.method, 'on_change_foo');
--        deepEqual(result.args, []);
--    });
--    test('Parse 1-arg onchange', function () {
--        var f = make_form({foo:  3});
--        var result = f.parse_on_change('on_change_foo(foo)', {});
--        equal(result.method, 'on_change_foo');
--        deepEqual(result.args, [3]);
--    });
--    test('Parse 2-args onchange', function () {
--        var f = make_form({foo: 3, bar: 'qux'});
--        var result = f.parse_on_change('on_change_foo(bar, foo)', {});
--        equal(result.method, 'on_change_foo');
--        deepEqual(result.args, ['qux', 3]);
--    });
--    test('Literal null', function () {
--        var f = make_form();
--        var result = f.parse_on_change('on_null(None)', {});
--        deepEqual(result.args, [null]);
--    });
--    test('Literal true', function () {
--        var f = make_form();
--        var result = f.parse_on_change('on_null(True)', {});
--        deepEqual(result.args, [true]);
--    });
--    test('Literal false', function () {
--        var f = make_form();
--        var result = f.parse_on_change('on_null(False)', {});
--        deepEqual(result.args, [false]);
--    });
--    test('Literal string', function () {
--        var f = make_form();
--        var result = f.parse_on_change('on_str("foo")', {});
--        deepEqual(result.args, ['foo']);
--        var result2 = f.parse_on_change("on_str('foo')", {});
--        deepEqual(result2.args, ['foo']);
--    });
--    test('Literal number', function () {
--        var f = make_form();
--        var result = f.parse_on_change('on_str(42)', {});
--        deepEqual(result.args, [42]);
--        var result2 = f.parse_on_change("on_str(-25)", {});
--        deepEqual(result2.args, [-25]);
--        var result3 = f.parse_on_change("on_str(25.02)", {});
--        deepEqual(result3.args, [25.02]);
--    });
--});
@@@ -2,8 -2,8 +2,8 @@@ $(document).ready(function () 
      var openerp;
      module('Registry', {
          setup: function () {
--            openerp = window.openerp.init(true);
--            window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
              openerp.web.Foo = {};
              openerp.web.Bar = {};
              openerp.web.Foo2 = {};
index 7dbd323,0000000..8a49a48
mode 100644,000000..100644
--- /dev/null
@@@ -1,130 -1,0 +1,131 @@@
 +$(document).ready(function () {
 +    var openerp;
 +
 +    module('Misordered resolution management', {
 +        setup: function () {
-             openerp = window.openerp.init();
-             window.openerp.web.core(openerp);
++            openerp = window.openerp.init([]);
++            window.openerp.web.corelib(openerp);
++            window.openerp.web.coresetup(openerp);
 +            window.openerp.web.data(openerp);
 +        }
 +    });
 +    test('Resolve all correctly ordered, sync', function () {
 +        var dm = new openerp.web.DropMisordered(), flag = false;
 +
 +        var d1 = $.Deferred(), d2 = $.Deferred(),
 +            r1 = dm.add(d1), r2 = dm.add(d2);
 +
 +        $.when(r1, r2).done(function () {
 +            flag = true;
 +        });
 +        d1.resolve();
 +        d2.resolve();
 +
 +        ok(flag);
 +    });
 +    test("Don't resolve mis-ordered, sync", function () {
 +        var dm = new openerp.web.DropMisordered(),
 +            done1 = false, done2 = false,
 +            fail1 = false, fail2 = false;
 +
 +        var d1 = $.Deferred(), d2 = $.Deferred();
 +        dm.add(d1).then(function () { done1 = true; },
 +                        function () { fail1 = true; });
 +        dm.add(d2).then(function () { done2 = true; },
 +                        function () { fail2 = true; });
 +
 +        d2.resolve();
 +        d1.resolve();
 +
 +        // d1 is in limbo
 +        ok(!done1);
 +        ok(!fail1);
 +        // d2 is resolved
 +        ok(done2);
 +        ok(!fail2);
 +    });
 +    test('Fail mis-ordered flag, sync', function () {
 +        var dm = new openerp.web.DropMisordered(true),
 +            done1 = false, done2 = false,
 +            fail1 = false, fail2 = false;
 +
 +        var d1 = $.Deferred(), d2 = $.Deferred();
 +        dm.add(d1).then(function () { done1 = true; },
 +                        function () { fail1 = true; });
 +        dm.add(d2).then(function () { done2 = true; },
 +                        function () { fail2 = true; });
 +
 +        d2.resolve();
 +        d1.resolve();
 +
 +        // d1 is failed
 +        ok(!done1);
 +        ok(fail1);
 +        // d2 is resolved
 +        ok(done2);
 +        ok(!fail2);
 +    });
 +
 +    asyncTest('Resolve all correctly ordered, sync', 1, function () {
 +        var dm = new openerp.web.DropMisordered();
 +
 +        var d1 = $.Deferred(), d2 = $.Deferred(),
 +            r1 = dm.add(d1), r2 = dm.add(d2);
 +
 +        setTimeout(function () { d1.resolve(); }, 100);
 +        setTimeout(function () { d2.resolve(); }, 200);
 +
 +        $.when(r1, r2).done(function () {
 +            start();
 +            ok(true);
 +        });
 +    });
 +    asyncTest("Don't resolve mis-ordered, sync", 4, function () {
 +        var dm = new openerp.web.DropMisordered(),
 +            done1 = false, done2 = false,
 +            fail1 = false, fail2 = false;
 +
 +        var d1 = $.Deferred(), d2 = $.Deferred();
 +        dm.add(d1).then(function () { done1 = true; },
 +                        function () { fail1 = true; });
 +        dm.add(d2).then(function () { done2 = true; },
 +                        function () { fail2 = true; });
 +
 +        setTimeout(function () { d1.resolve(); }, 200);
 +        setTimeout(function () { d2.resolve(); }, 100);
 +
 +        setTimeout(function () {
 +            start();
 +            // d1 is in limbo
 +            ok(!done1);
 +            ok(!fail1);
 +            // d2 is resolved
 +            ok(done2);
 +            ok(!fail2);
 +        }, 400);
 +    });
 +    asyncTest('Fail mis-ordered flag, sync', 4, function () {
 +        var dm = new openerp.web.DropMisordered(true),
 +            done1 = false, done2 = false,
 +            fail1 = false, fail2 = false;
 +
 +        var d1 = $.Deferred(), d2 = $.Deferred();
 +        dm.add(d1).then(function () { done1 = true; },
 +                        function () { fail1 = true; });
 +        dm.add(d2).then(function () { done2 = true; },
 +                        function () { fail2 = true; });
 +
 +        setTimeout(function () { d1.resolve(); }, 200);
 +        setTimeout(function () { d2.resolve(); }, 100);
 +
 +        setTimeout(function () {
 +            start();
 +            // d1 is failed
 +            ok(!done1);
 +            ok(fail1);
 +            // d2 is resolved
 +            ok(done2);
 +            ok(!fail2);
 +        }, 400);
 +    });
 +});
  
      <script src="/web/static/lib/qweb/qweb2.js"></script>
  
-     <script src="/web/static/lib/py.parse/lib/py.js"></script>
+     <script src="/web/static/lib/py.js/lib/py.js"></script>
  
 -    <script src="/web/static/lib/novajs/src/nova.js"></script>
 -
      <script src="/web/static/src/js/boot.js"></script>
--    <script src="/web/static/src/js/core.js"></script>
++    <script src="/web/static/src/js/corelib.js"></script>
++    <script src="/web/static/src/js/coresetup.js"></script>
      <script src="/web/static/src/js/dates.js"></script>
      <script src="/web/static/src/js/formats.js"></script>
      <script src="/web/static/src/js/chrome.js"></script>
@@@ -50,6 -52,6 +51,6 @@@
      <script type="text/javascript" src="/web/static/test/form.js"></script>
      <script type="text/javascript" src="/web/static/test/list-utils.js"></script>
      <script type="text/javascript" src="/web/static/test/formats.js"></script>
--    <script type="text/javascript" src="/web/static/test/onchange.js"></script>
 +    <script type="text/javascript" src="/web/static/test/rpc.js"></script>
+     <script type="text/javascript" src="/web/static/test/evals.js"></script>
  </html>
@@@ -107,7 -107,7 +107,7 @@@ openerp.web_process = function (instanc
              if(this.process_id)
                  return def.resolve().promise();
  
-             this.process_dataset = new openerp.web.DataSet(this, "process.process", this.session.context);
 -            this.process_dataset = new instance.web.DataSetStatic(this, "process.process", this.session.context);
++            this.process_dataset = new instance.web.DataSet(this, "process.process", this.session.context);
              this.process_dataset
              .call("search_by_model", [self.process_model,self.session.context])
              .done(function(res) {
          },
          jump_to_view: function(model, id) {
              var self = this;
-             var dataset = new openerp.web.DataSet(this, 'ir.values', this.session.context);
 -            var dataset = new instance.web.DataSetStatic(this, 'ir.values', this.session.context);
++            var dataset = new instance.web.DataSet(this, 'ir.values', this.session.context);
              dataset.call('get',
                  ['action', 'tree_but_open',[['ir.ui.menu', id]], dataset.context],
                  function(res) {
diff --cc doc/index.rst
@@@ -9,19 -9,10 +9,21 @@@ Welcome to OpenERP Web's documentation
  Contents:
  
  .. toctree::
 +    :maxdepth: 1
 +
 +    changelog-6.2
 +
 +    async
 +    rpc
 +
 +Older stuff
 +-----------
 +
 +.. toctree::
     :maxdepth: 2
  
+    search-view
     getting-started
     production
     widgets