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 oe_link_class = fn.oe_link_class;
129 if (!_.isFunction(fn)) {
133 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {'class': oe_link_class} }}}));
134 $customButons.append($but);
135 $but.on('click', function(ev) {
136 fn.call(self.$el, ev);
141 Initializes the popup.
143 @return The result returned by start().
145 init_dialog: function() {
147 var options = _.extend({}, this.dialog_options);
148 options.title = options.title || this.dialog_title;
149 if (options.buttons) {
150 this._add_buttons(options.buttons);
151 delete(options.buttons);
153 this.renderElement();
155 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
160 if (options.size !== 'large'){
161 var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
162 if (options.size === 'small'){
163 dialog_class_size.addClass('modal-sm');
167 this.$el.appendTo(this.$dialog_box.find(".modal-body"));
168 var $dialog_content = this.$dialog_box.find('.modal-content');
169 if (options.dialogClass){
170 $dialog_content.find(".modal-body").addClass(options.dialogClass);
172 $dialog_content.openerpClass();
174 this.$dialog_box.on('hidden.bs.modal', this, function() {
177 this.$dialog_box.modal('show');
179 this.dialog_inited = true;
180 var res = this.start();
184 Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
186 close: function(reason) {
187 if (this.dialog_inited && !this.__tmp_dialog_hiding) {
188 $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
189 if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal
190 this.__tmp_dialog_hiding = true;
191 this.$dialog_box.modal('hide');
192 this.__tmp_dialog_hiding = undefined;
194 this.trigger("closing", reason);
197 _closing: function() {
198 if (this.__tmp_dialog_destroying)
200 if (this.dialog_options.destroy_on_close) {
201 this.__tmp_dialog_closing = true;
203 this.__tmp_dialog_closing = undefined;
207 Destroys the popup, also closes it.
209 destroy: function (reason) {
210 this.$buttons.remove();
212 _.each(this.getChildren(), function(el) {
215 if (! this.__tmp_dialog_closing) {
216 this.__tmp_dialog_destroying = true;
218 this.__tmp_dialog_destroying = undefined;
220 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
221 //we need this to put the instruction to remove modal from DOM at the end
222 //of the queue, otherwise it might already have been removed before the modal-backdrop
223 //is removed when pressing escape key
224 var $element = this.$dialog_box;
225 setTimeout(function () {
226 //remove modal from list of opened modal since we just destroy it
227 var modal_list_index = $.inArray($element, opened_modal);
228 if (modal_list_index > -1){
229 opened_modal.splice(modal_list_index,1)[0].remove();
231 if (opened_modal.length > 0){
232 //we still have other opened modal so we should focus it
233 opened_modal[opened_modal.length-1].focus();
241 instance.web.CrashManager = instance.web.Class.extend({
246 rpc_error: function(error) {
250 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
252 new (handler)(this, error).display();
255 if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
256 this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
259 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
260 this.show_warning(error);
262 this.show_error(error);
265 show_warning: function(error) {
269 if (error.data.exception_type === "except_osv") {
270 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
272 new instance.web.Dialog(this, {
274 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
276 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
278 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
280 show_error: function(error) {
285 buttons[_t("Ok")] = function() {
286 this.parents('.modal').modal('hide');
288 new instance.web.Dialog(this, {
289 title: "Odoo " + _.str.capitalize(error.type),
291 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
293 show_message: function(exception) {
295 type: _t("Client Error"),
303 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
305 instance.web.ExceptionHandler = {
307 @param parent The parent.
308 @param error The error object as returned by the JSON-RPC implementation.
310 init: function(parent, error) {},
312 Called to inform to display the widget, if necessary. A typical way would be to implement
313 this interface in a class extending instance.web.Dialog and simply display the dialog in this
316 display: function() {},
320 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
321 instance.web.ExceptionHandler.
323 instance.web.crash_manager_registry = new instance.web.Registry();
326 * Handle redirection warnings, which behave more or less like a regular
327 * warning, with an additional redirection button.
329 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
330 init: function(parent, error) {
334 display: function() {
337 error.data.message = error.data.arguments[0];
339 new instance.web.Dialog(this, {
341 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
343 {text: _t("Ok"), click: function() { self.$el.parents('.modal').modal('hide'); self.destroy();}},
344 {text: error.data.arguments[2],
345 oe_link_class: 'oe_link',
347 window.location.href='#action='+error.data.arguments[1];
351 }, QWeb.render('CrashManager.warning', {error: error})).open();
354 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
356 instance.web.Loading = instance.web.Widget.extend({
357 template: _t("Loading"),
358 init: function(parent) {
361 this.blocked_ui = false;
362 this.session.on("request", this, this.request_call);
363 this.session.on("response", this, this.response_call);
364 this.session.on("response_failed", this, this.response_call);
366 destroy: function() {
367 this.on_rpc_event(-this.count);
370 request_call: function() {
371 this.on_rpc_event(1);
373 response_call: function() {
374 this.on_rpc_event(-1);
376 on_rpc_event : function(increment) {
378 if (!this.count && increment === 1) {
380 this.long_running_timer = setTimeout(function () {
381 self.blocked_ui = true;
382 instance.web.blockUI();
386 this.count += increment;
387 if (this.count > 0) {
388 if (instance.session.debug) {
389 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
391 this.$el.text(_t("Loading"));
394 this.getParent().$el.addClass('oe_wait');
397 clearTimeout(this.long_running_timer);
398 // Don't unblock if blocked by somebody else
399 if (self.blocked_ui) {
400 this.blocked_ui = false;
401 instance.web.unblockUI();
404 this.getParent().$el.removeClass('oe_wait');
409 instance.web.DatabaseManager = instance.web.Widget.extend({
410 init: function(parent) {
412 this.unblockUIFunction = instance.web.unblockUI;
413 $.validator.addMethod('matches', function (s, _, re) {
414 return new RegExp(re).test(s);
415 }, _t("Invalid database name"));
419 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
420 var fetch_db = this.rpc("/web/database/get_list", {}).then(
422 self.db_list = result;
428 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
429 self.lang_list = result;
431 return $.when(fetch_db, fetch_langs).always(self.do_render);
433 do_render: function() {
435 instance.webclient.toggle_bars(true);
436 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
437 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
438 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
439 $('ul.oe_secondary_submenu > li:first').addClass('active');
440 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
441 var menuitem = $(this);
442 menuitem.addClass('active').siblings().removeClass('active');
443 var form_id =menuitem.find('a').attr('href');
444 $(form_id).show().siblings().hide();
445 event.preventDefault();
447 $('#back-to-login').click(self.do_exit);
448 self.$el.find("td").addClass("oe_form_group_cell");
449 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
450 self.$el.find("label").addClass("oe_form_label");
451 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
452 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
453 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
454 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
455 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
456 self.$el.find("form[name=change_pwd_form]").validate({
458 old_pwd: _t("Please enter your previous password"),
459 new_pwd: _t("Please enter your new password"),
461 required: _t("Please confirm your new password"),
462 equalTo: _t("The confirmation does not match the password")
465 submitHandler: self.do_change_password
468 destroy: function () {
469 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
473 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
474 * from unblocking the UI
476 blockUI: function () {
477 instance.web.blockUI();
478 instance.web.unblockUI = function () {};
481 * Reinstates $.unblockUI so third parties can play with blockUI, and
484 unblockUI: function () {
485 instance.web.unblockUI = this.unblockUIFunction;
486 instance.web.unblockUI();
489 * Displays an error dialog resulting from the various RPC communications
490 * failing over themselves
492 * @param {Object} error error description
493 * @param {String} error.title title of the error dialog
494 * @param {String} error.error message of the error dialog
496 display_error: function (error) {
497 return new instance.web.Dialog(this, {
501 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
503 }, $('<div>').html(error.error)).open();
505 do_create: function(form) {
507 var fields = $(form).serializeArray();
508 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
510 instance.web.redirect('/web');
512 alert("Failed to create database");
516 do_duplicate: function(form) {
518 var fields = $(form).serializeArray();
519 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
521 self.display_error(result);
524 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
528 do_drop: function(form) {
531 fields = $form.serializeArray(),
532 $db_list = $form.find('[name=drop_db]'),
534 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
537 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
539 self.display_error(result);
542 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
546 do_backup: function(form) {
549 self.session.get_file({
551 success: function () {
552 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
554 error: function(error){
557 title: _t("Backup Database"),
558 error: 'AccessDenied'
562 complete: function() {
567 do_restore: function(form) {
571 url: '/web/database/restore',
574 success: function (body) {
575 // If empty body, everything went fine
576 if (!body) { return; }
578 if (body.indexOf('403 Forbidden') !== -1) {
580 title: _t("Access Denied"),
581 error: _t("Incorrect super-administrator password")
585 title: _t("Restore Database"),
586 error: _t("Could not restore the database")
590 complete: function() {
592 self.do_notify(_t("Restored"), _t("Database restored successfully"));
596 do_change_password: function(form) {
598 self.rpc("/web/database/change_password", {
599 'fields': $(form).serializeArray()
600 }).done(function(result) {
602 self.display_error(result);
606 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
609 do_exit: function () {
611 instance.web.redirect('/web');
614 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
616 instance.web.login = function() {
617 instance.web.redirect('/web/login');
619 instance.web.client_actions.add("login", "instance.web.login");
621 instance.web.logout = function() {
622 instance.web.redirect('/web/session/logout');
624 instance.web.client_actions.add("logout", "instance.web.logout");
628 * Redirect to url by replacing window.location
629 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
631 instance.web.redirect = function(url, wait) {
632 // Dont display a dialog if some xmlhttprequest are in progress
633 if (instance.client && instance.client.crashmanager) {
634 instance.client.crashmanager.active = false;
637 var load = function() {
638 var old = "" + window.location;
639 var old_no_hash = old.split("#")[0];
640 var url_no_hash = url.split("#")[0];
641 location.assign(url);
642 if (old_no_hash === url_no_hash) {
643 location.reload(true);
647 var wait_server = function() {
648 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
649 setTimeout(wait_server, 250);
654 setTimeout(wait_server, 1000);
661 * Client action to reload the whole interface.
662 * If params.menu_id, it opens the given menu entry.
663 * If params.wait, reload will wait the openerp server to be reachable before reloading
665 instance.web.Reload = function(parent, action) {
666 var params = action.params || {};
667 var menu_id = params.menu_id || false;
668 var l = window.location;
670 var sobj = $.deparam(l.search.substr(1));
671 if (params.url_search) {
672 sobj = _.extend(sobj, params.url_search);
674 var search = '?' + $.param(sobj);
678 hash = "#menu_id=" + menu_id;
680 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
682 instance.web.redirect(url, params.wait);
684 instance.web.client_actions.add("reload", "instance.web.Reload");
687 * Client action to refresh the session context (making sure
688 * HTTP requests will have the right one) then reload the
691 instance.web.ReloadContext = function(parent, action) {
692 // side-effect of get_session_info is to refresh the session context
693 instance.session.rpc("/web/session/get_session_info", {}).then(function() {
694 instance.web.Reload(parent, action);
697 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
700 * Client action to go back in breadcrumb history.
701 * If can't go back in history stack, will go back to home.
703 instance.web.HistoryBack = function(parent) {
704 if (!parent.history_back()) {
705 instance.web.Home(parent);
708 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
711 * Client action to go back home.
713 instance.web.Home = function(parent, action) {
714 var url = '/' + (window.location.search || '');
715 instance.web.redirect(url, action && action.params && action.params.wait);
717 instance.web.client_actions.add("home", "instance.web.Home");
719 instance.web.ChangePassword = instance.web.Widget.extend({
720 template: "ChangePassword",
723 this.getParent().dialog_title = _t("Change Password");
724 var $button = self.$el.find('.oe_form_button');
725 $button.appendTo(this.getParent().$buttons);
726 $button.eq(2).click(function(){
727 self.$el.parents('.modal').modal('hide');
729 $button.eq(0).click(function(){
730 self.rpc("/web/session/change_password",{
731 'fields': $("form[name=change_password_form]").serializeArray()
732 }).done(function(result) {
734 self.display_error(result);
737 instance.webclient.on_logout();
742 display_error: function (error) {
743 return new instance.web.Dialog(this, {
747 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
749 }, $('<div>').html(error.error)).open();
752 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
754 instance.web.Menu = instance.web.Widget.extend({
757 this._super.apply(this, arguments);
758 this.is_bound = $.Deferred();
759 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
760 this.data = {data:{children:[]}};
761 this.on("menu_bound", this, function() {
762 // launch the fetch of needaction counters, asynchronous
763 var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
764 var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
765 if (!_.isEmpty(all_menu_ids)) {
766 this.do_load_needaction(all_menu_ids);
771 this._super.apply(this, arguments);
772 return this.bind_menu();
774 do_reload: function() {
778 bind_menu: function() {
780 this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
781 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
782 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
783 // Hide second level submenus
784 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
785 if (self.current_menu) {
786 self.open_menu(self.current_menu);
788 this.trigger('menu_bound');
790 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
791 instance.web.bus.on('resize', this, function() {
792 if (parseInt(self.$el.parent().css('width')) <= 768 ) {
793 lazyreflow('all_outside');
798 instance.web.bus.trigger('resize');
800 this.is_bound.resolve();
802 do_load_needaction: function (menu_ids) {
804 menu_ids = _.compact(menu_ids);
805 if (_.isEmpty(menu_ids)) {
808 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
809 self.on_needaction_loaded(r);
812 on_needaction_loaded: function(data) {
814 this.needaction_data = data;
815 _.each(this.needaction_data, function (item, menu_id) {
816 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
817 $item.find('.badge').remove();
818 if (item.needaction_counter && item.needaction_counter > 0) {
819 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
824 * Reflow the menu items and dock overflowing items into a "More" menu item.
825 * Automatically called when 'menu_bound' event is triggered and on window resizing.
827 * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
828 * 'all_inside', all the items are hidden under the more item. If not set, only the
829 * overflowing items are hidden.
831 reflow: function(behavior) {
833 var $more_container = this.$('#menu_more_container').hide();
834 var $more = this.$('#menu_more');
835 var $systray = this.$el.parents().find('.oe_systray');
837 $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
839 // 'all_outside' beahavior should display all the items, so hide the more menu and exit
840 if (behavior === 'all_outside') {
841 this.$el.find('li').show();
842 $more_container.hide();
846 var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
847 $toplevel_items.each(function() {
848 // In all inside mode, we do not compute to know if we must hide the items, we hide them all
849 if (behavior === 'all_inside') {
852 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
853 self.$el.parent().children(':visible').each(function() {
854 remaining_space -= $(this).outerWidth();
857 if ($(this).width() > remaining_space) {
862 $more.append($toplevel_items.filter(':hidden').show());
863 $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
864 // Hide toplevel item if there is only one
865 var $toplevel = this.$el.children("li:visible");
866 if ($toplevel.length === 1 && behavior != 'all_inside') {
871 * Opens a given menu by id, as if a user had browsed to that menu by hand
872 * except does not trigger any event on the way
874 * @param {Number} id database id of the terminal menu to select
876 open_menu: function (id) {
877 this.current_menu = id;
878 this.session.active_id = id;
879 var $clicked_menu, $sub_menu, $main_menu;
880 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
881 this.trigger('open_menu', id, $clicked_menu);
883 if (this.$secondary_menus.has($clicked_menu).length) {
884 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
885 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
887 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
888 $main_menu = $clicked_menu;
891 // Activate current main menu
892 this.$el.find('.active').removeClass('active');
893 $main_menu.parent().addClass('active');
895 // Show current sub menu
896 this.$secondary_menus.find('.oe_secondary_menu').hide();
899 // Hide/Show the leftbar menu depending of the presence of sub-items
900 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
902 // Activate current menu item and show parents
903 this.$secondary_menus.find('.active').removeClass('active');
904 if ($main_menu !== $clicked_menu) {
905 $clicked_menu.parents().show();
906 if ($clicked_menu.is('.oe_menu_toggler')) {
907 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
909 $clicked_menu.parent().addClass('active');
912 // add a tooltip to cropped menu items
913 this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
914 $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
918 * Call open_menu with the first menu_item matching an action_id
920 * @param {Number} id the action_id to match
922 open_action: function (id) {
923 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
924 var menu_id = $menu.data('menu');
926 this.open_menu(menu_id);
930 * Process a click on a menu item
932 * @param {Number} id the menu_id
933 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
935 menu_click: function(id, needaction) {
938 // find back the menuitem in dom to get the action
939 var $item = this.$el.find('a[data-menu=' + id + ']');
941 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
943 var action_id = $item.data('action-id');
944 // If first level menu doesnt have action trigger first leaf
946 if(this.$el.has($item).length) {
947 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
948 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
950 action_id = $items.data('action-id');
951 id = $items.data('menu');
956 this.trigger('menu_click', {
957 action_id: action_id,
958 needaction: needaction,
960 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
963 console.log('Menu no action found web test 04 will fail');
967 do_reload_needaction: function () {
969 if (self.current_menu) {
970 self.do_load_needaction([self.current_menu]).then(function () {
971 self.trigger("need_action_reloaded");
976 * Jquery event handler for menu click
978 * @param {Event} ev the jquery event
980 on_top_menu_click: function(ev) {
983 var id = $(ev.currentTarget).data('menu');
985 // Fetch the menu leaves ids in order to check if they need a 'needaction'
986 var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
987 var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
988 var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
990 self.do_load_needaction(menu_ids).then(function () {
991 self.trigger("need_action_reloaded");
994 this.on_menu_click(ev);
996 on_menu_click: function(ev) {
998 var needaction = $(ev.target).is('div#menu_counter');
999 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1003 instance.web.UserMenu = instance.web.Widget.extend({
1004 template: "UserMenu",
1005 init: function(parent) {
1006 this._super(parent);
1007 this.update_promise = $.Deferred().resolve();
1011 this._super.apply(this, arguments);
1012 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1013 ev.preventDefault();
1014 var f = self['on_menu_' + $(this).data('menu')];
1019 this.$el.parent().show()
1021 do_update: function () {
1023 var fct = function() {
1024 var $avatar = self.$el.find('.oe_topbar_avatar');
1025 $avatar.attr('src', $avatar.data('default-src'));
1026 if (!self.session.uid)
1028 var func = new instance.web.Model("res.users").get_func("read");
1029 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1030 var topbar_name = res.name;
1031 if(instance.session.debug)
1032 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1033 if(res.company_id[0] > 1)
1034 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1035 self.$el.find('.oe_topbar_name').text(topbar_name);
1036 if (!instance.session.debug) {
1037 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1039 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1040 $avatar.attr('src', avatar_src);
1042 openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic
1045 this.update_promise = this.update_promise.then(fct, fct);
1047 on_menu_help: function() {
1048 window.open('http://help.openerp.com', '_blank');
1050 on_menu_logout: function() {
1051 this.trigger('user_logout');
1053 on_menu_settings: function() {
1055 if (!this.getParent().has_uncommitted_changes()) {
1056 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1057 result.res_id = instance.session.uid;
1058 self.getParent().action_manager.do_action(result);
1062 on_menu_account: function() {
1064 if (!this.getParent().has_uncommitted_changes()) {
1065 var P = new instance.web.Model('ir.config_parameter');
1066 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1068 'd': instance.session.db,
1069 'u': window.location.protocol + '//' + window.location.host,
1072 response_type: 'token',
1073 client_id: dbuuid || '',
1074 state: JSON.stringify(state),
1077 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1081 on_menu_about: function() {
1083 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1084 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1085 $help.find('a.oe_activate_debug_mode').click(function (e) {
1087 window.location = $.param.querystring( window.location.href, 'debug');
1089 new instance.web.Dialog(this, {
1091 dialogClass: 'oe_act_window',
1098 instance.web.FullscreenWidget = instance.web.Widget.extend({
1100 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1101 * and will have a fixed 1:1 zoom level on mobile devices.
1104 if(!$('#oe-fullscreenwidget-viewport').length){
1105 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1107 instance.webclient.set_content_full_screen(true);
1108 return this._super();
1110 destroy: function(){
1111 instance.webclient.set_content_full_screen(false);
1112 $('#oe-fullscreenwidget-viewport').remove();
1113 return this._super();
1118 instance.web.Client = instance.web.Widget.extend({
1119 init: function(parent, origin) {
1120 instance.client = instance.webclient = this;
1121 this.client_options = {};
1122 this._super(parent);
1123 this.origin = origin;
1127 return instance.session.session_bind(this.origin).then(function() {
1129 return self.show_common();
1132 bind_events: function() {
1134 $('.oe_systray').show();
1135 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1136 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1138 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1139 ev.preventDefault();
1140 var $toggle = $(this);
1141 var doc_width = $(document).width();
1142 var $menu = $toggle.siblings('.oe_dropdown_menu');
1143 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1144 var state = $menu.is('.oe_opened');
1145 setTimeout(function() {
1146 // Do not alter propagation
1147 $toggle.add($menu).toggleClass('oe_opened', !state);
1149 // Move $menu if outside window's edge
1150 var offset = $menu.offset();
1151 var menu_width = $menu.width();
1152 var x = doc_width - offset.left - menu_width - 2;
1154 $menu.offset({ left: offset.left + x }).width(menu_width);
1159 instance.web.bus.on('click', this, function(ev) {
1160 $('.tooltip').remove();
1161 if (!$(ev.target).is('input[type=file]')) {
1162 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1166 show_common: function() {
1168 this.crashmanager = new instance.web.CrashManager();
1169 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1170 self.notification = new instance.web.Notification(this);
1171 self.notification.appendTo(self.$el);
1172 self.loading = new instance.web.Loading(self);
1173 self.loading.appendTo(self.$('.openerp_webclient_container'));
1174 self.action_manager = new instance.web.ActionManager(self);
1175 self.action_manager.appendTo(self.$('.oe_application'));
1177 toggle_bars: function(value) {
1178 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1180 has_uncommitted_changes: function() {
1185 instance.web.WebClient = instance.web.Client.extend({
1186 init: function(parent, client_options) {
1187 this._super(parent);
1188 if (client_options) {
1189 _.extend(this.client_options, client_options);
1191 this._current_state = null;
1192 this.menu_dm = new instance.web.DropMisordered();
1193 this.action_mutex = new $.Mutex();
1194 this.set('title_part', {"zopenerp": "Odoo"});
1198 this.on("change:title_part", this, this._title_changed);
1199 this._title_changed();
1201 return $.when(this._super()).then(function() {
1202 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1205 if (self.session.session_is_valid()) {
1206 self.show_application();
1208 if (self.client_options.action) {
1209 self.action_manager.do_action(self.client_options.action);
1210 delete(self.client_options.action);
1214 to_kitten: function() {
1216 $("body").addClass("kitten-mode-activated");
1217 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1219 var imgkit = Math.floor(Math.random() * 2 + 1);
1220 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1224 Sets the first part of the title of the window, dedicated to the current action.
1226 set_title: function(title) {
1227 this.set_title_part("action", title);
1230 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1231 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1232 title of the window separated by '-'.
1234 set_title_part: function(part, title) {
1235 var tmp = _.clone(this.get("title_part"));
1237 this.set("title_part", tmp);
1239 _title_changed: function() {
1240 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1242 _.each(parts, function(part) {
1243 var str = this.get("title_part")[part];
1245 tmp = tmp ? tmp + " - " + str : str;
1248 document.title = tmp;
1250 show_common: function() {
1253 window.onerror = function (message, file, line) {
1254 self.crashmanager.show_error({
1255 type: _t("Client Error"),
1257 data: {debug: file + ':' + line}
1261 show_application: function() {
1263 self.toggle_bars(true);
1266 this.$('.oe_logo_edit_admin').click(function(ev) {
1270 // Menu is rendered server-side thus we don't want the widget to create any dom
1271 self.menu = new instance.web.Menu(self);
1272 self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1274 self.menu.on('menu_click', this, this.on_menu_action);
1275 self.user_menu = new instance.web.UserMenu(self);
1276 self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1277 self.user_menu.on('user_logout', self, self.on_logout);
1278 self.user_menu.do_update();
1279 self.bind_hashchange();
1281 self.check_timezone();
1282 if (self.client_options.action_post_login) {
1283 self.action_manager.do_action(self.client_options.action_post_login);
1284 delete(self.client_options.action_post_login);
1287 update_logo: function() {
1288 var img = this.session.url('/web/binary/company_logo');
1289 this.$('.oe_logo img').attr('src', '').attr('src', img);
1290 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1292 logo_edit: function(ev) {
1294 ev.preventDefault();
1295 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1296 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1297 result.res_id = res['company_id'][0];
1298 result.target = "new";
1299 result.views = [[false, 'form']];
1301 action_buttons: true,
1303 self.action_manager.do_action(result);
1304 var form = self.action_manager.dialog_widget.views.form.controller;
1305 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1306 form.on('record_saved', self, function() {
1307 self.action_manager.dialog_stop();
1314 check_timezone: function() {
1316 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1317 var user_offset = result[0]['tz_offset'];
1318 var offset = -(new Date().getTimezoneOffset());
1319 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1320 var browser_offset = (offset < 0) ? "-" : "+";
1321 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1322 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1323 if (browser_offset !== user_offset) {
1324 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1325 $icon.on('click', function() {
1326 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1327 user_timezone: instance.session.user_context.tz || 'UTC',
1328 user_offset: user_offset,
1329 browser_offset: browser_offset,
1331 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1332 notification.close();
1333 }).find('a').on('click', function() {
1334 notification.close();
1335 self.user_menu.on_menu_settings();
1339 $icon.prependTo(window.$('.oe_systray'));
1343 destroy_content: function() {
1344 _.each(_.clone(this.getChildren()), function(el) {
1347 this.$el.children().remove();
1349 do_reload: function() {
1351 return this.session.session_reload().then(function () {
1352 instance.session.load_modules(true).then(
1353 self.menu.proxy('do_reload')); });
1355 do_notify: function() {
1356 var n = this.notification;
1357 return n.notify.apply(n, arguments);
1359 do_warn: function() {
1360 var n = this.notification;
1361 return n.warn.apply(n, arguments);
1363 on_logout: function() {
1365 if (!this.has_uncommitted_changes()) {
1366 self.action_manager.do_action('logout');
1369 bind_hashchange: function() {
1371 $(window).bind('hashchange', this.on_hashchange);
1373 var state = $.bbq.getState(true);
1374 if (_.isEmpty(state) || state.action == "login") {
1375 self.menu.is_bound.done(function() {
1376 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1377 if(data.action_id) {
1378 self.action_manager.do_action(data.action_id[0]);
1379 self.menu.open_action(data.action_id[0]);
1381 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1383 self.menu.menu_click(first_menu_id);
1388 $(window).trigger('hashchange');
1391 on_hashchange: function(event) {
1393 var stringstate = event.getState(false);
1394 if (!_.isEqual(this._current_state, stringstate)) {
1395 var state = event.getState(true);
1396 if(!state.action && state.menu_id) {
1397 self.menu.is_bound.done(function() {
1398 self.menu.menu_click(state.menu_id);
1401 state._push_me = false; // no need to push state back...
1402 this.action_manager.do_load_state(state, !!this._current_state);
1405 this._current_state = stringstate;
1407 do_push_state: function(state) {
1408 this.set_title(state.title);
1410 var url = '#' + $.param(state);
1411 this._current_state = $.deparam($.param(state), false); // stringify all values
1412 $.bbq.pushState(url);
1413 this.trigger('state_pushed', state);
1415 on_menu_action: function(options) {
1417 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1418 .then(function (result) {
1419 return self.action_mutex.exec(function() {
1420 if (options.needaction) {
1421 result.context = new instance.web.CompoundContext(result.context, {
1422 search_default_message_unread: true,
1423 search_disable_custom_filters: true,
1426 var completed = $.Deferred();
1427 $.when(self.action_manager.do_action(result, {
1428 clear_breadcrumbs: true,
1429 action_menu_id: self.menu.current_menu,
1430 })).fail(function() {
1431 self.menu.open_menu(options.previous_menu_id);
1432 }).always(function() {
1433 completed.resolve();
1435 setTimeout(function() {
1436 completed.resolve();
1438 // We block the menu when clicking on an element until the action has correctly finished
1439 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1444 set_content_full_screen: function(fullscreen) {
1445 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1446 this.$('.oe_webclient').toggleClass(
1447 'oe_content_full_screen', fullscreen);
1449 has_uncommitted_changes: function() {
1450 var $e = $.Event('clear_uncommitted_changes');
1451 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1452 if ($e.isDefaultPrevented()) {
1455 return this._super.apply(this, arguments);
1460 instance.web.EmbeddedClient = instance.web.Client.extend({
1461 _template: 'EmbedClient',
1462 init: function(parent, origin, dbname, login, key, action_id, options) {
1463 this._super(parent, origin);
1464 this.bind_credentials(dbname, login, key);
1465 this.action_id = action_id;
1466 this.options = options || {};
1470 return $.when(this._super()).then(function() {
1471 return self.authenticate().then(function() {
1472 if (!self.action_id) {
1475 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1476 var action = result;
1477 action.flags = _.extend({
1478 //views_switcher : false,
1479 search_view : false,
1480 action_buttons : false,
1483 }, self.options, action.flags || {});
1485 self.do_action(action);
1491 do_action: function(/*...*/) {
1492 var am = this.action_manager;
1493 return am.do_action.apply(am, arguments);
1496 authenticate: function() {
1497 var s = instance.session;
1498 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1501 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1504 bind_credentials: function(dbname, login, key) {
1505 this.dbname = dbname;
1512 instance.web.embed = function (origin, dbname, login, key, action, options) {
1513 $('head').append($('<link>', {
1514 'rel': 'stylesheet',
1516 'href': origin +'/web/css/web.assets_webclient'
1518 var currentScript = document.currentScript;
1519 if (!currentScript) {
1520 var sc = document.getElementsByTagName('script');
1521 currentScript = sc[sc.length-1];
1523 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1524 client.insertAfter(currentScript);
1529 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: