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 if (!_.isFunction(fn)) {
132 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
133 $customButons.append($but);
134 $but.on('click', function(ev) {
135 fn.call(self.$el, ev);
140 Initializes the popup.
142 @return The result returned by start().
144 init_dialog: function() {
146 var options = _.extend({}, this.dialog_options);
147 options.title = options.title || this.dialog_title;
148 if (options.buttons) {
149 this._add_buttons(options.buttons);
150 delete(options.buttons);
152 this.renderElement();
154 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
159 if (options.size !== 'large'){
160 var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
161 if (options.size === 'small'){
162 dialog_class_size.addClass('modal-sm');
166 this.$el.appendTo(this.$dialog_box.find(".modal-body"));
167 var $dialog_content = this.$dialog_box.find('.modal-content');
168 if (options.dialogClass){
169 $dialog_content.find(".modal-body").addClass(options.dialogClass);
171 $dialog_content.openerpClass();
173 this.$dialog_box.on('hidden.bs.modal', this, function() {
176 this.$dialog_box.modal('show');
178 this.dialog_inited = true;
179 var res = this.start();
183 Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
185 close: function(reason) {
186 if (this.dialog_inited && !this.__tmp_dialog_hiding) {
187 $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
188 if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal
189 this.__tmp_dialog_hiding = true;
190 this.$dialog_box.modal('hide');
191 this.__tmp_dialog_hiding = undefined;
193 this.trigger("closing", reason);
196 _closing: function() {
197 if (this.__tmp_dialog_destroying)
199 if (this.dialog_options.destroy_on_close) {
200 this.__tmp_dialog_closing = true;
202 this.__tmp_dialog_closing = undefined;
206 Destroys the popup, also closes it.
208 destroy: function (reason) {
209 this.$buttons.remove();
211 _.each(this.getChildren(), function(el) {
214 if (! this.__tmp_dialog_closing) {
215 this.__tmp_dialog_destroying = true;
217 this.__tmp_dialog_destroying = undefined;
219 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
220 //we need this to put the instruction to remove modal from DOM at the end
221 //of the queue, otherwise it might already have been removed before the modal-backdrop
222 //is removed when pressing escape key
223 var $element = this.$dialog_box;
224 setTimeout(function () {
225 //remove modal from list of opened modal since we just destroy it
226 var modal_list_index = $.inArray($element, opened_modal);
227 if (modal_list_index > -1){
228 opened_modal.splice(modal_list_index,1)[0].remove();
230 if (opened_modal.length > 0){
231 //we still have other opened modal so we should focus it
232 opened_modal[opened_modal.length-1].focus();
240 instance.web.CrashManager = instance.web.Class.extend({
245 rpc_error: function(error) {
249 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
251 new (handler)(this, error).display();
254 if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
255 this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
258 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
259 this.show_warning(error);
261 this.show_error(error);
264 show_warning: function(error) {
268 if (error.data.exception_type === "except_osv") {
269 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
271 new instance.web.Dialog(this, {
273 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
275 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
277 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
279 show_error: function(error) {
284 buttons[_t("Ok")] = function() {
285 this.parents('.modal').modal('hide');
287 new instance.web.Dialog(this, {
288 title: "OpenERP " + _.str.capitalize(error.type),
290 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
292 show_message: function(exception) {
294 type: _t("Client Error"),
302 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
304 instance.web.ExceptionHandler = {
306 @param parent The parent.
307 @param error The error object as returned by the JSON-RPC implementation.
309 init: function(parent, error) {},
311 Called to inform to display the widget, if necessary. A typical way would be to implement
312 this interface in a class extending instance.web.Dialog and simply display the dialog in this
315 display: function() {},
319 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
320 instance.web.ExceptionHandler.
322 instance.web.crash_manager_registry = new instance.web.Registry();
325 * Handle redirection warnings, which behave more or less like a regular
326 * warning, with an additional redirection button.
328 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
329 init: function(parent, error) {
333 display: function() {
335 error.data.message = error.data.arguments[0];
337 new instance.web.Dialog(this, {
339 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
341 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }},
342 {text: error.data.arguments[2], click: function() {
343 window.location.href='#action='+error.data.arguments[1];
344 this.$el.parents('.modal').modal('hide');
347 }, QWeb.render('CrashManager.warning', {error: error})).open();
351 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
353 instance.web.Loading = instance.web.Widget.extend({
354 template: _t("Loading"),
355 init: function(parent) {
358 this.blocked_ui = false;
359 this.session.on("request", this, this.request_call);
360 this.session.on("response", this, this.response_call);
361 this.session.on("response_failed", this, this.response_call);
363 destroy: function() {
364 this.on_rpc_event(-this.count);
367 request_call: function() {
368 this.on_rpc_event(1);
370 response_call: function() {
371 this.on_rpc_event(-1);
373 on_rpc_event : function(increment) {
375 if (!this.count && increment === 1) {
377 this.long_running_timer = setTimeout(function () {
378 self.blocked_ui = true;
379 instance.web.blockUI();
383 this.count += increment;
384 if (this.count > 0) {
385 if (instance.session.debug) {
386 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
388 this.$el.text(_t("Loading"));
391 this.getParent().$el.addClass('oe_wait');
394 clearTimeout(this.long_running_timer);
395 // Don't unblock if blocked by somebody else
396 if (self.blocked_ui) {
397 this.blocked_ui = false;
398 instance.web.unblockUI();
401 this.getParent().$el.removeClass('oe_wait');
406 instance.web.DatabaseManager = instance.web.Widget.extend({
407 init: function(parent) {
409 this.unblockUIFunction = instance.web.unblockUI;
410 $.validator.addMethod('matches', function (s, _, re) {
411 return new RegExp(re).test(s);
412 }, _t("Invalid database name"));
416 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
417 var fetch_db = this.rpc("/web/database/get_list", {}).then(
419 self.db_list = result;
425 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
426 self.lang_list = result;
428 return $.when(fetch_db, fetch_langs).always(self.do_render);
430 do_render: function() {
432 instance.webclient.toggle_bars(true);
433 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
434 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
435 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
436 $('ul.oe_secondary_submenu > li:first').addClass('active');
437 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
438 var menuitem = $(this);
439 menuitem.addClass('active').siblings().removeClass('active');
440 var form_id =menuitem.find('a').attr('href');
441 $(form_id).show().siblings().hide();
442 event.preventDefault();
444 $('#back-to-login').click(self.do_exit);
445 self.$el.find("td").addClass("oe_form_group_cell");
446 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
447 self.$el.find("label").addClass("oe_form_label");
448 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
449 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
450 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
451 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
452 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
453 self.$el.find("form[name=change_pwd_form]").validate({
455 old_pwd: _t("Please enter your previous password"),
456 new_pwd: _t("Please enter your new password"),
458 required: _t("Please confirm your new password"),
459 equalTo: _t("The confirmation does not match the password")
462 submitHandler: self.do_change_password
465 destroy: function () {
466 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
470 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
471 * from unblocking the UI
473 blockUI: function () {
474 instance.web.blockUI();
475 instance.web.unblockUI = function () {};
478 * Reinstates $.unblockUI so third parties can play with blockUI, and
481 unblockUI: function () {
482 instance.web.unblockUI = this.unblockUIFunction;
483 instance.web.unblockUI();
486 * Displays an error dialog resulting from the various RPC communications
487 * failing over themselves
489 * @param {Object} error error description
490 * @param {String} error.title title of the error dialog
491 * @param {String} error.error message of the error dialog
493 display_error: function (error) {
494 return new instance.web.Dialog(this, {
498 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
500 }, $('<div>').html(error.error)).open();
502 do_create: function(form) {
504 var fields = $(form).serializeArray();
505 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
507 instance.web.redirect('/web');
509 alert("Failed to create database");
513 do_duplicate: function(form) {
515 var fields = $(form).serializeArray();
516 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
518 self.display_error(result);
521 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
525 do_drop: function(form) {
528 fields = $form.serializeArray(),
529 $db_list = $form.find('[name=drop_db]'),
531 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
534 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
536 self.display_error(result);
539 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
543 do_backup: function(form) {
546 self.session.get_file({
548 success: function () {
549 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
551 error: function(error){
554 title: _t("Backup Database"),
555 error: 'AccessDenied'
559 complete: function() {
564 do_restore: function(form) {
568 url: '/web/database/restore',
571 success: function (body) {
572 // If empty body, everything went fine
573 if (!body) { return; }
575 if (body.indexOf('403 Forbidden') !== -1) {
577 title: _t("Access Denied"),
578 error: _t("Incorrect super-administrator password")
582 title: _t("Restore Database"),
583 error: _t("Could not restore the database")
587 complete: function() {
589 self.do_notify(_t("Restored"), _t("Database restored successfully"));
593 do_change_password: function(form) {
595 self.rpc("/web/database/change_password", {
596 'fields': $(form).serializeArray()
597 }).done(function(result) {
599 self.display_error(result);
603 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
606 do_exit: function () {
608 instance.web.redirect('/web');
611 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
613 instance.web.login = function() {
614 instance.web.redirect('/web/login');
616 instance.web.client_actions.add("login", "instance.web.login");
618 instance.web.logout = function() {
619 instance.web.redirect('/web/session/logout');
621 instance.web.client_actions.add("logout", "instance.web.logout");
625 * Redirect to url by replacing window.location
626 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
628 instance.web.redirect = function(url, wait) {
629 // Dont display a dialog if some xmlhttprequest are in progress
630 if (instance.client && instance.client.crashmanager) {
631 instance.client.crashmanager.active = false;
634 var load = function() {
635 var old = "" + window.location;
636 var old_no_hash = old.split("#")[0];
637 var url_no_hash = url.split("#")[0];
638 location.assign(url);
639 if (old_no_hash === url_no_hash) {
640 location.reload(true);
644 var wait_server = function() {
645 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
646 setTimeout(wait_server, 250);
651 setTimeout(wait_server, 1000);
658 * Client action to reload the whole interface.
659 * If params.menu_id, it opens the given menu entry.
660 * If params.wait, reload will wait the openerp server to be reachable before reloading
662 instance.web.Reload = function(parent, action) {
663 var params = action.params || {};
664 var menu_id = params.menu_id || false;
665 var l = window.location;
667 var sobj = $.deparam(l.search.substr(1));
668 if (params.url_search) {
669 sobj = _.extend(sobj, params.url_search);
671 var search = '?' + $.param(sobj);
675 hash = "#menu_id=" + menu_id;
677 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
679 instance.web.redirect(url, params.wait);
681 instance.web.client_actions.add("reload", "instance.web.Reload");
684 * Client action to go back in breadcrumb history.
685 * If can't go back in history stack, will go back to home.
687 instance.web.HistoryBack = function(parent) {
688 if (!parent.history_back()) {
689 instance.web.Home(parent);
692 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
695 * Client action to go back home.
697 instance.web.Home = function(parent, action) {
698 var url = '/' + (window.location.search || '');
699 instance.web.redirect(url, action && action.params && action.params.wait);
701 instance.web.client_actions.add("home", "instance.web.Home");
703 instance.web.ChangePassword = instance.web.Widget.extend({
704 template: "ChangePassword",
707 this.getParent().dialog_title = _t("Change Password");
708 var $button = self.$el.find('.oe_form_button');
709 $button.appendTo(this.getParent().$buttons);
710 $button.eq(2).click(function(){
711 self.$el.parents('.modal').modal('hide');
713 $button.eq(0).click(function(){
714 self.rpc("/web/session/change_password",{
715 'fields': $("form[name=change_password_form]").serializeArray()
716 }).done(function(result) {
718 self.display_error(result);
721 instance.webclient.on_logout();
726 display_error: function (error) {
727 return new instance.web.Dialog(this, {
731 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
733 }, $('<div>').html(error.error)).open();
736 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
738 instance.web.Menu = instance.web.Widget.extend({
742 this._super.apply(this, arguments);
743 this.has_been_loaded = $.Deferred();
744 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
745 this.data = {data:{children:[]}};
746 this.on("menu_loaded", this, function (menu_data) {
748 // launch the fetch of needaction counters, asynchronous
749 if (!_.isEmpty(menu_data.all_menu_ids)) {
750 this.do_load_needaction(menu_data.all_menu_ids);
753 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
754 instance.web.bus.on('resize', this, function() {
760 this._super.apply(this, arguments);
761 this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
762 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
763 return this.do_reload();
765 do_reload: function() {
767 return this.rpc("/web/menu/load", {}).done(function(r) {
771 menu_loaded: function(data) {
773 this.data = {data: data};
774 this.renderElement();
775 this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
776 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
777 // Hide second level submenus
778 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
779 if (self.current_menu) {
780 self.open_menu(self.current_menu);
782 this.trigger('menu_loaded', data);
783 this.has_been_loaded.resolve();
785 do_load_needaction: function (menu_ids) {
787 menu_ids = _.compact(menu_ids);
788 if (_.isEmpty(menu_ids)) {
791 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
792 self.on_needaction_loaded(r);
795 on_needaction_loaded: function(data) {
797 this.needaction_data = data;
798 _.each(this.needaction_data, function (item, menu_id) {
799 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
800 $item.find('.badge').remove();
801 if (item.needaction_counter && item.needaction_counter > 0) {
802 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
807 * Reflow the menu items and dock overflowing items into a "More" menu item.
808 * Automatically called when 'menu_loaded' event is triggered and on window resizing.
812 this.$el.height('auto').show();
813 var $more_container = this.$('#menu_more_container').hide();
814 var $more = this.$('#menu_more');
815 $more.children('li').insertBefore($more_container);
816 var $toplevel_items = this.$el.children('li').not($more_container).hide();
817 $toplevel_items.each(function() {
818 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
819 self.$el.parent().children(':visible').each(function() {
820 remaining_space -= $(this).outerWidth();
822 if ($(this).width() > remaining_space) {
827 $more.append($toplevel_items.filter(':hidden').show());
828 $more_container.toggle(!!$more.children().length);
829 // Hide toplevel item if there is only one
830 var $toplevel = this.$el.children("li:visible");
831 if ($toplevel.length === 1) {
836 * Opens a given menu by id, as if a user had browsed to that menu by hand
837 * except does not trigger any event on the way
839 * @param {Number} id database id of the terminal menu to select
841 open_menu: function (id) {
842 this.current_menu = id;
843 this.session.active_id = id;
844 var $clicked_menu, $sub_menu, $main_menu;
845 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
846 this.trigger('open_menu', id, $clicked_menu);
848 if (this.$secondary_menus.has($clicked_menu).length) {
849 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
850 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
852 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
853 $main_menu = $clicked_menu;
856 // Activate current main menu
857 this.$el.find('.active').removeClass('active');
858 $main_menu.addClass('active');
860 // Show current sub menu
861 this.$secondary_menus.find('.oe_secondary_menu').hide();
864 // Hide/Show the leftbar menu depending of the presence of sub-items
865 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
867 // Activate current menu item and show parents
868 this.$secondary_menus.find('.active').removeClass('active');
869 if ($main_menu !== $clicked_menu) {
870 $clicked_menu.parents().show();
871 if ($clicked_menu.is('.oe_menu_toggler')) {
872 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
874 $clicked_menu.parent().addClass('active');
877 // add a tooltip to cropped menu items
878 this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
879 $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'auto right'} :'destroy');
883 * Call open_menu with the first menu_item matching an action_id
885 * @param {Number} id the action_id to match
887 open_action: function (id) {
888 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
889 var menu_id = $menu.data('menu');
891 this.open_menu(menu_id);
895 * Process a click on a menu item
897 * @param {Number} id the menu_id
898 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
900 menu_click: function(id, needaction) {
903 // find back the menuitem in dom to get the action
904 var $item = this.$el.find('a[data-menu=' + id + ']');
906 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
908 var action_id = $item.data('action-id');
909 // If first level menu doesnt have action trigger first leaf
911 if(this.$el.has($item).length) {
912 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
913 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
915 action_id = $items.data('action-id');
916 id = $items.data('menu');
921 this.trigger('menu_click', {
922 action_id: action_id,
923 needaction: needaction,
925 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
930 do_reload_needaction: function () {
932 if (self.current_menu) {
933 self.do_load_needaction([self.current_menu]).then(function () {
934 self.trigger("need_action_reloaded");
939 * Jquery event handler for menu click
941 * @param {Event} ev the jquery event
943 on_top_menu_click: function(ev) {
945 var id = $(ev.currentTarget).data('menu');
947 var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
948 function add_menu_ids (menu) {
950 _.each(menu.children, function (menu) {
951 menu_ids.push(menu.id);
957 self.do_load_needaction(menu_ids).then(function () {
958 self.trigger("need_action_reloaded");
960 this.on_menu_click(ev);
962 on_menu_click: function(ev) {
964 var needaction = $(ev.target).is('div#menu_counter');
965 this.menu_click($(ev.currentTarget).data('menu'), needaction);
969 instance.web.UserMenu = instance.web.Widget.extend({
970 template: "UserMenu",
971 init: function(parent) {
973 this.update_promise = $.Deferred().resolve();
977 this._super.apply(this, arguments);
978 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
980 var f = self['on_menu_' + $(this).data('menu')];
986 do_update: function () {
988 var fct = function() {
989 var $avatar = self.$el.find('.oe_topbar_avatar');
990 $avatar.attr('src', $avatar.data('default-src'));
991 if (!self.session.uid)
993 var func = new instance.web.Model("res.users").get_func("read");
994 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
995 var topbar_name = res.name;
996 if(instance.session.debug)
997 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
998 if(res.company_id[0] > 1)
999 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1000 self.$el.find('.oe_topbar_name').text(topbar_name);
1001 if (!instance.session.debug) {
1002 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1004 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1005 $avatar.attr('src', avatar_src);
1008 this.update_promise = this.update_promise.then(fct, fct);
1010 on_menu_help: function() {
1011 window.open('http://help.odoo.com', '_blank');
1013 on_menu_logout: function() {
1014 this.trigger('user_logout');
1016 on_menu_settings: function() {
1018 if (!this.getParent().has_uncommitted_changes()) {
1019 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1020 result.res_id = instance.session.uid;
1021 self.getParent().action_manager.do_action(result);
1025 on_menu_account: function() {
1027 if (!this.getParent().has_uncommitted_changes()) {
1028 var P = new instance.web.Model('ir.config_parameter');
1029 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1031 'd': instance.session.db,
1032 'u': window.location.protocol + '//' + window.location.host,
1035 response_type: 'token',
1036 client_id: dbuuid || '',
1037 state: JSON.stringify(state),
1040 instance.web.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params));
1041 }).fail(function(result, ev){
1042 ev.preventDefault();
1043 instance.web.redirect('https://accounts.odoo.com/web');
1047 on_menu_about: function() {
1049 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1050 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1051 $help.find('a.oe_activate_debug_mode').click(function (e) {
1053 window.location = $.param.querystring( window.location.href, 'debug');
1055 new instance.web.Dialog(this, {
1057 dialogClass: 'oe_act_window',
1064 instance.web.FullscreenWidget = instance.web.Widget.extend({
1066 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1067 * and will have a fixed 1:1 zoom level on mobile devices.
1070 if(!$('#oe-fullscreenwidget-viewport').length){
1071 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1073 instance.webclient.set_content_full_screen(true);
1074 return this._super();
1076 destroy: function(){
1077 instance.webclient.set_content_full_screen(false);
1078 $('#oe-fullscreenwidget-viewport').remove();
1079 return this._super();
1084 instance.web.Client = instance.web.Widget.extend({
1085 init: function(parent, origin) {
1086 instance.client = instance.webclient = this;
1087 this.client_options = {};
1088 this._super(parent);
1089 this.origin = origin;
1093 return instance.session.session_bind(this.origin).then(function() {
1094 var $e = $(QWeb.render(self._template, {widget: self}));
1095 self.replaceElement($e);
1098 return self.show_common();
1101 bind_events: function() {
1103 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1104 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1106 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1107 ev.preventDefault();
1108 var $toggle = $(this);
1109 var doc_width = $(document).width();
1110 var $menu = $toggle.siblings('.oe_dropdown_menu');
1111 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1112 var state = $menu.is('.oe_opened');
1113 setTimeout(function() {
1114 // Do not alter propagation
1115 $toggle.add($menu).toggleClass('oe_opened', !state);
1117 // Move $menu if outside window's edge
1118 var offset = $menu.offset();
1119 var menu_width = $menu.width();
1120 var x = doc_width - offset.left - menu_width - 2;
1122 $menu.offset({ left: offset.left + x }).width(menu_width);
1127 instance.web.bus.on('click', this, function(ev) {
1128 $('.tooltip').remove();
1129 if (!$(ev.target).is('input[type=file]')) {
1130 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1134 show_common: function() {
1136 this.crashmanager = new instance.web.CrashManager();
1137 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1138 self.notification = new instance.web.Notification(this);
1139 self.notification.appendTo(self.$el);
1140 self.loading = new instance.web.Loading(self);
1141 self.loading.appendTo(self.$el);
1142 self.action_manager = new instance.web.ActionManager(self);
1143 self.action_manager.appendTo(self.$('.oe_application'));
1145 toggle_bars: function(value) {
1146 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1148 has_uncommitted_changes: function() {
1153 instance.web.WebClient = instance.web.Client.extend({
1154 _template: 'WebClient',
1156 'click .oe_logo_edit_admin': 'logo_edit'
1158 init: function(parent, client_options) {
1159 this._super(parent);
1160 if (client_options) {
1161 _.extend(this.client_options, client_options);
1163 this._current_state = null;
1164 this.menu_dm = new instance.web.DropMisordered();
1165 this.action_mutex = new $.Mutex();
1166 this.set('title_part', {"zopenerp": "OpenERP"});
1170 this.on("change:title_part", this, this._title_changed);
1171 this._title_changed();
1172 return $.when(this._super()).then(function() {
1173 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1176 if (self.session.session_is_valid()) {
1177 self.show_application();
1179 if (self.client_options.action) {
1180 self.action_manager.do_action(self.client_options.action);
1181 delete(self.client_options.action);
1185 to_kitten: function() {
1187 $("body").addClass("kitten-mode-activated");
1188 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1190 var imgkit = Math.floor(Math.random() * 2 + 1);
1191 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1195 Sets the first part of the title of the window, dedicated to the current action.
1197 set_title: function(title) {
1198 this.set_title_part("action", title);
1201 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1202 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1203 title of the window separated by '-'.
1205 set_title_part: function(part, title) {
1206 var tmp = _.clone(this.get("title_part"));
1208 this.set("title_part", tmp);
1210 _title_changed: function() {
1211 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1213 _.each(parts, function(part) {
1214 var str = this.get("title_part")[part];
1216 tmp = tmp ? tmp + " - " + str : str;
1219 document.title = tmp;
1221 show_common: function() {
1224 window.onerror = function (message, file, line) {
1225 self.crashmanager.show_error({
1226 type: _t("Client Error"),
1228 data: {debug: file + ':' + line}
1232 show_application: function() {
1234 self.toggle_bars(true);
1236 self.menu = new instance.web.Menu(self);
1237 self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1238 self.menu.on('menu_click', this, this.on_menu_action);
1239 self.user_menu = new instance.web.UserMenu(self);
1240 self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1241 self.user_menu.on('user_logout', self, self.on_logout);
1242 self.user_menu.do_update();
1243 self.bind_hashchange();
1245 self.check_timezone();
1246 if (self.client_options.action_post_login) {
1247 self.action_manager.do_action(self.client_options.action_post_login);
1248 delete(self.client_options.action_post_login);
1251 update_logo: function() {
1252 var img = this.session.url('/web/binary/company_logo');
1253 this.$('.oe_logo img').attr('src', '').attr('src', img);
1254 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1256 logo_edit: function(ev) {
1258 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1259 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1260 result.res_id = res['company_id'][0];
1261 result.target = "new";
1262 result.views = [[false, 'form']];
1264 action_buttons: true,
1266 self.action_manager.do_action(result);
1267 var form = self.action_manager.dialog_widget.views.form.controller;
1268 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1269 form.on('record_saved', self, function() {
1270 self.action_manager.dialog_stop();
1277 check_timezone: function() {
1279 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1280 var user_offset = result[0]['tz_offset'];
1281 var offset = -(new Date().getTimezoneOffset());
1282 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1283 var browser_offset = (offset < 0) ? "-" : "+";
1284 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1285 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1286 if (browser_offset !== user_offset) {
1287 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1288 $icon.on('click', function() {
1289 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1290 user_timezone: instance.session.user_context.tz || 'UTC',
1291 user_offset: user_offset,
1292 browser_offset: browser_offset,
1294 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1295 notification.close();
1296 }).find('a').on('click', function() {
1297 notification.close();
1298 self.user_menu.on_menu_settings();
1302 $icon.appendTo(self.$('.oe_systray'));
1306 destroy_content: function() {
1307 _.each(_.clone(this.getChildren()), function(el) {
1310 this.$el.children().remove();
1312 do_reload: function() {
1314 return this.session.session_reload().then(function () {
1315 instance.session.load_modules(true).then(
1316 self.menu.proxy('do_reload')); });
1319 do_notify: function() {
1320 var n = this.notification;
1321 return n.notify.apply(n, arguments);
1323 do_warn: function() {
1324 var n = this.notification;
1325 return n.warn.apply(n, arguments);
1327 on_logout: function() {
1329 if (!this.has_uncommitted_changes()) {
1330 self.action_manager.do_action('logout');
1333 bind_hashchange: function() {
1335 $(window).bind('hashchange', this.on_hashchange);
1337 var state = $.bbq.getState(true);
1338 if (_.isEmpty(state) || state.action == "login") {
1339 self.menu.has_been_loaded.done(function() {
1340 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1341 if(data.action_id) {
1342 self.action_manager.do_action(data.action_id[0]);
1343 self.menu.open_action(data.action_id[0]);
1345 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1347 self.menu.menu_click(first_menu_id);
1352 $(window).trigger('hashchange');
1355 on_hashchange: function(event) {
1357 var stringstate = event.getState(false);
1358 if (!_.isEqual(this._current_state, stringstate)) {
1359 var state = event.getState(true);
1360 if(!state.action && state.menu_id) {
1361 self.menu.has_been_loaded.done(function() {
1362 self.menu.do_reload().done(function() {
1363 self.menu.menu_click(state.menu_id);
1367 state._push_me = false; // no need to push state back...
1368 this.action_manager.do_load_state(state, !!this._current_state);
1371 this._current_state = stringstate;
1373 do_push_state: function(state) {
1374 this.set_title(state.title);
1376 var url = '#' + $.param(state);
1377 this._current_state = $.deparam($.param(state), false); // stringify all values
1378 $.bbq.pushState(url);
1379 this.trigger('state_pushed', state);
1381 on_menu_action: function(options) {
1383 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1384 .then(function (result) {
1385 return self.action_mutex.exec(function() {
1386 if (options.needaction) {
1387 result.context = new instance.web.CompoundContext(result.context, {
1388 search_default_message_unread: true,
1389 search_disable_custom_filters: true,
1392 var completed = $.Deferred();
1393 $.when(self.action_manager.do_action(result, {
1394 clear_breadcrumbs: true,
1395 action_menu_id: self.menu.current_menu,
1396 })).fail(function() {
1397 self.menu.open_menu(options.previous_menu_id);
1398 }).always(function() {
1399 completed.resolve();
1401 setTimeout(function() {
1402 completed.resolve();
1404 // We block the menu when clicking on an element until the action has correctly finished
1405 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1410 set_content_full_screen: function(fullscreen) {
1411 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1412 this.$('.oe_webclient').toggleClass(
1413 'oe_content_full_screen', fullscreen);
1415 has_uncommitted_changes: function() {
1416 var $e = $.Event('clear_uncommitted_changes');
1417 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1418 if ($e.isDefaultPrevented()) {
1421 return this._super.apply(this, arguments);
1426 instance.web.EmbeddedClient = instance.web.Client.extend({
1427 _template: 'EmbedClient',
1428 init: function(parent, origin, dbname, login, key, action_id, options) {
1429 this._super(parent, origin);
1430 this.bind_credentials(dbname, login, key);
1431 this.action_id = action_id;
1432 this.options = options || {};
1436 return $.when(this._super()).then(function() {
1437 return self.authenticate().then(function() {
1438 if (!self.action_id) {
1441 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1442 var action = result;
1443 action.flags = _.extend({
1444 //views_switcher : false,
1445 search_view : false,
1446 action_buttons : false,
1449 }, self.options, action.flags || {});
1451 self.do_action(action);
1457 do_action: function(/*...*/) {
1458 var am = this.action_manager;
1459 return am.do_action.apply(am, arguments);
1462 authenticate: function() {
1463 var s = instance.session;
1464 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1467 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1470 bind_credentials: function(dbname, login, key) {
1471 this.dbname = dbname;
1478 instance.web.embed = function (origin, dbname, login, key, action, options) {
1479 $('head').append($('<link>', {
1480 'rel': 'stylesheet',
1482 'href': origin +'/web/css/web.assets_webclient'
1484 var currentScript = document.currentScript;
1485 if (!currentScript) {
1486 var sc = document.getElementsByTagName('script');
1487 currentScript = sc[sc.length-1];
1489 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1490 client.insertAfter(currentScript);
1495 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: