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