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