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