1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
6 var instance = openerp;
7 openerp.web.chrome = {};
9 var QWeb = instance.web.qweb,
12 instance.web.Notification = instance.web.Widget.extend({
13 template: 'Notification',
15 this._super.apply(this, arguments);
16 instance.web.notification = this;
19 this._super.apply(this, arguments);
25 notify: function(title, text, sticky) {
31 return this.$el.notify('create', {
36 warn: function(title, text, sticky) {
42 return this.$el.notify('create', 'oe_notification_alert', {
49 var opened_modal = [];
51 instance.web.action_notify = function(element, action) {
52 element.do_notify(action.params.title, action.params.text, action.params.sticky);
54 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
56 instance.web.action_warn = function(element, action) {
57 element.do_warn(action.params.title, action.params.text, action.params.sticky);
59 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
62 A useful class to handle dialogs.
65 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
66 during the lifecycle of the dialog.
68 instance.web.Dialog = instance.web.Widget.extend({
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 .
86 init: function (parent, options, content) {
89 this.content_to_set = content;
90 this.dialog_options = {
91 destroy_on_close: true,
92 size: 'large', //'medium', 'small'
96 _.extend(this.dialog_options, options);
98 this.on("closing", this, this._closing);
99 this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
101 renderElement: function() {
102 if (this.content_to_set) {
103 this.setElement(this.content_to_set);
104 } else if (this.template) {
109 Opens the popup. Inits the dialog if it is not already inited.
114 if (!this.dialog_inited) {
117 this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
118 $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal is opened
119 //add to list of currently opened modal
120 opened_modal.push(this.$dialog_box);
123 _add_buttons: function(buttons) {
125 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
126 _.each(buttons, function(fn, text) {
127 // buttons can be object or array
128 var pre_text = fn.pre_text || "";
129 var post_text = fn.post_text || "";
130 var oe_link_class = fn.oe_link_class;
131 if (!_.isFunction(fn)) {
135 var $but = $(QWeb.render('WidgetButton', { widget : { pre_text: pre_text, post_text: post_text, string: text, node: { attrs: {'class': oe_link_class} }}}));
136 $customButons.append($but);
137 $but.filter('button').on('click', function(ev) {
138 fn.call(self.$el, ev);
143 Initializes the popup.
145 @return The result returned by start().
147 init_dialog: function() {
149 var options = _.extend({}, this.dialog_options);
150 options.title = options.title || this.dialog_title;
151 if (options.buttons) {
152 this._add_buttons(options.buttons);
153 delete(options.buttons);
155 this.renderElement();
156 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
161 if (options.size !== 'large'){
162 var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
163 if (options.size === 'small'){
164 dialog_class_size.addClass('modal-sm');
168 this.$el.appendTo(this.$dialog_box.find(".modal-body"));
169 var $dialog_content = this.$dialog_box.find('.modal-content');
170 if (options.dialogClass){
171 $dialog_content.find(".modal-body").addClass(options.dialogClass);
173 $dialog_content.openerpClass();
175 this.$dialog_box.on('hidden.bs.modal', this, function() {
178 this.$dialog_box.modal('show');
180 this.dialog_inited = true;
181 var res = this.start();
185 Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
187 close: function(reason) {
188 if (this.dialog_inited && !this.__tmp_dialog_hiding) {
189 $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
190 if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal
191 this.__tmp_dialog_hiding = true;
192 this.$dialog_box.modal('hide');
193 this.__tmp_dialog_hiding = undefined;
195 this.trigger("closing", reason);
198 _closing: function() {
199 if (this.__tmp_dialog_destroying)
201 if (this.dialog_options.destroy_on_close) {
202 this.__tmp_dialog_closing = true;
204 this.__tmp_dialog_closing = undefined;
208 Destroys the popup, also closes it.
210 destroy: function (reason) {
211 this.$buttons.remove();
213 _.each(this.getChildren(), function(el) {
216 if (! this.__tmp_dialog_closing) {
217 this.__tmp_dialog_destroying = true;
219 this.__tmp_dialog_destroying = undefined;
221 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
222 //we need this to put the instruction to remove modal from DOM at the end
223 //of the queue, otherwise it might already have been removed before the modal-backdrop
224 //is removed when pressing escape key
225 var $element = this.$dialog_box;
226 setTimeout(function () {
227 //remove modal from list of opened modal since we just destroy it
228 var modal_list_index = $.inArray($element, opened_modal);
229 if (modal_list_index > -1){
230 opened_modal.splice(modal_list_index,1)[0].remove();
232 if (opened_modal.length > 0){
233 //we still have other opened modal so we should focus it
234 opened_modal[opened_modal.length-1].focus();
235 //keep class modal-open (deleted by bootstrap hide fnct) on body
236 //to allow scrolling inside the modal
237 $('body').addClass('modal-open');
245 instance.web.CrashManager = instance.web.Class.extend({
250 rpc_error: function(error) {
254 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
256 new (handler)(this, error).display();
259 if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
260 this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
263 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
264 this.show_warning(error);
266 this.show_error(error);
269 show_warning: function(error) {
273 if (error.data.exception_type === "except_osv") {
274 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
276 new instance.web.Dialog(this, {
278 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
280 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
282 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
284 show_error: function(error) {
289 buttons[_t("Ok")] = function() {
290 this.parents('.modal').modal('hide');
292 new instance.web.Dialog(this, {
293 title: "Odoo " + _.str.capitalize(error.type),
295 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
297 show_message: function(exception) {
299 type: _t("Client Error"),
307 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
309 instance.web.ExceptionHandler = {
311 @param parent The parent.
312 @param error The error object as returned by the JSON-RPC implementation.
314 init: function(parent, error) {},
316 Called to inform to display the widget, if necessary. A typical way would be to implement
317 this interface in a class extending instance.web.Dialog and simply display the dialog in this
320 display: function() {},
324 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
325 instance.web.ExceptionHandler.
327 instance.web.crash_manager_registry = new instance.web.Registry();
330 * Handle redirection warnings, which behave more or less like a regular
331 * warning, with an additional redirection button.
333 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
334 init: function(parent, error) {
338 display: function() {
341 error.data.message = error.data.arguments[0];
343 new instance.web.Dialog(this, {
345 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
347 {text: error.data.arguments[2],
348 oe_link_class : 'oe_highlight',
349 post_text : _t("or"),
351 window.location.href='#action='+error.data.arguments[1]
354 {text: _t("Cancel"), oe_link_class: 'oe_link', click: function() { self.$el.parents('.modal').modal('hide'); self.destroy();}}
356 }, QWeb.render('CrashManager.warning', {error: error})).open();
359 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
361 instance.web.Loading = instance.web.Widget.extend({
362 template: _t("Loading"),
363 init: function(parent) {
366 this.blocked_ui = false;
367 this.session.on("request", this, this.request_call);
368 this.session.on("response", this, this.response_call);
369 this.session.on("response_failed", this, this.response_call);
371 destroy: function() {
372 this.on_rpc_event(-this.count);
375 request_call: function() {
376 this.on_rpc_event(1);
378 response_call: function() {
379 this.on_rpc_event(-1);
381 on_rpc_event : function(increment) {
383 if (!this.count && increment === 1) {
385 this.long_running_timer = setTimeout(function () {
386 self.blocked_ui = true;
387 instance.web.blockUI();
391 this.count += increment;
392 if (this.count > 0) {
393 if (instance.session.debug) {
394 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
396 this.$el.text(_t("Loading"));
399 this.getParent().$el.addClass('oe_wait');
402 clearTimeout(this.long_running_timer);
403 // Don't unblock if blocked by somebody else
404 if (self.blocked_ui) {
405 this.blocked_ui = false;
406 instance.web.unblockUI();
409 this.getParent().$el.removeClass('oe_wait');
414 instance.web.DatabaseManager = instance.web.Widget.extend({
415 init: function(parent) {
417 this.unblockUIFunction = instance.web.unblockUI;
418 $.validator.addMethod('matches', function (s, _, re) {
419 return new RegExp(re).test(s);
420 }, _t("Invalid database name"));
424 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
425 var fetch_db = this.rpc("/web/database/get_list", {}).then(
427 self.db_list = result;
433 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
434 self.lang_list = result;
436 return $.when(fetch_db, fetch_langs).always(self.do_render);
438 do_render: function() {
440 instance.webclient.toggle_bars(true);
441 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
442 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
443 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
444 $('ul.oe_secondary_submenu > li:first').addClass('active');
445 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
446 var menuitem = $(this);
447 menuitem.addClass('active').siblings().removeClass('active');
448 var form_id =menuitem.find('a').attr('href');
449 $(form_id).show().siblings().hide();
450 event.preventDefault();
452 $('#back-to-login').click(self.do_exit);
453 self.$el.find("td").addClass("oe_form_group_cell");
454 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
455 self.$el.find("label").addClass("oe_form_label");
456 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
457 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
458 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
459 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
460 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
461 self.$el.find("form[name=change_pwd_form]").validate({
463 old_pwd: _t("Please enter your previous password"),
464 new_pwd: _t("Please enter your new password"),
466 required: _t("Please confirm your new password"),
467 equalTo: _t("The confirmation does not match the password")
470 submitHandler: self.do_change_password
473 destroy: function () {
474 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
478 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
479 * from unblocking the UI
481 blockUI: function () {
482 instance.web.blockUI();
483 instance.web.unblockUI = function () {};
486 * Reinstates $.unblockUI so third parties can play with blockUI, and
489 unblockUI: function () {
490 instance.web.unblockUI = this.unblockUIFunction;
491 instance.web.unblockUI();
494 * Displays an error dialog resulting from the various RPC communications
495 * failing over themselves
497 * @param {Object} error error description
498 * @param {String} error.title title of the error dialog
499 * @param {String} error.error message of the error dialog
501 display_error: function (error) {
502 return new instance.web.Dialog(this, {
506 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
508 }, $('<div>').html(error.error)).open();
510 do_create: function(form) {
512 var fields = $(form).serializeArray();
513 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
515 instance.web.redirect('/web');
517 alert("Failed to create database");
521 do_duplicate: function(form) {
523 var fields = $(form).serializeArray();
524 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
526 self.display_error(result);
529 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
533 do_drop: function(form) {
536 fields = $form.serializeArray(),
537 $db_list = $form.find('[name=drop_db]'),
539 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
542 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
544 self.display_error(result);
547 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
551 do_backup: function(form) {
554 self.session.get_file({
556 success: function () {
557 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
559 error: function(error){
562 title: _t("Backup Database"),
563 error: 'AccessDenied'
567 complete: function() {
572 do_restore: function(form) {
576 url: '/web/database/restore',
579 success: function (body) {
580 // If empty body, everything went fine
581 if (!body) { return; }
583 if (body.indexOf('403 Forbidden') !== -1) {
585 title: _t("Access Denied"),
586 error: _t("Incorrect super-administrator password")
590 title: _t("Restore Database"),
591 error: _t("Could not restore the database")
595 complete: function() {
597 self.do_notify(_t("Restored"), _t("Database restored successfully"));
601 do_change_password: function(form) {
603 self.rpc("/web/database/change_password", {
604 'fields': $(form).serializeArray()
605 }).done(function(result) {
607 self.display_error(result);
611 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
614 do_exit: function () {
616 instance.web.redirect('/web');
619 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
621 instance.web.login = function() {
622 instance.web.redirect('/web/login');
624 instance.web.client_actions.add("login", "instance.web.login");
626 instance.web.logout = function() {
627 instance.web.redirect('/web/session/logout');
629 instance.web.client_actions.add("logout", "instance.web.logout");
633 * Redirect to url by replacing window.location
634 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
636 instance.web.redirect = function(url, wait) {
637 // Dont display a dialog if some xmlhttprequest are in progress
638 if (instance.client && instance.client.crashmanager) {
639 instance.client.crashmanager.active = false;
642 var load = function() {
643 var old = "" + window.location;
644 var old_no_hash = old.split("#")[0];
645 var url_no_hash = url.split("#")[0];
646 location.assign(url);
647 if (old_no_hash === url_no_hash) {
648 location.reload(true);
652 var wait_server = function() {
653 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
654 setTimeout(wait_server, 250);
659 setTimeout(wait_server, 1000);
666 * Client action to reload the whole interface.
667 * If params.menu_id, it opens the given menu entry.
668 * If params.wait, reload will wait the openerp server to be reachable before reloading
670 instance.web.Reload = function(parent, action) {
671 var params = action.params || {};
672 var menu_id = params.menu_id || false;
673 var l = window.location;
675 var sobj = $.deparam(l.search.substr(1));
676 if (params.url_search) {
677 sobj = _.extend(sobj, params.url_search);
679 var search = '?' + $.param(sobj);
683 hash = "#menu_id=" + menu_id;
685 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
687 instance.web.redirect(url, params.wait);
689 instance.web.client_actions.add("reload", "instance.web.Reload");
692 * Client action to refresh the session context (making sure
693 * HTTP requests will have the right one) then reload the
696 instance.web.ReloadContext = function(parent, action) {
697 // side-effect of get_session_info is to refresh the session context
698 instance.session.rpc("/web/session/get_session_info", {}).then(function() {
699 instance.web.Reload(parent, action);
702 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
705 * Client action to go back in breadcrumb history.
706 * If can't go back in history stack, will go back to home.
708 instance.web.HistoryBack = function(parent) {
709 if (!parent.history_back()) {
710 instance.web.Home(parent);
713 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
716 * Client action to go back home.
718 instance.web.Home = function(parent, action) {
719 var url = '/' + (window.location.search || '');
720 instance.web.redirect(url, action && action.params && action.params.wait);
722 instance.web.client_actions.add("home", "instance.web.Home");
724 instance.web.ChangePassword = instance.web.Widget.extend({
725 template: "ChangePassword",
728 this.getParent().dialog_title = _t("Change Password");
729 var $button = self.$el.find('.oe_form_button');
730 $button.appendTo(this.getParent().$buttons);
731 $button.eq(2).click(function(){
732 self.$el.parents('.modal').modal('hide');
734 $button.eq(0).click(function(){
735 self.rpc("/web/session/change_password",{
736 'fields': $("form[name=change_password_form]").serializeArray()
737 }).done(function(result) {
739 self.display_error(result);
742 instance.webclient.on_logout();
747 display_error: function (error) {
748 return new instance.web.Dialog(this, {
752 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
754 }, $('<div>').html(error.error)).open();
757 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
759 instance.web.Menu = instance.web.Widget.extend({
762 this._super.apply(this, arguments);
763 this.is_bound = $.Deferred();
764 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
765 this.data = {data:{children:[]}};
766 this.on("menu_bound", this, function() {
767 // launch the fetch of needaction counters, asynchronous
768 var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
769 var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
770 if (!_.isEmpty(all_menu_ids)) {
771 this.do_load_needaction(all_menu_ids);
776 this._super.apply(this, arguments);
777 return this.bind_menu();
779 do_reload: function() {
783 bind_menu: function() {
785 this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
786 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
787 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
788 // Hide second level submenus
789 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
790 if (self.current_menu) {
791 self.open_menu(self.current_menu);
793 this.trigger('menu_bound');
795 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
796 instance.web.bus.on('resize', this, function() {
797 if (parseInt(self.$el.parent().css('width')) <= 768 ) {
798 lazyreflow('all_outside');
803 instance.web.bus.trigger('resize');
805 this.is_bound.resolve();
807 do_load_needaction: function (menu_ids) {
809 menu_ids = _.compact(menu_ids);
810 if (_.isEmpty(menu_ids)) {
813 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
814 self.on_needaction_loaded(r);
817 on_needaction_loaded: function(data) {
819 this.needaction_data = data;
820 _.each(this.needaction_data, function (item, menu_id) {
821 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
822 $item.find('.badge').remove();
823 if (item.needaction_counter && item.needaction_counter > 0) {
824 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
829 * Reflow the menu items and dock overflowing items into a "More" menu item.
830 * Automatically called when 'menu_bound' event is triggered and on window resizing.
832 * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
833 * 'all_inside', all the items are hidden under the more item. If not set, only the
834 * overflowing items are hidden.
836 reflow: function(behavior) {
838 var $more_container = this.$('#menu_more_container').hide();
839 var $more = this.$('#menu_more');
840 var $systray = this.$el.parents().find('.oe_systray');
842 $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
844 // 'all_outside' beahavior should display all the items, so hide the more menu and exit
845 if (behavior === 'all_outside') {
846 this.$el.find('li').show();
847 $more_container.hide();
851 var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
852 $toplevel_items.each(function() {
853 // In all inside mode, we do not compute to know if we must hide the items, we hide them all
854 if (behavior === 'all_inside') {
857 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
858 self.$el.parent().children(':visible').each(function() {
859 remaining_space -= $(this).outerWidth();
862 if ($(this).width() > remaining_space) {
867 $more.append($toplevel_items.filter(':hidden').show());
868 $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
869 // Hide toplevel item if there is only one
870 var $toplevel = this.$el.children("li:visible");
871 if ($toplevel.length === 1 && behavior != 'all_inside') {
876 * Opens a given menu by id, as if a user had browsed to that menu by hand
877 * except does not trigger any event on the way
879 * @param {Number} id database id of the terminal menu to select
881 open_menu: function (id) {
882 this.current_menu = id;
883 this.session.active_id = id;
884 var $clicked_menu, $sub_menu, $main_menu;
885 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
886 this.trigger('open_menu', id, $clicked_menu);
888 if (this.$secondary_menus.has($clicked_menu).length) {
889 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
890 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
892 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
893 $main_menu = $clicked_menu;
896 // Activate current main menu
897 this.$el.find('.active').removeClass('active');
898 $main_menu.parent().addClass('active');
900 // Show current sub menu
901 this.$secondary_menus.find('.oe_secondary_menu').hide();
904 // Hide/Show the leftbar menu depending of the presence of sub-items
905 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
907 // Activate current menu item and show parents
908 this.$secondary_menus.find('.active').removeClass('active');
909 if ($main_menu !== $clicked_menu) {
910 $clicked_menu.parents().show();
911 if ($clicked_menu.is('.oe_menu_toggler')) {
912 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
914 $clicked_menu.parent().addClass('active');
917 // add a tooltip to cropped menu items
918 this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
919 $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
923 * Call open_menu with the first menu_item matching an action_id
925 * @param {Number} id the action_id to match
927 open_action: function (id) {
928 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
929 var menu_id = $menu.data('menu');
931 this.open_menu(menu_id);
935 * Process a click on a menu item
937 * @param {Number} id the menu_id
938 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
940 menu_click: function(id, needaction) {
943 // find back the menuitem in dom to get the action
944 var $item = this.$el.find('a[data-menu=' + id + ']');
946 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
948 var action_id = $item.data('action-id');
949 // If first level menu doesnt have action trigger first leaf
951 if(this.$el.has($item).length) {
952 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
953 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
955 action_id = $items.data('action-id');
956 id = $items.data('menu');
961 this.trigger('menu_click', {
962 action_id: action_id,
963 needaction: needaction,
965 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
968 console.log('Menu no action found web test 04 will fail');
972 do_reload_needaction: function () {
974 if (self.current_menu) {
975 self.do_load_needaction([self.current_menu]).then(function () {
976 self.trigger("need_action_reloaded");
981 * Jquery event handler for menu click
983 * @param {Event} ev the jquery event
985 on_top_menu_click: function(ev) {
988 var id = $(ev.currentTarget).data('menu');
990 // Fetch the menu leaves ids in order to check if they need a 'needaction'
991 var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
992 var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
993 var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
995 self.do_load_needaction(menu_ids).then(function () {
996 self.trigger("need_action_reloaded");
998 this.$el.parents().find(".oe_secondary_menus_container").scrollTop(0,0);
1000 this.on_menu_click(ev);
1002 on_menu_click: function(ev) {
1003 ev.preventDefault();
1004 var needaction = $(ev.target).is('div#menu_counter');
1005 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1009 instance.web.UserMenu = instance.web.Widget.extend({
1010 template: "UserMenu",
1011 init: function(parent) {
1012 this._super(parent);
1013 this.update_promise = $.Deferred().resolve();
1017 this._super.apply(this, arguments);
1018 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1019 ev.preventDefault();
1020 var f = self['on_menu_' + $(this).data('menu')];
1025 this.$el.parent().show()
1027 do_update: function () {
1029 var fct = function() {
1030 var $avatar = self.$el.find('.oe_topbar_avatar');
1031 $avatar.attr('src', $avatar.data('default-src'));
1032 if (!self.session.uid)
1034 var func = new instance.web.Model("res.users").get_func("read");
1035 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1036 var topbar_name = res.name;
1037 if(instance.session.debug)
1038 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1039 if(res.company_id[0] > 1)
1040 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1041 self.$el.find('.oe_topbar_name').text(topbar_name);
1042 if (!instance.session.debug) {
1043 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1045 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1046 $avatar.attr('src', avatar_src);
1048 openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic
1051 this.update_promise = this.update_promise.then(fct, fct);
1053 on_menu_help: function() {
1054 window.open('http://help.odoo.com', '_blank');
1056 on_menu_logout: function() {
1057 this.trigger('user_logout');
1059 on_menu_settings: function() {
1061 if (!this.getParent().has_uncommitted_changes()) {
1062 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1063 result.res_id = instance.session.uid;
1064 self.getParent().action_manager.do_action(result);
1068 on_menu_account: function() {
1070 if (!this.getParent().has_uncommitted_changes()) {
1071 var P = new instance.web.Model('ir.config_parameter');
1072 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1074 'd': instance.session.db,
1075 'u': window.location.protocol + '//' + window.location.host,
1078 response_type: 'token',
1079 client_id: dbuuid || '',
1080 state: JSON.stringify(state),
1083 instance.web.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params));
1084 }).fail(function(result, ev){
1085 ev.preventDefault();
1086 instance.web.redirect('https://accounts.odoo.com/web');
1090 on_menu_about: function() {
1092 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1093 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1094 $help.find('a.oe_activate_debug_mode').click(function (e) {
1096 window.location = $.param.querystring( window.location.href, 'debug');
1098 new instance.web.Dialog(this, {
1100 dialogClass: 'oe_act_window',
1107 instance.web.FullscreenWidget = instance.web.Widget.extend({
1109 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1110 * and will have a fixed 1:1 zoom level on mobile devices.
1113 if(!$('#oe-fullscreenwidget-viewport').length){
1114 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1116 instance.webclient.set_content_full_screen(true);
1117 return this._super();
1119 destroy: function(){
1120 instance.webclient.set_content_full_screen(false);
1121 $('#oe-fullscreenwidget-viewport').remove();
1122 return this._super();
1127 instance.web.Client = instance.web.Widget.extend({
1128 init: function(parent, origin) {
1129 instance.client = instance.webclient = this;
1130 this.client_options = {};
1131 this._super(parent);
1132 this.origin = origin;
1136 return instance.session.session_bind(this.origin).then(function() {
1138 return self.show_common();
1141 bind_events: function() {
1143 $('.oe_systray').show();
1144 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1145 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1147 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1148 ev.preventDefault();
1149 var $toggle = $(this);
1150 var doc_width = $(document).width();
1151 var $menu = $toggle.siblings('.oe_dropdown_menu');
1152 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1153 var state = $menu.is('.oe_opened');
1154 setTimeout(function() {
1155 // Do not alter propagation
1156 $toggle.add($menu).toggleClass('oe_opened', !state);
1158 // Move $menu if outside window's edge
1159 var offset = $menu.offset();
1160 var menu_width = $menu.width();
1161 var x = doc_width - offset.left - menu_width - 2;
1163 $menu.offset({ left: offset.left + x }).width(menu_width);
1168 instance.web.bus.on('click', this, function(ev) {
1169 $('.tooltip').remove();
1170 if (!$(ev.target).is('input[type=file]')) {
1171 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1175 on_logo_click: function(ev){
1176 if (!this.has_uncommitted_changes()) {
1179 ev.preventDefault();
1182 show_common: function() {
1184 this.crashmanager = new instance.web.CrashManager();
1185 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1186 self.notification = new instance.web.Notification(this);
1187 self.notification.appendTo(self.$el);
1188 self.loading = new instance.web.Loading(self);
1189 self.loading.appendTo(self.$('.openerp_webclient_container'));
1190 self.action_manager = new instance.web.ActionManager(self);
1191 self.action_manager.replace(self.$('.oe_application'));
1193 toggle_bars: function(value) {
1194 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1196 has_uncommitted_changes: function() {
1201 instance.web.WebClient = instance.web.Client.extend({
1202 init: function(parent, client_options) {
1203 this._super(parent);
1204 if (client_options) {
1205 _.extend(this.client_options, client_options);
1207 this._current_state = null;
1208 this.menu_dm = new instance.web.DropMisordered();
1209 this.action_mutex = new $.Mutex();
1210 this.set('title_part', {"zopenerp": "Odoo"});
1214 this.on("change:title_part", this, this._title_changed);
1215 this._title_changed();
1217 return $.when(this._super()).then(function() {
1218 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1221 if (self.session.session_is_valid()) {
1222 self.show_application();
1224 if (self.client_options.action) {
1225 self.action_manager.do_action(self.client_options.action);
1226 delete(self.client_options.action);
1230 to_kitten: function() {
1232 $("body").addClass("kitten-mode-activated");
1233 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1235 var imgkit = Math.floor(Math.random() * 2 + 1);
1236 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1240 Sets the first part of the title of the window, dedicated to the current action.
1242 set_title: function(title) {
1243 this.set_title_part("action", title);
1246 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1247 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1248 title of the window separated by '-'.
1250 set_title_part: function(part, title) {
1251 var tmp = _.clone(this.get("title_part"));
1253 this.set("title_part", tmp);
1255 _title_changed: function() {
1256 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1258 _.each(parts, function(part) {
1259 var str = this.get("title_part")[part];
1261 tmp = tmp ? tmp + " - " + str : str;
1264 document.title = tmp;
1266 show_common: function() {
1269 window.onerror = function (message, file, line) {
1270 self.crashmanager.show_error({
1271 type: _t("Client Error"),
1273 data: {debug: file + ':' + line}
1277 show_application: function() {
1279 self.toggle_bars(true);
1282 this.$('.oe_logo_edit_admin').click(function(ev) {
1286 this.$('.oe_logo img').click(function(ev) {
1287 self.on_logo_click(ev);
1289 // Menu is rendered server-side thus we don't want the widget to create any dom
1290 self.menu = new instance.web.Menu(self);
1291 self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1293 self.menu.on('menu_click', this, this.on_menu_action);
1294 self.user_menu = new instance.web.UserMenu(self);
1295 self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1296 self.user_menu.on('user_logout', self, self.on_logout);
1297 self.user_menu.do_update();
1298 self.bind_hashchange();
1300 self.check_timezone();
1301 if (self.client_options.action_post_login) {
1302 self.action_manager.do_action(self.client_options.action_post_login);
1303 delete(self.client_options.action_post_login);
1306 update_logo: function() {
1307 var company = this.session.company_id;
1308 var img = this.session.url('/web/binary/company_logo' + (company ? '?company=' + company : ''));
1309 this.$('.oe_logo img').attr('src', '').attr('src', img);
1310 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1312 logo_edit: function(ev) {
1314 ev.preventDefault();
1315 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1316 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1317 result.res_id = res['company_id'][0];
1318 result.target = "new";
1319 result.views = [[false, 'form']];
1321 action_buttons: true,
1323 self.action_manager.do_action(result);
1324 var form = self.action_manager.dialog_widget.views.form.controller;
1325 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1326 form.on('record_saved', self, function() {
1327 self.action_manager.dialog_stop();
1334 check_timezone: function() {
1336 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1337 var user_offset = result[0]['tz_offset'];
1338 var offset = -(new Date().getTimezoneOffset());
1339 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1340 var browser_offset = (offset < 0) ? "-" : "+";
1341 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1342 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1343 if (browser_offset !== user_offset) {
1344 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1345 $icon.on('click', function() {
1346 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1347 user_timezone: instance.session.user_context.tz || 'UTC',
1348 user_offset: user_offset,
1349 browser_offset: browser_offset,
1351 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1352 notification.close();
1353 }).find('a').on('click', function() {
1354 notification.close();
1355 self.user_menu.on_menu_settings();
1359 $icon.prependTo(window.$('.oe_systray'));
1363 destroy_content: function() {
1364 _.each(_.clone(this.getChildren()), function(el) {
1367 this.$el.children().remove();
1369 do_reload: function() {
1371 return this.session.session_reload().then(function () {
1372 instance.session.load_modules(true).then(
1373 self.menu.proxy('do_reload')); });
1375 do_notify: function() {
1376 var n = this.notification;
1377 return n.notify.apply(n, arguments);
1379 do_warn: function() {
1380 var n = this.notification;
1381 return n.warn.apply(n, arguments);
1383 on_logout: function() {
1385 if (!this.has_uncommitted_changes()) {
1386 self.action_manager.do_action('logout');
1389 bind_hashchange: function() {
1391 $(window).bind('hashchange', this.on_hashchange);
1393 var state = $.bbq.getState(true);
1394 if (_.isEmpty(state) || state.action == "login") {
1395 self.menu.is_bound.done(function() {
1396 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1397 if(data.action_id) {
1398 self.action_manager.do_action(data.action_id[0]);
1399 self.menu.open_action(data.action_id[0]);
1401 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1403 self.menu.menu_click(first_menu_id);
1408 $(window).trigger('hashchange');
1411 on_hashchange: function(event) {
1413 var stringstate = event.getState(false);
1414 if (!_.isEqual(this._current_state, stringstate)) {
1415 var state = event.getState(true);
1416 if(!state.action && state.menu_id) {
1417 self.menu.is_bound.done(function() {
1418 self.menu.menu_click(state.menu_id);
1421 state._push_me = false; // no need to push state back...
1422 this.action_manager.do_load_state(state, !!this._current_state);
1425 this._current_state = stringstate;
1427 do_push_state: function(state) {
1428 this.set_title(state.title);
1430 var url = '#' + $.param(state);
1431 this._current_state = $.deparam($.param(state), false); // stringify all values
1432 $.bbq.pushState(url);
1433 this.trigger('state_pushed', state);
1435 on_menu_action: function(options) {
1437 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1438 .then(function (result) {
1439 return self.action_mutex.exec(function() {
1440 if (options.needaction) {
1441 result.context = new instance.web.CompoundContext(result.context, {
1442 search_default_message_unread: true,
1443 search_disable_custom_filters: true,
1446 var completed = $.Deferred();
1447 $.when(self.action_manager.do_action(result, {
1448 clear_breadcrumbs: true,
1449 action_menu_id: self.menu.current_menu,
1450 })).fail(function() {
1451 self.menu.open_menu(options.previous_menu_id);
1452 }).always(function() {
1453 completed.resolve();
1455 setTimeout(function() {
1456 completed.resolve();
1458 // We block the menu when clicking on an element until the action has correctly finished
1459 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1464 set_content_full_screen: function(fullscreen) {
1465 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1466 this.$('.oe_webclient').toggleClass(
1467 'oe_content_full_screen', fullscreen);
1469 has_uncommitted_changes: function() {
1470 var $e = $.Event('clear_uncommitted_changes');
1471 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1472 if ($e.isDefaultPrevented()) {
1475 return this._super.apply(this, arguments);
1480 instance.web.EmbeddedClient = instance.web.Client.extend({
1481 _template: 'EmbedClient',
1482 init: function(parent, origin, dbname, login, key, action_id, options) {
1483 this._super(parent, origin);
1484 this.bind_credentials(dbname, login, key);
1485 this.action_id = action_id;
1486 this.options = options || {};
1490 return $.when(this._super()).then(function() {
1491 return self.authenticate().then(function() {
1492 if (!self.action_id) {
1495 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1496 var action = result;
1497 action.flags = _.extend({
1498 //views_switcher : false,
1499 search_view : false,
1500 action_buttons : false,
1503 }, self.options, action.flags || {});
1505 self.do_action(action);
1511 do_action: function(/*...*/) {
1512 var am = this.action_manager;
1513 return am.do_action.apply(am, arguments);
1516 authenticate: function() {
1517 var s = instance.session;
1518 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1521 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1524 bind_credentials: function(dbname, login, key) {
1525 this.dbname = dbname;
1532 instance.web.embed = function (origin, dbname, login, key, action, options) {
1533 $('head').append($('<link>', {
1534 'rel': 'stylesheet',
1536 'href': origin +'/web/css/web.assets_webclient'
1538 var currentScript = document.currentScript;
1539 if (!currentScript) {
1540 var sc = document.getElementsByTagName('script');
1541 currentScript = sc[sc.length-1];
1543 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1544 client.insertAfter(currentScript);
1549 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: