[FIX] web: force seession context reload when reloading client
[odoo/odoo.git] / addons / web / static / src / js / chrome.js
1 /*---------------------------------------------------------
2  * OpenERP Web chrome
3  *---------------------------------------------------------*/
4 (function() {
5
6 var instance = openerp;
7 openerp.web.chrome = {};
8
9 var QWeb = instance.web.qweb,
10     _t = instance.web._t;
11
12 instance.web.Notification =  instance.web.Widget.extend({
13     template: 'Notification',
14     init: function() {
15         this._super.apply(this, arguments);
16         instance.web.notification = this;
17     },
18     start: function() {
19         this._super.apply(this, arguments);
20         this.$el.notify({
21             speed: 500,
22             expires: 2500
23         });
24     },
25     notify: function(title, text, sticky) {
26         sticky = !!sticky;
27         var opts = {};
28         if (sticky) {
29             opts.expires = false;
30         }
31         return this.$el.notify('create', {
32             title: title,
33             text: text
34         }, opts);
35     },
36     warn: function(title, text, sticky) {
37         sticky = !!sticky;
38         var opts = {};
39         if (sticky) {
40             opts.expires = false;
41         }
42         return this.$el.notify('create', 'oe_notification_alert', {
43             title: title,
44             text: text
45         }, opts);
46     }
47 });
48
49 var opened_modal = [];
50
51 instance.web.action_notify = function(element, action) {
52     element.do_notify(action.params.title, action.params.text, action.params.sticky);
53 };
54 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
55
56 instance.web.action_warn = function(element, action) {
57     element.do_warn(action.params.title, action.params.text, action.params.sticky);
58 };
59 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
60
61 /**
62     A useful class to handle dialogs.
63
64     Attributes:
65     - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
66     during the lifecycle of the dialog.
67 */
68 instance.web.Dialog = instance.web.Widget.extend({
69     dialog_title: "",
70     /**
71         Constructor.
72
73         @param {Widget} parent
74         @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
75             dictionary can contain the following keys:
76             - size: one of the following: 'large', 'medium', 'small'
77             - dialogClass: class to add to the body of dialog
78             - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
79                 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
80                 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
81                 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
82                 feature does not allow that kind of personalization.
83             - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
84         @param {jQuery object} content Some content to replace this.$el .
85     */
86     init: function (parent, options, content) {
87         var self = this;
88         this._super(parent);
89         this.content_to_set = content;
90         this.dialog_options = {
91             destroy_on_close: true,
92             size: 'large', //'medium', 'small'
93             buttons: null,
94         };
95         if (options) {
96             _.extend(this.dialog_options, options);
97         }
98         this.on("closing", this, this._closing);
99         this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
100     },
101     renderElement: function() {
102         if (this.content_to_set) {
103             this.setElement(this.content_to_set);
104         } else if (this.template) {
105             this._super();
106         }
107     },
108     /**
109         Opens the popup. Inits the dialog if it is not already inited.
110
111         @return this
112     */
113     open: function() {
114         if (!this.dialog_inited) {
115             this.init_dialog();
116         }
117         this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
118         //add to list of currently opened modal
119         opened_modal.push(this.$dialog_box);
120         return this;
121     },
122     _add_buttons: function(buttons) {
123         var self = this;
124         var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
125         _.each(buttons, function(fn, text) {
126             // buttons can be object or array
127             if (!_.isFunction(fn)) {
128                 text = fn.text;
129                 fn = fn.click;
130             }
131             var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
132             $customButons.append($but);
133             $but.on('click', function(ev) {
134                 fn.call(self.$el, ev);
135             });
136         });
137     },
138     /**
139         Initializes the popup.
140
141         @return The result returned by start().
142     */
143     init_dialog: function() {
144         var self = this;
145         var options = _.extend({}, this.dialog_options);
146         options.title = options.title || this.dialog_title;
147         if (options.buttons) {
148             this._add_buttons(options.buttons);
149             delete(options.buttons);
150         }
151         this.renderElement();
152
153         this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
154         this.$el.modal({
155             'backdrop': false,
156             'keyboard': true,
157         });
158         if (options.size !== 'large'){
159             var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
160             if (options.size === 'small'){
161                 dialog_class_size.addClass('modal-sm');
162             }
163         }
164
165         this.$el.appendTo(this.$dialog_box.find(".modal-body"));
166         var $dialog_content = this.$dialog_box.find('.modal-content');
167         if (options.dialogClass){
168             $dialog_content.find(".modal-body").addClass(options.dialogClass);
169         }
170         $dialog_content.openerpClass();
171
172         this.$dialog_box.on('hidden.bs.modal', this, function() {
173             self.close();
174         });
175         this.$dialog_box.modal('show');
176
177         this.dialog_inited = true;
178         var res = this.start();
179         return res;
180     },
181     /**
182         Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
183     */
184     close: function(reason) {
185         if (this.dialog_inited && !this.__tmp_dialog_hiding) {
186             $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
187             this.trigger("closing", reason);
188             if (this.$el.is(":data(bs.modal)")) {     // may have been destroyed by closing signal
189                 this.__tmp_dialog_hiding = true;
190                 this.$dialog_box.modal('hide');
191                 this.__tmp_dialog_hiding = undefined;
192             }
193         }
194     },
195     _closing: function() {
196         if (this.__tmp_dialog_destroying)
197             return;
198         if (this.dialog_options.destroy_on_close) {
199             this.__tmp_dialog_closing = true;
200             this.destroy();
201             this.__tmp_dialog_closing = undefined;
202         }
203     },
204     /**
205         Destroys the popup, also closes it.
206     */
207     destroy: function (reason) {
208         this.$buttons.remove();
209         var self = this;
210         _.each(this.getChildren(), function(el) {
211             el.destroy();
212         });
213         if (! this.__tmp_dialog_closing) {
214             this.__tmp_dialog_destroying = true;
215             this.close(reason);
216             this.__tmp_dialog_destroying = undefined;
217         }
218         if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
219             //we need this to put the instruction to remove modal from DOM at the end
220             //of the queue, otherwise it might already have been removed before the modal-backdrop
221             //is removed when pressing escape key
222             var $element = this.$dialog_box;
223             setTimeout(function () {
224                 //remove modal from list of opened modal since we just destroy it
225                 var modal_list_index = $.inArray($element, opened_modal);
226                 if (modal_list_index > -1){
227                     opened_modal.splice(modal_list_index,1)[0].remove();
228                 }
229                 if (opened_modal.length > 0){
230                     //we still have other opened modal so we should focus it
231                     opened_modal[opened_modal.length-1].focus();
232                 }
233             },0);
234         }
235         this._super();
236     }
237 });
238
239 instance.web.CrashManager = instance.web.Class.extend({
240     init: function() {
241         this.active = true;
242     },
243
244     rpc_error: function(error) {
245         if (!this.active) {
246             return;
247         }
248         var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
249         if (handler) {
250             new (handler)(this, error).display();
251             return;
252         }
253         if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
254             this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
255             return;
256         }
257         if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
258             this.show_warning(error);
259         } else {
260             this.show_error(error);
261         }
262     },
263     show_warning: function(error) {
264         if (!this.active) {
265             return;
266         }
267         if (error.data.exception_type === "except_osv") {
268             error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
269         }
270         new instance.web.Dialog(this, {
271             size: 'medium',
272             title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
273             buttons: [
274                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
275             ],
276         }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
277     },
278     show_error: function(error) {
279         if (!this.active) {
280             return;
281         }
282         var buttons = {};
283         buttons[_t("Ok")] = function() {
284             this.parents('.modal').modal('hide');
285         };
286         new instance.web.Dialog(this, {
287             title: "OpenERP " + _.str.capitalize(error.type),
288             buttons: buttons
289         }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
290     },
291     show_message: function(exception) {
292         this.show_error({
293             type: _t("Client Error"),
294             message: exception,
295             data: {debug: ""}
296         });
297     },
298 });
299
300 /**
301     An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
302 */
303 instance.web.ExceptionHandler = {
304     /**
305         @param parent The parent.
306         @param error The error object as returned by the JSON-RPC implementation.
307     */
308     init: function(parent, error) {},
309     /**
310         Called to inform to display the widget, if necessary. A typical way would be to implement
311         this interface in a class extending instance.web.Dialog and simply display the dialog in this
312         method.
313     */
314     display: function() {},
315 };
316
317 /**
318     The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
319     instance.web.ExceptionHandler.
320 */
321 instance.web.crash_manager_registry = new instance.web.Registry();
322
323 /**
324  * Handle redirection warnings, which behave more or less like a regular
325  * warning, with an additional redirection button.
326  */
327 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
328     init: function(parent, error) {
329         this._super(parent);
330         this.error = error;
331     },
332     display: function() {
333         error = this.error;
334         error.data.message = error.data.arguments[0];
335
336         new instance.web.Dialog(this, {
337             size: 'medium',
338             title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
339             buttons: [
340                 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }},
341                 {text: error.data.arguments[2], click: function() {
342                     window.location.href='#action='+error.data.arguments[1];
343                     this.$el.parents('.modal').modal('hide');
344                 }}
345             ],
346         }, QWeb.render('CrashManager.warning', {error: error})).open();
347         this.destroy();
348     }
349 });
350 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
351
352 instance.web.Loading = instance.web.Widget.extend({
353     template: _t("Loading"),
354     init: function(parent) {
355         this._super(parent);
356         this.count = 0;
357         this.blocked_ui = false;
358         this.session.on("request", this, this.request_call);
359         this.session.on("response", this, this.response_call);
360         this.session.on("response_failed", this, this.response_call);
361     },
362     destroy: function() {
363         this.on_rpc_event(-this.count);
364         this._super();
365     },
366     request_call: function() {
367         this.on_rpc_event(1);
368     },
369     response_call: function() {
370         this.on_rpc_event(-1);
371     },
372     on_rpc_event : function(increment) {
373         var self = this;
374         if (!this.count && increment === 1) {
375             // Block UI after 3s
376             this.long_running_timer = setTimeout(function () {
377                 self.blocked_ui = true;
378                 instance.web.blockUI();
379             }, 3000);
380         }
381
382         this.count += increment;
383         if (this.count > 0) {
384             if (instance.session.debug) {
385                 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
386             } else {
387                 this.$el.text(_t("Loading"));
388             }
389             this.$el.show();
390             this.getParent().$el.addClass('oe_wait');
391         } else {
392             this.count = 0;
393             clearTimeout(this.long_running_timer);
394             // Don't unblock if blocked by somebody else
395             if (self.blocked_ui) {
396                 this.blocked_ui = false;
397                 instance.web.unblockUI();
398             }
399             this.$el.fadeOut();
400             this.getParent().$el.removeClass('oe_wait');
401         }
402     }
403 });
404
405 instance.web.DatabaseManager = instance.web.Widget.extend({
406     init: function(parent) {
407         this._super(parent);
408         this.unblockUIFunction = instance.web.unblockUI;
409         $.validator.addMethod('matches', function (s, _, re) {
410             return new RegExp(re).test(s);
411         }, _t("Invalid database name"));
412     },
413     start: function() {
414         var self = this;
415         $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
416         var fetch_db = this.rpc("/web/database/get_list", {}).then(
417             function(result) {
418                 self.db_list = result;
419             },
420             function (_, ev) {
421                 ev.preventDefault();
422                 self.db_list = null;
423             });
424         var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
425             self.lang_list = result;
426         });
427         return $.when(fetch_db, fetch_langs).always(self.do_render);
428     },
429     do_render: function() {
430         var self = this;
431         instance.webclient.toggle_bars(true);
432         self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
433         $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
434         $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
435         $('ul.oe_secondary_submenu > li:first').addClass('active');
436         $('ul.oe_secondary_submenu > li').bind('click', function (event) {
437             var menuitem = $(this);
438             menuitem.addClass('active').siblings().removeClass('active');
439             var form_id =menuitem.find('a').attr('href');
440             $(form_id).show().siblings().hide();
441             event.preventDefault();
442         });
443         $('#back-to-login').click(self.do_exit);
444         self.$el.find("td").addClass("oe_form_group_cell");
445         self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
446         self.$el.find("label").addClass("oe_form_label");
447         self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
448         self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
449         self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
450         self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
451         self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
452         self.$el.find("form[name=change_pwd_form]").validate({
453             messages: {
454                 old_pwd: _t("Please enter your previous password"),
455                 new_pwd: _t("Please enter your new password"),
456                 confirm_pwd: {
457                     required: _t("Please confirm your new password"),
458                     equalTo: _t("The confirmation does not match the password")
459                 }
460             },
461             submitHandler: self.do_change_password
462         });
463     },
464     destroy: function () {
465         this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
466         this._super();
467     },
468     /**
469      * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
470      * from unblocking the UI
471      */
472     blockUI: function () {
473         instance.web.blockUI();
474         instance.web.unblockUI = function () {};
475     },
476     /**
477      * Reinstates $.unblockUI so third parties can play with blockUI, and
478      * unblocks the UI
479      */
480     unblockUI: function () {
481         instance.web.unblockUI = this.unblockUIFunction;
482         instance.web.unblockUI();
483     },
484     /**
485      * Displays an error dialog resulting from the various RPC communications
486      * failing over themselves
487      *
488      * @param {Object} error error description
489      * @param {String} error.title title of the error dialog
490      * @param {String} error.error message of the error dialog
491      */
492     display_error: function (error) {
493         return new instance.web.Dialog(this, {
494             size: 'medium',
495             title: error.title,
496             buttons: [
497                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
498             ]
499         }, $('<div>').html(error.error)).open();
500     },
501     do_create: function(form) {
502         var self = this;
503         var fields = $(form).serializeArray();
504         self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
505             if (result) {
506                 instance.web.redirect('/web');
507             } else {
508                 alert("Failed to create database");
509             }
510         });
511     },
512     do_duplicate: function(form) {
513         var self = this;
514         var fields = $(form).serializeArray();
515         self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
516             if (result.error) {
517                 self.display_error(result);
518                 return;
519             }
520             self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
521             self.start();
522         });
523     },
524     do_drop: function(form) {
525         var self = this;
526         var $form = $(form),
527             fields = $form.serializeArray(),
528             $db_list = $form.find('[name=drop_db]'),
529             db = $db_list.val();
530         if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
531             return;
532         }
533         self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
534             if (result.error) {
535                 self.display_error(result);
536                 return;
537             }
538             self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
539             self.start();
540         });
541     },
542     do_backup: function(form) {
543         var self = this;
544         self.blockUI();
545         self.session.get_file({
546             form: form,
547             success: function () {
548                 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
549             },
550             error: function(error){
551                if(error){
552                   self.display_error({
553                         title: _t("Backup Database"),
554                         error: 'AccessDenied'
555                   });
556                }
557             },
558             complete: function() {
559                 self.unblockUI();
560             }
561         });
562     },
563     do_restore: function(form) {
564         var self = this;
565         self.blockUI();
566         $(form).ajaxSubmit({
567             url: '/web/database/restore',
568             type: 'POST',
569             resetForm: true,
570             success: function (body) {
571                 // If empty body, everything went fine
572                 if (!body) { return; }
573
574                 if (body.indexOf('403 Forbidden') !== -1) {
575                     self.display_error({
576                         title: _t("Access Denied"),
577                         error: _t("Incorrect super-administrator password")
578                     });
579                 } else {
580                     self.display_error({
581                         title: _t("Restore Database"),
582                         error: _t("Could not restore the database")
583                     });
584                 }
585             },
586             complete: function() {
587                 self.unblockUI();
588                 self.do_notify(_t("Restored"), _t("Database restored successfully"));
589             }
590         });
591     },
592     do_change_password: function(form) {
593         var self = this;
594         self.rpc("/web/database/change_password", {
595             'fields': $(form).serializeArray()
596         }).done(function(result) {
597             if (result.error) {
598                 self.display_error(result);
599                 return;
600             }
601             self.unblockUI();
602             self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
603         });
604     },
605     do_exit: function () {
606         this.$el.remove();
607         instance.web.redirect('/web');
608     }
609 });
610 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
611
612 instance.web.login = function() {
613     instance.web.redirect('/web/login');
614 };
615 instance.web.client_actions.add("login", "instance.web.login");
616
617 instance.web.logout = function() {
618     instance.web.redirect('/web/session/logout');
619 };
620 instance.web.client_actions.add("logout", "instance.web.logout");
621
622
623 /**
624  * Redirect to url by replacing window.location
625  * If wait is true, sleep 1s and wait for the server i.e. after a restart.
626  */
627 instance.web.redirect = function(url, wait) {
628     // Dont display a dialog if some xmlhttprequest are in progress
629     if (instance.client && instance.client.crashmanager) {
630         instance.client.crashmanager.active = false;
631     }
632
633     var load = function() {
634         var old = "" + window.location;
635         var old_no_hash = old.split("#")[0];
636         var url_no_hash = url.split("#")[0];
637         location.assign(url);
638         if (old_no_hash === url_no_hash) {
639             location.reload(true);
640         }
641     };
642
643     var wait_server = function() {
644         instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
645             setTimeout(wait_server, 250);
646         });
647     };
648
649     if (wait) {
650         setTimeout(wait_server, 1000);
651     } else {
652         load();
653     }
654 };
655
656 /**
657  * Client action to reload the whole interface.
658  * If params.menu_id, it opens the given menu entry.
659  * If params.wait, reload will wait the openerp server to be reachable before reloading
660  */
661 instance.web.Reload = function(parent, action) {
662     var params = action.params || {};
663     var menu_id = params.menu_id || false;
664     var l = window.location;
665
666     var sobj = $.deparam(l.search.substr(1));
667     if (params.url_search) {
668         sobj = _.extend(sobj, params.url_search);
669     }
670     var search = '?' + $.param(sobj);
671
672     var hash = l.hash;
673     if (menu_id) {
674         hash = "#menu_id=" + menu_id;
675     }
676     var url = l.protocol + "//" + l.host + l.pathname + search + hash;
677
678     instance.web.redirect(url, params.wait);
679 };
680 instance.web.client_actions.add("reload", "instance.web.Reload");
681
682 /**
683  * Client action to refresh the session context (making sure
684  * HTTP requests will have the right one) then reload the
685  * whole interface.
686  */
687 instance.web.ReloadContext = function(parent, action) {
688     // side-effect of get_session_info is to refresh the session context
689     instance.session.rpc("/web/session/get_session_info", {}).then(function() {
690         instance.web.Reload(parent, action);
691     });
692 }
693 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
694
695 /**
696  * Client action to go back in breadcrumb history.
697  * If can't go back in history stack, will go back to home.
698  */
699 instance.web.HistoryBack = function(parent) {
700     if (!parent.history_back()) {
701         instance.web.Home(parent);
702     }
703 };
704 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
705
706 /**
707  * Client action to go back home.
708  */
709 instance.web.Home = function(parent, action) {
710     var url = '/' + (window.location.search || '');
711     instance.web.redirect(url, action && action.params && action.params.wait);
712 };
713 instance.web.client_actions.add("home", "instance.web.Home");
714
715 instance.web.ChangePassword =  instance.web.Widget.extend({
716     template: "ChangePassword",
717     start: function() {
718         var self = this;
719         this.getParent().dialog_title = _t("Change Password");
720         var $button = self.$el.find('.oe_form_button');
721         $button.appendTo(this.getParent().$buttons);
722         $button.eq(2).click(function(){
723            self.$el.parents('.modal').modal('hide');
724         });
725         $button.eq(0).click(function(){
726           self.rpc("/web/session/change_password",{
727                'fields': $("form[name=change_password_form]").serializeArray()
728           }).done(function(result) {
729                if (result.error) {
730                   self.display_error(result);
731                   return;
732                } else {
733                    instance.webclient.on_logout();
734                }
735           });
736        });
737     },
738     display_error: function (error) {
739         return new instance.web.Dialog(this, {
740             size: 'medium',
741             title: error.title,
742             buttons: [
743                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
744             ]
745         }, $('<div>').html(error.error)).open();
746     },
747 });
748 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
749
750 instance.web.Menu =  instance.web.Widget.extend({
751     init: function() {
752         var self = this;
753         this._super.apply(this, arguments);
754         this.is_bound = $.Deferred();
755         this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
756         this.data = {data:{children:[]}};
757         this.on("menu_bound", this, function() {
758             // launch the fetch of needaction counters, asynchronous
759             var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
760             var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
761             if (!_.isEmpty(all_menu_ids)) {
762                 this.do_load_needaction(all_menu_ids);
763             }
764         });
765     },
766     start: function() {
767         this._super.apply(this, arguments);
768         return this.bind_menu();
769     },
770     do_reload: function() {
771         var self = this;
772         self.bind_menu();
773     },
774     bind_menu: function() {
775         var self = this;
776         this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
777         this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
778         this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
779         // Hide second level submenus
780         this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
781         if (self.current_menu) {
782             self.open_menu(self.current_menu);
783         }
784         this.trigger('menu_bound');
785
786         var lazyreflow = _.debounce(this.reflow.bind(this), 200);
787         instance.web.bus.on('resize', this, function() {
788             if (parseInt(self.$el.parent().css('width')) <= 768 ) {
789                 lazyreflow('all_outside');
790             } else {
791                 lazyreflow();
792             }
793         });
794         instance.web.bus.trigger('resize');
795
796         this.is_bound.resolve();
797     },
798     do_load_needaction: function (menu_ids) {
799         var self = this;
800         menu_ids = _.compact(menu_ids);
801         if (_.isEmpty(menu_ids)) {
802             return $.when();
803         }
804         return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
805             self.on_needaction_loaded(r);
806         });
807     },
808     on_needaction_loaded: function(data) {
809         var self = this;
810         this.needaction_data = data;
811         _.each(this.needaction_data, function (item, menu_id) {
812             var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
813             $item.find('.badge').remove();
814             if (item.needaction_counter && item.needaction_counter > 0) {
815                 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
816             }
817         });
818     },
819     /**
820      * Reflow the menu items and dock overflowing items into a "More" menu item.
821      * Automatically called when 'menu_bound' event is triggered and on window resizing.
822      *
823      * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
824      * 'all_inside', all the items are hidden under the more item. If not set, only the 
825      * overflowing items are hidden.
826      */
827     reflow: function(behavior) {
828         var self = this;
829         var $more_container = this.$('#menu_more_container').hide();
830         var $more = this.$('#menu_more');
831         var $systray = this.$el.parents().find('.oe_systray');
832
833         $more.children('li').insertBefore($more_container);  // Pull all the items out of the more menu
834         
835         // 'all_outside' beahavior should display all the items, so hide the more menu and exit
836         if (behavior === 'all_outside') {
837             this.$el.find('li').show();
838             $more_container.hide();
839             return;
840         }
841
842         var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
843         $toplevel_items.each(function() {
844             // In all inside mode, we do not compute to know if we must hide the items, we hide them all
845             if (behavior === 'all_inside') {
846                 return false;
847             }
848             var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
849             self.$el.parent().children(':visible').each(function() {
850                 remaining_space -= $(this).outerWidth();
851             });
852
853             if ($(this).width() > remaining_space) {
854                 return false;
855             }
856             $(this).show();
857         });
858         $more.append($toplevel_items.filter(':hidden').show());
859         $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
860         // Hide toplevel item if there is only one
861         var $toplevel = this.$el.children("li:visible");
862         if ($toplevel.length === 1 && behavior != 'all_inside') {
863             $toplevel.hide();
864         }
865     },
866     /**
867      * Opens a given menu by id, as if a user had browsed to that menu by hand
868      * except does not trigger any event on the way
869      *
870      * @param {Number} id database id of the terminal menu to select
871      */
872     open_menu: function (id) {
873         this.current_menu = id;
874         this.session.active_id = id;
875         var $clicked_menu, $sub_menu, $main_menu;
876         $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
877         this.trigger('open_menu', id, $clicked_menu);
878
879         if (this.$secondary_menus.has($clicked_menu).length) {
880             $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
881             $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
882         } else {
883             $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
884             $main_menu = $clicked_menu;
885         }
886
887         // Activate current main menu
888         this.$el.find('.active').removeClass('active');
889         $main_menu.parent().addClass('active');
890
891         // Show current sub menu
892         this.$secondary_menus.find('.oe_secondary_menu').hide();
893         $sub_menu.show();
894
895         // Hide/Show the leftbar menu depending of the presence of sub-items
896         this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
897
898         // Activate current menu item and show parents
899         this.$secondary_menus.find('.active').removeClass('active');
900         if ($main_menu !== $clicked_menu) {
901             $clicked_menu.parents().show();
902             if ($clicked_menu.is('.oe_menu_toggler')) {
903                 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
904             } else {
905                 $clicked_menu.parent().addClass('active');
906             }
907         }
908         // add a tooltip to cropped menu items
909         this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
910             $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'auto right'} :'destroy');
911        });
912     },
913     /**
914      * Call open_menu with the first menu_item matching an action_id
915      *
916      * @param {Number} id the action_id to match
917      */
918     open_action: function (id) {
919         var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
920         var menu_id = $menu.data('menu');
921         if (menu_id) {
922             this.open_menu(menu_id);
923         }
924     },
925     /**
926      * Process a click on a menu item
927      *
928      * @param {Number} id the menu_id
929      * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
930      */
931     menu_click: function(id, needaction) {
932         if (!id) { return; }
933
934         // find back the menuitem in dom to get the action
935         var $item = this.$el.find('a[data-menu=' + id + ']');
936         if (!$item.length) {
937             $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
938         }
939         var action_id = $item.data('action-id');
940         // If first level menu doesnt have action trigger first leaf
941         if (!action_id) {
942             if(this.$el.has($item).length) {
943                 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
944                 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
945                 if($items.length) {
946                     action_id = $items.data('action-id');
947                     id = $items.data('menu');
948                 }
949             }
950         }
951         if (action_id) {
952             this.trigger('menu_click', {
953                 action_id: action_id,
954                 needaction: needaction,
955                 id: id,
956                 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
957             }, $item);
958         }
959         this.open_menu(id);
960     },
961     do_reload_needaction: function () {
962         var self = this;
963         if (self.current_menu) {
964             self.do_load_needaction([self.current_menu]).then(function () {
965                 self.trigger("need_action_reloaded");
966             });
967         }
968     },
969     /**
970      * Jquery event handler for menu click
971      *
972      * @param {Event} ev the jquery event
973      */
974     on_top_menu_click: function(ev) {
975         ev.preventDefault();
976         var self = this;
977         var id = $(ev.currentTarget).data('menu');
978
979         // Fetch the menu leaves ids in order to check if they need a 'needaction'
980         var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
981         var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
982         var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
983
984         self.do_load_needaction(menu_ids).then(function () {
985             self.trigger("need_action_reloaded");
986         });
987
988         this.on_menu_click(ev);
989     },
990     on_menu_click: function(ev) {
991         ev.preventDefault();
992         var needaction = $(ev.target).is('div#menu_counter');
993         this.menu_click($(ev.currentTarget).data('menu'), needaction);
994     },
995 });
996
997 instance.web.UserMenu =  instance.web.Widget.extend({
998     template: "UserMenu",
999     init: function(parent) {
1000         this._super(parent);
1001         this.update_promise = $.Deferred().resolve();
1002     },
1003     start: function() {
1004         var self = this;
1005         this._super.apply(this, arguments);
1006         this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1007             ev.preventDefault();
1008             var f = self['on_menu_' + $(this).data('menu')];
1009             if (f) {
1010                 f($(this));
1011             }
1012         });
1013         this.$el.parent().show()
1014     },
1015     do_update: function () {
1016         var self = this;
1017         var fct = function() {
1018             var $avatar = self.$el.find('.oe_topbar_avatar');
1019             $avatar.attr('src', $avatar.data('default-src'));
1020             if (!self.session.uid)
1021                 return;
1022             var func = new instance.web.Model("res.users").get_func("read");
1023             return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1024                 var topbar_name = res.name;
1025                 if(instance.session.debug)
1026                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1027                 if(res.company_id[0] > 1)
1028                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1029                 self.$el.find('.oe_topbar_name').text(topbar_name);
1030                 if (!instance.session.debug) {
1031                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1032                 }
1033                 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1034                 $avatar.attr('src', avatar_src);
1035
1036                 openerp.web.bus.trigger('resize');  // Re-trigger the reflow logic
1037             });
1038         };
1039         this.update_promise = this.update_promise.then(fct, fct);
1040     },
1041     on_menu_help: function() {
1042         window.open('http://help.openerp.com', '_blank');
1043     },
1044     on_menu_logout: function() {
1045         this.trigger('user_logout');
1046     },
1047     on_menu_settings: function() {
1048         var self = this;
1049         if (!this.getParent().has_uncommitted_changes()) {
1050             self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1051                 result.res_id = instance.session.uid;
1052                 self.getParent().action_manager.do_action(result);
1053             });
1054         }
1055     },
1056     on_menu_account: function() {
1057         var self = this;
1058         if (!this.getParent().has_uncommitted_changes()) {
1059             var P = new instance.web.Model('ir.config_parameter');
1060             P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1061                 var state = {
1062                             'd': instance.session.db,
1063                             'u': window.location.protocol + '//' + window.location.host,
1064                         };
1065                 var params = {
1066                     response_type: 'token',
1067                     client_id: dbuuid || '',
1068                     state: JSON.stringify(state),
1069                     scope: 'userinfo',
1070                 };
1071                 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1072             });
1073         }
1074     },
1075     on_menu_about: function() {
1076         var self = this;
1077         self.rpc("/web/webclient/version_info", {}).done(function(res) {
1078             var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1079             $help.find('a.oe_activate_debug_mode').click(function (e) {
1080                 e.preventDefault();
1081                 window.location = $.param.querystring( window.location.href, 'debug');
1082             });
1083             new instance.web.Dialog(this, {
1084                 size: 'medium',
1085                 dialogClass: 'oe_act_window',
1086                 title: _t("About"),
1087             }, $help).open();
1088         });
1089     },
1090 });
1091
1092 instance.web.FullscreenWidget = instance.web.Widget.extend({
1093     /**
1094      * Widgets extending the FullscreenWidget will be displayed fullscreen,
1095      * and will have a fixed 1:1 zoom level on mobile devices.
1096      */
1097     start: function(){
1098         if(!$('#oe-fullscreenwidget-viewport').length){
1099             $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1100         }
1101         instance.webclient.set_content_full_screen(true);
1102         return this._super();
1103     },
1104     destroy: function(){
1105         instance.webclient.set_content_full_screen(false);
1106         $('#oe-fullscreenwidget-viewport').remove();
1107         return this._super();
1108     },
1109
1110 });
1111
1112 instance.web.Client = instance.web.Widget.extend({
1113     init: function(parent, origin) {
1114         instance.client = instance.webclient = this;
1115         this.client_options = {};
1116         this._super(parent);
1117         this.origin = origin;
1118     },
1119     start: function() {
1120         var self = this;
1121         return instance.session.session_bind(this.origin).then(function() {
1122             self.bind_events();
1123             return self.show_common();
1124         });
1125     },
1126     bind_events: function() {
1127         var self = this;
1128         $('.oe_systray').show();
1129         this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1130             $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1131         });
1132         this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1133             ev.preventDefault();
1134             var $toggle = $(this);
1135             var doc_width = $(document).width();
1136             var $menu = $toggle.siblings('.oe_dropdown_menu');
1137             $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1138             var state = $menu.is('.oe_opened');
1139             setTimeout(function() {
1140                 // Do not alter propagation
1141                 $toggle.add($menu).toggleClass('oe_opened', !state);
1142                 if (!state) {
1143                     // Move $menu if outside window's edge
1144                     var offset = $menu.offset();
1145                     var menu_width = $menu.width();
1146                     var x = doc_width - offset.left - menu_width - 2;
1147                     if (x < 0) {
1148                         $menu.offset({ left: offset.left + x }).width(menu_width);
1149                     }
1150                 }
1151             }, 0);
1152         });
1153         instance.web.bus.on('click', this, function(ev) {
1154             $.fn.tooltip('destroy');
1155             if (!$(ev.target).is('input[type=file]')) {
1156                 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1157             }
1158         });
1159     },
1160     show_common: function() {
1161         var self = this;
1162         this.crashmanager =  new instance.web.CrashManager();
1163         instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1164         self.notification = new instance.web.Notification(this);
1165         self.notification.appendTo(self.$el);
1166         self.loading = new instance.web.Loading(self);
1167         self.loading.appendTo(self.$('.openerp_webclient_container'));
1168         self.action_manager = new instance.web.ActionManager(self);
1169         self.action_manager.appendTo(self.$('.oe_application'));
1170     },
1171     toggle_bars: function(value) {
1172         this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1173     },
1174     has_uncommitted_changes: function() {
1175         return false;
1176     },
1177 });
1178
1179 instance.web.WebClient = instance.web.Client.extend({
1180     init: function(parent, client_options) {
1181         this._super(parent);
1182         if (client_options) {
1183             _.extend(this.client_options, client_options);
1184         }
1185         this._current_state = null;
1186         this.menu_dm = new instance.web.DropMisordered();
1187         this.action_mutex = new $.Mutex();
1188         this.set('title_part', {"zopenerp": "OpenERP"});
1189     },
1190     start: function() {
1191         var self = this;
1192         this.on("change:title_part", this, this._title_changed);
1193         this._title_changed();
1194
1195         return $.when(this._super()).then(function() {
1196             if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1197                 self.to_kitten();
1198             }
1199             if (self.session.session_is_valid()) {
1200                 self.show_application();
1201             }
1202             if (self.client_options.action) {
1203                 self.action_manager.do_action(self.client_options.action);
1204                 delete(self.client_options.action);
1205             }
1206         });
1207     },
1208     to_kitten: function() {
1209         this.kitten = true;
1210         $("body").addClass("kitten-mode-activated");
1211         $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1212         if ($.blockUI) {
1213             var imgkit = Math.floor(Math.random() * 2 + 1);
1214             $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1215         }
1216     },
1217     /**
1218         Sets the first part of the title of the window, dedicated to the current action.
1219     */
1220     set_title: function(title) {
1221         this.set_title_part("action", title);
1222     },
1223     /**
1224         Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1225         a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1226         title of the window separated by '-'.
1227     */
1228     set_title_part: function(part, title) {
1229         var tmp = _.clone(this.get("title_part"));
1230         tmp[part] = title;
1231         this.set("title_part", tmp);
1232     },
1233     _title_changed: function() {
1234         var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1235         var tmp = "";
1236         _.each(parts, function(part) {
1237             var str = this.get("title_part")[part];
1238             if (str) {
1239                 tmp = tmp ? tmp + " - " + str : str;
1240             }
1241         }, this);
1242         document.title = tmp;
1243     },
1244     show_common: function() {
1245         var self = this;
1246         this._super();
1247         window.onerror = function (message, file, line) {
1248             self.crashmanager.show_error({
1249                 type: _t("Client Error"),
1250                 message: message,
1251                 data: {debug: file + ':' + line}
1252             });
1253         };
1254     },
1255     show_application: function() {
1256         var self = this;
1257         self.toggle_bars(true);
1258
1259         self.update_logo();
1260         this.$('.oe_logo_edit_admin').click(function(ev) {
1261             self.logo_edit(ev);
1262         });
1263
1264         // Menu is rendered server-side thus we don't want the widget to create any dom
1265         self.menu = new instance.web.Menu(self);
1266         self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1267         self.menu.start();
1268         self.menu.on('menu_click', this, this.on_menu_action);
1269         self.user_menu = new instance.web.UserMenu(self);
1270         self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1271         self.user_menu.on('user_logout', self, self.on_logout);
1272         self.user_menu.do_update();
1273         self.bind_hashchange();
1274         self.set_title();
1275         self.check_timezone();
1276         if (self.client_options.action_post_login) {
1277             self.action_manager.do_action(self.client_options.action_post_login);
1278             delete(self.client_options.action_post_login);
1279         }
1280     },
1281     update_logo: function() {
1282         var img = this.session.url('/web/binary/company_logo');
1283         this.$('.oe_logo img').attr('src', '').attr('src', img);
1284         this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1285     },
1286     logo_edit: function(ev) {
1287         var self = this;
1288         ev.preventDefault();
1289         self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1290             self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1291                 result.res_id = res['company_id'][0];
1292                 result.target = "new";
1293                 result.views = [[false, 'form']];
1294                 result.flags = {
1295                     action_buttons: true,
1296                 };
1297                 self.action_manager.do_action(result);
1298                 var form = self.action_manager.dialog_widget.views.form.controller;
1299                 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1300                 form.on('record_saved', self, function() {
1301                     self.action_manager.dialog_stop();
1302                     self.update_logo();
1303                 });
1304             });
1305         });
1306         return false;
1307     },
1308     check_timezone: function() {
1309         var self = this;
1310         return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1311             var user_offset = result[0]['tz_offset'];
1312             var offset = -(new Date().getTimezoneOffset());
1313             // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1314             var browser_offset = (offset < 0) ? "-" : "+";
1315             browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1316             browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1317             if (browser_offset !== user_offset) {
1318                 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1319                 $icon.on('click', function() {
1320                     var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1321                         user_timezone: instance.session.user_context.tz || 'UTC',
1322                         user_offset: user_offset,
1323                         browser_offset: browser_offset,
1324                     }), true);
1325                     notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1326                         notification.close();
1327                     }).find('a').on('click', function() {
1328                         notification.close();
1329                         self.user_menu.on_menu_settings();
1330                         return false;
1331                     });
1332                 });
1333                 $icon.prependTo(window.$('.oe_systray'));
1334             }
1335         });
1336     },
1337     destroy_content: function() {
1338         _.each(_.clone(this.getChildren()), function(el) {
1339             el.destroy();
1340         });
1341         this.$el.children().remove();
1342     },
1343     do_reload: function() {
1344         var self = this;
1345         return this.session.session_reload().then(function () {
1346             instance.session.load_modules(true).then(
1347                 self.menu.proxy('do_reload')); });
1348     },
1349     do_notify: function() {
1350         var n = this.notification;
1351         return n.notify.apply(n, arguments);
1352     },
1353     do_warn: function() {
1354         var n = this.notification;
1355         return n.warn.apply(n, arguments);
1356     },
1357     on_logout: function() {
1358         var self = this;
1359         if (!this.has_uncommitted_changes()) {
1360             self.action_manager.do_action('logout');
1361         }
1362     },
1363     bind_hashchange: function() {
1364         var self = this;
1365         $(window).bind('hashchange', this.on_hashchange);
1366
1367         var state = $.bbq.getState(true);
1368         if (_.isEmpty(state) || state.action == "login") {
1369             self.menu.is_bound.done(function() {
1370                 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1371                     if(data.action_id) {
1372                         self.action_manager.do_action(data.action_id[0]);
1373                         self.menu.open_action(data.action_id[0]);
1374                     } else {
1375                         var first_menu_id = self.menu.$el.find("a:first").data("menu");
1376                         if(first_menu_id) {
1377                             self.menu.menu_click(first_menu_id);
1378                         }
1379                     }
1380                 });
1381             });
1382         } else {
1383             $(window).trigger('hashchange');
1384         }
1385     },
1386     on_hashchange: function(event) {
1387         var self = this;
1388         var stringstate = event.getState(false);
1389         if (!_.isEqual(this._current_state, stringstate)) {
1390             var state = event.getState(true);
1391             if(!state.action && state.menu_id) {
1392                 self.menu.is_bound.done(function() {
1393                     self.menu.menu_click(state.menu_id);
1394                 });
1395             } else {
1396                 state._push_me = false;  // no need to push state back...
1397                 this.action_manager.do_load_state(state, !!this._current_state);
1398             }
1399         }
1400         this._current_state = stringstate;
1401     },
1402     do_push_state: function(state) {
1403         this.set_title(state.title);
1404         delete state.title;
1405         var url = '#' + $.param(state);
1406         this._current_state = $.deparam($.param(state), false);     // stringify all values
1407         $.bbq.pushState(url);
1408         this.trigger('state_pushed', state);
1409     },
1410     on_menu_action: function(options) {
1411         var self = this;
1412         return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1413             .then(function (result) {
1414                 return self.action_mutex.exec(function() {
1415                     if (options.needaction) {
1416                         result.context = new instance.web.CompoundContext(result.context, {
1417                             search_default_message_unread: true,
1418                             search_disable_custom_filters: true,
1419                         });
1420                     }
1421                     var completed = $.Deferred();
1422                     $.when(self.action_manager.do_action(result, {
1423                         clear_breadcrumbs: true,
1424                         action_menu_id: self.menu.current_menu,
1425                     })).fail(function() {
1426                         self.menu.open_menu(options.previous_menu_id);
1427                     }).always(function() {
1428                         completed.resolve();
1429                     });
1430                     setTimeout(function() {
1431                         completed.resolve();
1432                     }, 2000);
1433                     // We block the menu when clicking on an element until the action has correctly finished
1434                     // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1435                     return completed;
1436                 });
1437             });
1438     },
1439     set_content_full_screen: function(fullscreen) {
1440         $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1441         this.$('.oe_webclient').toggleClass(
1442             'oe_content_full_screen', fullscreen);
1443     },
1444     has_uncommitted_changes: function() {
1445         var $e = $.Event('clear_uncommitted_changes');
1446         instance.web.bus.trigger('clear_uncommitted_changes', $e);
1447         if ($e.isDefaultPrevented()) {
1448             return true;
1449         } else {
1450             return this._super.apply(this, arguments);
1451         }
1452     },
1453 });
1454
1455 instance.web.EmbeddedClient = instance.web.Client.extend({
1456     _template: 'EmbedClient',
1457     init: function(parent, origin, dbname, login, key, action_id, options) {
1458         this._super(parent, origin);
1459         this.bind_credentials(dbname, login, key);
1460         this.action_id = action_id;
1461         this.options = options || {};
1462     },
1463     start: function() {
1464         var self = this;
1465         return $.when(this._super()).then(function() {
1466             return self.authenticate().then(function() {
1467                 if (!self.action_id) {
1468                     return;
1469                 }
1470                 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1471                     var action = result;
1472                     action.flags = _.extend({
1473                         //views_switcher : false,
1474                         search_view : false,
1475                         action_buttons : false,
1476                         sidebar : false
1477                         //pager : false
1478                     }, self.options, action.flags || {});
1479
1480                     self.do_action(action);
1481                 });
1482             });
1483         });
1484     },
1485
1486     do_action: function(/*...*/) {
1487         var am = this.action_manager;
1488         return am.do_action.apply(am, arguments);
1489     },
1490
1491     authenticate: function() {
1492         var s = instance.session;
1493         if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1494             return $.when();
1495         }
1496         return instance.session.session_authenticate(this.dbname, this.login, this.key);
1497     },
1498
1499     bind_credentials: function(dbname, login, key) {
1500         this.dbname = dbname;
1501         this.login = login;
1502         this.key = key;
1503     },
1504
1505 });
1506
1507 instance.web.embed = function (origin, dbname, login, key, action, options) {
1508     $('head').append($('<link>', {
1509         'rel': 'stylesheet',
1510         'type': 'text/css',
1511         'href': origin +'/web/css/web.assets_webclient'
1512     }));
1513     var currentScript = document.currentScript;
1514     if (!currentScript) {
1515         var sc = document.getElementsByTagName('script');
1516         currentScript = sc[sc.length-1];
1517     }
1518     var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1519     client.insertAfter(currentScript);
1520 };
1521
1522 })();
1523
1524 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: