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