diff --git a/addons/base/__openerp__.py b/addons/base/__openerp__.py index fa17af8c536..01d712ef41e 100644 --- a/addons/base/__openerp__.py +++ b/addons/base/__openerp__.py @@ -17,7 +17,7 @@ "static/lib/qweb/qweb2.js", "static/lib/underscore/underscore.js", "static/lib/underscore/underscore.string.js", - "static/src/js/base.js", + "static/src/js/boot.js", "static/src/js/chrome.js", "static/src/js/controller.js", "static/src/js/views.js", diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/boot.js similarity index 97% rename from addons/base/static/src/js/base.js rename to addons/base/static/src/js/boot.js index 8e593de5949..25ab0bda632 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/boot.js @@ -1,5 +1,5 @@ //--------------------------------------------------------- -// OpenERP initialisation and black magic about the pool +// OpenERP Web Boostrap //--------------------------------------------------------- /** @@ -57,7 +57,6 @@ // OpenERP base module split //--------------------------------------------------------- -/** @namespace */ openerp.base = function(instance) { openerp.base.controller(instance); openerp.base.dates(instance); diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 2cc16acc7ef..996642385ef 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -136,107 +136,6 @@ openerp.base.Registry = openerp.base.Class.extend( /** @lends openerp.base.Regis } }); -/** - * OpenERP session aware controller - * a controller takes an already existing dom element and manage it - */ -openerp.base.Controller = openerp.base.Controller.extend( /** @lends openerp.base.Controller# */{ - init: function(parent, element_id) { - this._super(parent, element_id); - if(this.controller_parent && this.controller_parent.session) { - this.session = this.controller_parent.session; - } - }, - /** - * Performs a JSON-RPC call - * - * @param {String} url endpoint url - * @param {Object} data RPC parameters - * @param {Function} success RPC call success callback - * @param {Function} error RPC call error callback - * @returns {jQuery.Deferred} deferred object for the RPC call - */ - rpc: function(url, data, success, error) { - return this.session.rpc(url, data, success, error); - }, - do_action: function(action, on_finished) { - return this.parent.do_action(action, on_finished); - } -}); - -/** - * OpenERP session aware widget - * A widget is a controller that doesnt take an element_id - * it render its own html render() that you should insert into the dom - * and bind it at start() - */ -openerp.base.BaseWidget = openerp.base.Controller.extend({ - /** - * The name of the QWeb template that will be used for rendering. Must be - * redefined in subclasses or the render() method can not be used. - * - * @type string - */ - template: null, - /** - * The prefix used to generate an id automatically. Should be redefined in - * subclasses. If it is not defined, a default identifier will be used. - * - * @type string - */ - identifier_prefix: 'generic-identifier', - /** - * Base class for widgets. Handle rendering (based on a QWeb template), - * identifier generation, parenting and destruction of the widget. - * Also initialize the identifier. - * - * @constructs - * @params {openerp.base.search.BaseWidget} parent The parent widget. - */ - init: function (parent) { - this._super(parent); - this.make_id(this.identifier_prefix); - }, - /** - * Sets and returns a globally unique identifier for the widget. - * - * If a prefix is appended, the identifier will be appended to it. - * - * @params sections prefix sections, empty/falsy sections will be removed - */ - make_id: function () { - this.element_id = _.uniqueId(_.toArray(arguments).join('_')); - return this.element_id; - }, - /** - * Render the widget. This.template must be defined. - * The content of the current object is passed as context to the template. - * - * @param {object} additional Additional context arguments to pass to the template. - */ - render: function (additional) { - return QWeb.render(this.template, _.extend({}, this, additional != null ? additional : {})); - }, - /** - * "Starts" the widgets. Called at the end of the rendering, this allows - * to get a jQuery object referring to the DOM ($element attribute). - */ - start: function () { - this._super(); - var tmp = document.getElementById(this.element_id); - this.$element = tmp ? $(tmp) : null; - }, - /** - * "Stops" the widgets. Called when the view destroys itself, this - * lets the widgets clean up after themselves. - */ - stop: function () { - if(this.$element != null) { - this.$element.remove(); - } - this._super(); - } -}); openerp.base.Session = openerp.base.Controller.extend( /** @lends openerp.base.Session# */{ /** @@ -544,8 +443,8 @@ openerp.base.Dialog = openerp.base.BaseWidget.extend({ max_height: '100%', autoOpen: false, buttons: {}, - close: function () { - self.stop(); + beforeClose: function () { + self.on_close(); } }; for (var f in this) { @@ -609,7 +508,15 @@ openerp.base.Dialog = openerp.base.BaseWidget.extend({ this.set_options(options); this.$dialog.dialog(this.options).dialog('open'); }, + close: function() { + // Closes the dialog but leave it in a state where it could be opened again. + this.$dialog.dialog('close'); + }, + on_close: function() { + }, stop: function () { + // Destroy widget + this.close(); this.$dialog.dialog('destroy'); } }); diff --git a/addons/base/static/src/js/controller.js b/addons/base/static/src/js/controller.js index c9ca3193e70..1a13ab18607 100644 --- a/addons/base/static/src/js/controller.js +++ b/addons/base/static/src/js/controller.js @@ -145,6 +145,10 @@ instance.base.generate_null_object_class = function(claz, add) { return tmpclass.extend(add || {}); }; +// -------------------------------------------------------- +// OLD +// -------------------------------------------------------- + /** * OpenERP Controller * TODO merge BaseWidget with Controller @@ -243,6 +247,303 @@ instance.base.Controller = instance.base.Class.extend( /** @lends instance.base. } }); +/** + * OpenERP session aware controller + * a controller takes an already existing dom element and manage it + */ +instance.base.Controller = instance.base.Controller.extend( /** @lends openerp.base.Controller# */{ + init: function(parent, element_id) { + this._super(parent, element_id); + if(this.controller_parent && this.controller_parent.session) { + this.session = this.controller_parent.session; + } + }, + /** + * Performs a JSON-RPC call + * + * @param {String} url endpoint url + * @param {Object} data RPC parameters + * @param {Function} success RPC call success callback + * @param {Function} error RPC call error callback + * @returns {jQuery.Deferred} deferred object for the RPC call + */ + rpc: function(url, data, success, error) { + return this.session.rpc(url, data, success, error); + }, + do_action: function(action, on_finished) { + return this.parent.do_action(action, on_finished); + } +}); + +/** + * OpenERP session aware widget + * A widget is a controller that doesnt take an element_id + * it render its own html render() that you should insert into the dom + * and bind it at start() + */ +instance.base.BaseWidget = instance.base.Controller.extend({ + /** + * The name of the QWeb template that will be used for rendering. Must be + * redefined in subclasses or the render() method can not be used. + * + * @type string + */ + template: null, + /** + * The prefix used to generate an id automatically. Should be redefined in + * subclasses. If it is not defined, a default identifier will be used. + * + * @type string + */ + identifier_prefix: 'generic-identifier', + /** + * Base class for widgets. Handle rendering (based on a QWeb template), + * identifier generation, parenting and destruction of the widget. + * Also initialize the identifier. + * + * @constructs + * @params {openerp.base.search.BaseWidget} parent The parent widget. + */ + init: function (parent) { + this._super(parent); + this.make_id(this.identifier_prefix); + }, + /** + * Sets and returns a globally unique identifier for the widget. + * + * If a prefix is appended, the identifier will be appended to it. + * + * @params sections prefix sections, empty/falsy sections will be removed + */ + make_id: function () { + this.element_id = _.uniqueId(_.toArray(arguments).join('_')); + return this.element_id; + }, + /** + * Render the widget. This.template must be defined. + * The content of the current object is passed as context to the template. + * + * @param {object} additional Additional context arguments to pass to the template. + */ + render: function (additional) { + return QWeb.render(this.template, _.extend({}, this, additional != null ? additional : {})); + }, + /** + * "Starts" the widgets. Called at the end of the rendering, this allows + * to get a jQuery object referring to the DOM ($element attribute). + */ + start: function () { + this._super(); + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : null; + }, + /** + * "Stops" the widgets. Called when the view destroys itself, this + * lets the widgets clean up after themselves. + */ + stop: function () { + if(this.$element != null) { + this.$element.remove(); + } + this._super(); + } +}); + +// -------------------------------------------------------- +// N-style aka New-Style or Niv-Style +// -------------------------------------------------------- + +instance.base.NivController = instance.base.Class.extend({ + init: function(parent) { + this.controller_parent = parent; + // Take the session of the parent if defined + if(this.controller_parent && this.controller_parent.session) { + this.session = this.controller_parent.session; + } + // Transform on_* method into openerp.base.callbacks + for (var name in this) { + if(typeof(this[name]) == "function") { + this[name].debug_name = name; + // bind ALL function to this not only on_and _do ? + if((/^on_|^do_/).test(name)) { + this[name] = instance.base.callback(this, this[name]); + } + } + } + }, + /** + * Event binding, rpc and callback calling required to initialize the + * object should happen here + * + * Returns a promise object letting callers (subclasses and direct callers) + * know when this component is done starting + * + * @returns {jQuery.Deferred} + */ + start: function() { + // returns an already fulfilled promise. Maybe we could return nothing? + // $.when can take non-deferred and in that case it simply considers + // them all as fulfilled promises. + // But in thise case we *have* to ensure callers use $.when and don't + // try to call deferred methods on this return value. + return $.Deferred().done().promise(); + }, + stop: function() { + }, + log: function() { + var args = Array.prototype.slice.call(arguments); + var caller = arguments.callee.caller; + // TODO add support for line number using + // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js + // args.unshift("" + caller.debug_name); + this.on_log.apply(this,args); + }, + on_log: function() { + if(window.openerp.debug || (window.location.search.indexOf('?debug') !== -1)) { + var notify = false; + var body = false; + if(window.console) { + console.log(arguments); + } else { + body = true; + } + var a = Array.prototype.slice.call(arguments, 0); + for(var i = 0; i < a.length; i++) { + var v = a[i]==null ? "null" : a[i].toString(); + if(i==0) { + notify = v.match(/^not/); + body = v.match(/^bod/); + } + if(body) { + $('
').text(v).appendTo($('body'));
+                }
+                if(notify && this.notification) {
+                    this.notification.notify("Logging:",v);
+                }
+            }
+        }
+
+    },
+    /**
+     * Performs a JSON-RPC call
+     *
+     * @param {String} url endpoint url
+     * @param {Object} data RPC parameters
+     * @param {Function} success RPC call success callback
+     * @param {Function} error RPC call error callback
+     * @returns {jQuery.Deferred} deferred object for the RPC call
+     */
+    rpc: function(url, data, success, error) {
+        return this.session.rpc(url, data, success, error);
+    },
+    do_action: function(action, on_finished) {
+        return this.controller_parent.do_action(action, on_finished);
+    }
+});
+
+instance.base.NivWidget = instance.base.Controller.extend({
+    /**
+     * The name of the QWeb template that will be used for rendering. Must be
+     * redefined in subclasses or the render() method can not be used.
+     * 
+     * @type string
+     */
+    template: null,
+    /**
+     * The prefix used to generate an id automatically. Should be redefined in
+     * subclasses. If it is not defined, a default identifier will be used.
+     * 
+     * @type string
+     */
+    identifier_prefix: 'generic-identifier',
+    /**
+     * Base class for widgets. Handle rendering (based on a QWeb template),
+     * identifier generation, parenting and destruction of the widget.
+     * Also initialize the identifier.
+     *
+     * @constructs
+     * @params {openerp.base.search.BaseWidget} parent The parent widget.
+     */
+    init: function(parent, element_id) {
+        this._super(parent);
+        this.make_id(this.identifier_prefix);
+        // this.element_id = element_id;
+        // this.$element = $('#' + element_id);
+        // if (element_id) {
+        //     instance.screen[element_id] = this;
+        // }
+        // save the parent children relationship
+        this.controller_children = [];
+        if(parent && parent.controller_children) {
+            parent.controller_children.push(this);
+        }
+        // backward compatibility
+        this.parent = this.controller_parent;
+        this.children = this.controller_children;
+
+    },
+    /**
+     * Event binding, rpc and callback calling required to initialize the
+     * object should happen here
+     *
+     * Returns a promise object letting callers (subclasses and direct callers)
+     * know when this component is done starting
+     *
+     * @returns {jQuery.Deferred}
+     */
+    /**
+     * "Starts" the widgets. Called at the end of the rendering, this allows
+     * to get a jQuery object referring to the DOM ($element attribute).
+     */
+    start: function () {
+        this._super();
+        var tmp = document.getElementById(this.element_id);
+        this.$element = tmp ? $(tmp) : null;
+    },
+    stop: function() {
+        if(this.$element != null) {
+            this.$element.remove();
+        }
+        if (this.parent && this.parent.children) {
+            this.parent.children = _.without(this.parent.children, this);
+            this.parent.controller_children = this.parent.children;
+        }
+        this.parent = null;
+        this.controller_parent = null;
+    },
+    /**
+     * Sets and returns a globally unique identifier for the widget.
+     *
+     * If a prefix is appended, the identifier will be appended to it.
+     *
+     * @params sections prefix sections, empty/falsy sections will be removed
+     */
+    make_id: function () {
+        this.element_id = _.uniqueId(_.toArray(arguments).join('_'));
+        return this.element_id;
+    },
+    /**
+     * Render the widget. This.template must be defined.
+     * The content of the current object is passed as context to the template.
+     * 
+     * @param {object} additional Additional context arguments to pass to the template.
+     */
+    render: function (additional) {
+        return QWeb.render(this.template, _.extend({}, this, additional != null ? additional : {}));
+    },
+    widget_add: function(element, addfunc) {
+    },
+    widget_append: function(element) {
+    },
+    widget_prepend: function(element) {
+    },
+    widget_append2: function(element) {
+    },
+    widget_prepend2: function(element) {
+    },
+});
+
+
 };
 
 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js
index 385bd93d360..9eb08cf0fde 100644
--- a/addons/base/static/src/js/data.js
+++ b/addons/base/static/src/js/data.js
@@ -291,14 +291,18 @@ openerp.base.DataSet =  openerp.base.Controller.extend( /** @lends openerp.base.
      * Read the indexed record.
      */
     read_index: function (fields, callback) {
+        var def = $.Deferred().then(callback);
         if (_.isEmpty(this.ids)) {
-            return $.Deferred().reject().promise();
+            def.reject();
         } else {
             fields = fields || false;
-            return this.read_ids([this.ids[this.index]], fields, function(records) {
-                callback(records[0]);
+            return this.read_ids([this.ids[this.index]], fields).then(function(records) {
+                def.resolve(records[0]);
+            }, function() {
+                def.reject.apply(def, arguments);
             });
         }
+        return def.promise();
     },
     default_get: function(fields, callback) {
         return this.rpc('/base/dataset/default_get', {
diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js
index d5a87a78309..80ef7efe96d 100644
--- a/addons/base/static/src/js/form.js
+++ b/addons/base/static/src/js/form.js
@@ -20,7 +20,6 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi
     init: function(parent, element_id, dataset, view_id, options) {
         this._super(parent, element_id);
         this.set_default_options();
-        this.view_manager = parent || new openerp.base.NullViewManager();
         this.dataset = dataset;
         this.model = dataset.model;
         this.view_id = view_id;
@@ -49,9 +48,6 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi
             return def.promise();
         } else {
             var context = new openerp.base.CompoundContext(this.dataset.get_context());
-            if (this.view_manager.action && this.view_manager.action.context) {
-                context.add(this.view_manager.action.context);
-            }
             return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id,
                 toolbar: this.options.sidebar, context: context}, this.on_loaded);
         }
@@ -718,6 +714,10 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({
     init: function(view, node) {
         this._super(view, node);
         this.template = "WidgetButton";
+        if (this.string) {
+            // We don't have button key bindings in the webclient
+            this.string = this.string.replace(/_/g, '');
+        }
         if (node.attrs.default_focus == '1') {
             // TODO fme: provide enter key binding to widgets
             this.view.default_focus_button = this;
@@ -774,11 +774,12 @@ openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({
         if (this.node.tag == 'label' && this.node.attrs.colspan) {
             this.is_field_label = false;
             this.template = "WidgetParagraph";
+            this.colspan = this.node.attrs.colspan;
         } else {
             this.is_field_label = true;
             this.template = "WidgetLabel";
+            this.colspan = 1;
         }
-        this.colspan = 1;
     },
     render: function () {
         if (this['for'] && this.type !== 'label') {
@@ -833,10 +834,12 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({
     },
     update_dom: function() {
         this._super.apply(this, arguments);
-        this.$element.toggleClass('disabled', this.readonly);
-        this.$element.toggleClass('required', this.required);
-        if (this.view.show_invalid) {
-            this.$element.toggleClass('invalid', !this.is_valid());
+        if (!this.disable_utility_classes) {
+            this.$element.toggleClass('disabled', this.readonly);
+            this.$element.toggleClass('required', this.required);
+            if (this.view.show_invalid) {
+                this.$element.toggleClass('invalid', !this.is_valid());
+            }
         }
     },
     on_ui_change: function() {
@@ -1477,7 +1480,7 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({
                 }, self.build_domain(),
                 new openerp.base.CompoundContext(self.build_context(), context || {}));
         pop.on_select_elements.add(function(element_ids) {
-            var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context());
+            var dataset = new openerp.base.DataSetStatic(self, self.field.relation, self.build_context());
             dataset.name_get([element_ids[0]], function(data) {
                 self._change_int_ext_value(data[0]);
                 pop.stop();
@@ -1594,6 +1597,7 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
         this.template = "FieldOne2Many";
         this.is_started = $.Deferred();
         this.form_last_update = $.Deferred();
+        this.disable_utility_classes = true;
     },
     start: function() {
         this._super.apply(this, arguments);
@@ -1639,12 +1643,16 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
                 controller.on_record_loaded.add_last(function() {
                     once.resolve();
                 });
-                controller.on_form_changed.add_last(function() {
+                controller.on_pager_action.add_first(function() {
                     self.save_form_view();
                 });
+                controller.$element.find(".oe_form_button_save_edit").hide();
             }
             self.is_started.resolve();
         });
+        this.viewmanager.on_mode_switch.add_first(function() {
+            self.save_form_view();
+        });
         setTimeout(function () {
             self.viewmanager.start();
         }, 0);
@@ -1714,6 +1722,9 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
             this._super(value);
             this.dataset.reset_ids(value);
         }
+        if (this.dataset.index === null && this.dataset.ids.length > 0) {
+            this.dataset.index = 0;
+        }
         $.when(this.is_started).then(function() {
             self.reload_current_view();
         });
@@ -1722,7 +1733,6 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
         var self = this;
         if (!this.dataset)
             return [];
-        this.save_form_view();
         var val = this.dataset.delete_all ? [commands.delete_all()] : [];
         val = val.concat(_.map(this.dataset.ids, function(id) {
             var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
@@ -1753,16 +1763,20 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
                 } else if (!res.isResolved()) {
                     throw "Asynchronous get_value() is not supported in form view.";
                 }
+                return res;
             }
         }
+        return false;
+    },
+    is_valid: function() {
+        this.validate();
+        return this._super();
     },
     validate: function() {
         this.invalid = false;
         var self = this;
         var view = self.viewmanager.views[self.viewmanager.active_view].controller;
-        if(self.viewmanager.active_view === "list") {
-            return;
-        } else if (self.viewmanager.active_view === "form") {
+        if (self.viewmanager.active_view === "form") {
             for (var f in view.fields) {
                 f = view.fields[f];
                 if (!f.is_valid()) {
@@ -1771,6 +1785,14 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
                 }
             }
         }
+    },
+    is_dirty: function() {
+        this.save_form_view();
+        return this._super();
+    },
+    update_dom: function() {
+        this._super.apply(this, arguments);
+        this.$element.toggleClass('disabled', this.readonly);
     }
 });
 
@@ -1837,7 +1859,7 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({
 
         var self = this;
 
-        this.dataset = new openerp.base.form.Many2ManyDataSet( this, this.field.relation);
+        this.dataset = new openerp.base.form.Many2ManyDataSet(this, this.field.relation);
         this.dataset.m2m = this;
         this.dataset.on_unlink.add_last(function(ids) {
             self.on_ui_change();
diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js
index aa66f40e925..d25e34a26f4 100644
--- a/addons/base/static/src/js/views.js
+++ b/addons/base/static/src/js/views.js
@@ -98,11 +98,14 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
 
 openerp.base.ActionDialog = openerp.base.Dialog.extend({
     identifier_prefix: 'action_dialog',
-    stop: function() {
+    on_close: function() {
         this._super(this, arguments);
         if (this.close_callback) {
             this.close_callback();
         }
+    },
+    stop: function() {
+        this._super(this, arguments);
         if (this.viewmanager) {
             this.viewmanager.stop();
         }