summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/img/k-waiting1.gifbin29257 -> 0 bytes
-rw-r--r--addons/web/static/src/img/k-waiting2.gifbin276059 -> 0 bytes
-rw-r--r--addons/web/static/src/js/action_manager.js534
-rw-r--r--addons/web/static/src/js/boot.js159
-rw-r--r--addons/web/static/src/js/chrome.js1643
-rw-r--r--addons/web/static/src/js/core.js787
-rw-r--r--addons/web/static/src/js/formats.js398
-rw-r--r--addons/web/static/src/js/framework/ajax.js286
-rw-r--r--addons/web/static/src/js/framework/class.js154
-rw-r--r--addons/web/static/src/js/framework/core.js191
-rw-r--r--addons/web/static/src/js/framework/crash_manager.js195
-rw-r--r--addons/web/static/src/js/framework/data.js (renamed from addons/web/static/src/js/data.js)487
-rw-r--r--addons/web/static/src/js/framework/dialog.js194
-rw-r--r--addons/web/static/src/js/framework/formats.js189
-rw-r--r--addons/web/static/src/js/framework/misc.js339
-rw-r--r--addons/web/static/src/js/framework/mixins.js328
-rw-r--r--addons/web/static/src/js/framework/model.js191
-rw-r--r--addons/web/static/src/js/framework/pyeval.js1018
-rw-r--r--addons/web/static/src/js/framework/registry.js54
-rw-r--r--addons/web/static/src/js/framework/session.js393
-rw-r--r--addons/web/static/src/js/framework/time.js285
-rw-r--r--addons/web/static/src/js/framework/translation.js98
-rw-r--r--addons/web/static/src/js/framework/utils.js383
-rw-r--r--addons/web/static/src/js/framework/view.js239
-rw-r--r--addons/web/static/src/js/framework/widget.js367
-rw-r--r--addons/web/static/src/js/menu.js261
-rw-r--r--addons/web/static/src/js/openerpframework.js1564
-rw-r--r--addons/web/static/src/js/pyeval.js1012
-rw-r--r--addons/web/static/src/js/search.js2333
-rw-r--r--addons/web/static/src/js/test_support.js83
-rw-r--r--addons/web/static/src/js/testing.js304
-rw-r--r--addons/web/static/src/js/tour.js57
-rw-r--r--addons/web/static/src/js/view_form.js6443
-rw-r--r--addons/web/static/src/js/view_list_editable.js830
-rw-r--r--addons/web/static/src/js/view_manager.js514
-rw-r--r--addons/web/static/src/js/views.js1593
-rw-r--r--addons/web/static/src/js/views/form_common.js1135
-rw-r--r--addons/web/static/src/js/views/form_relational_widgets.js2028
-rw-r--r--addons/web/static/src/js/views/form_view.js1587
-rw-r--r--addons/web/static/src/js/views/form_widgets.js1590
-rw-r--r--addons/web/static/src/js/views/graph_view.js128
-rw-r--r--addons/web/static/src/js/views/graph_widget.js (renamed from addons/web/static/src/js/view_graph.js)168
-rw-r--r--addons/web/static/src/js/views/list_common.js402
-rw-r--r--addons/web/static/src/js/views/list_view.js (renamed from addons/web/static/src/js/view_list.js)620
-rw-r--r--addons/web/static/src/js/views/list_view_editable.js843
-rw-r--r--addons/web/static/src/js/views/pivot_view.js (renamed from addons/web/static/src/js/view_pivot.js)70
-rw-r--r--addons/web/static/src/js/views/search_filters.js311
-rw-r--r--addons/web/static/src/js/views/search_inputs.js736
-rw-r--r--addons/web/static/src/js/views/search_menus.js437
-rw-r--r--addons/web/static/src/js/views/search_view.js945
-rw-r--r--addons/web/static/src/js/views/tree_view.js (renamed from addons/web/static/src/js/view_tree.js)40
-rw-r--r--addons/web/static/src/js/watch.js58
-rw-r--r--addons/web/static/src/js/web_client.js368
-rw-r--r--addons/web/static/src/js/widgets/change_password.js47
-rw-r--r--addons/web/static/src/js/widgets/data_export.js (renamed from addons/web/static/src/js/data_export.js)36
-rw-r--r--addons/web/static/src/js/widgets/database_manager.js217
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js114
-rw-r--r--addons/web/static/src/js/widgets/loading.js66
-rw-r--r--addons/web/static/src/js/widgets/notification.js44
-rw-r--r--addons/web/static/src/js/widgets/sidebar.js215
-rw-r--r--addons/web/static/src/js/widgets/systray_menu.js38
-rw-r--r--addons/web/static/src/js/widgets/user_menu.js112
-rw-r--r--addons/web/static/src/xml/base.xml4
63 files changed, 18085 insertions, 18180 deletions
diff --git a/addons/web/static/src/img/k-waiting1.gif b/addons/web/static/src/img/k-waiting1.gif
deleted file mode 100644
index b76cdfaac23..00000000000
--- a/addons/web/static/src/img/k-waiting1.gif
+++ /dev/null
Binary files differ
diff --git a/addons/web/static/src/img/k-waiting2.gif b/addons/web/static/src/img/k-waiting2.gif
deleted file mode 100644
index b0b79e80830..00000000000
--- a/addons/web/static/src/img/k-waiting2.gif
+++ /dev/null
Binary files differ
diff --git a/addons/web/static/src/js/action_manager.js b/addons/web/static/src/js/action_manager.js
new file mode 100644
index 00000000000..4c49a7a5772
--- /dev/null
+++ b/addons/web/static/src/js/action_manager.js
@@ -0,0 +1,534 @@
+odoo.define('web.ActionManager', ['web.core', 'web.crash_manager', 'web.data', 'web.Dialog', 'web.framework', 'web.pyeval', 'web.session', 'web.ViewManager', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+var ViewManager = require('web.ViewManager');
+var Widget = require('web.Widget');
+
+var CompoundContext = data.CompoundContext;
+
+var ActionManager = Widget.extend({
+ template: "ActionManager",
+ init: function(parent) {
+ this._super(parent);
+ this.inner_action = null;
+ this.inner_widget = null;
+ this.webclient = parent;
+ this.dialog = null;
+ this.dialog_widget = null;
+ this.widgets = [];
+ this.on('history_back', this, this.proxy('history_back'));
+ },
+ dialog_stop: function (reason) {
+ if (this.dialog) {
+ this.dialog.destroy(reason);
+ }
+ this.dialog = null;
+ },
+ /**
+ * Add a new widget to the action manager
+ *
+ * widget: typically, widgets added are instance.web.ViewManager. The action manager
+ * uses this list of widget to handle the breadcrumbs.
+ * action: new action
+ * options.on_reverse_breadcrumb: will be called when breadcrumb is selected
+ * options.clear_breadcrumbs: boolean, if true, current widgets are destroyed
+ * options.replace_breadcrumb: boolean, if true, replace current breadcrumb
+ */
+ push_widget: function(widget, action, options) {
+ var self = this,
+ to_destroy,
+ old_widget = this.inner_widget;
+ options = options || {};
+
+ if (options.clear_breadcrumbs) {
+ to_destroy = this.widgets;
+ this.widgets = [];
+ } else if (options.replace_breadcrumb) {
+ to_destroy = _.last(this.widgets);
+ this.widgets = _.initial(this.widgets);
+ }
+ if (widget instanceof Widget) {
+ var title = widget.get('title') || action.display_name || action.name;
+ widget.set('title', title);
+ this.widgets.push(widget);
+ } else {
+ this.widgets.push({
+ view_stack: [{
+ controller: {get: function () {return action.display_name || action.name; }},
+ }],
+ destroy: function () {},
+ });
+ }
+ _.last(this.widgets).__on_reverse_breadcrumb = options.on_reverse_breadcrumb;
+ this.inner_action = action;
+ this.inner_widget = widget;
+ return $.when(this.inner_widget.appendTo(this.$el)).done(function () {
+ if ((action.target !== 'inline') && (!action.flags.headless) && widget.$header) {
+ widget.$header.show();
+ }
+ if (old_widget) {
+ old_widget.$el.hide();
+ }
+ if (options.clear_breadcrumbs) {
+ self.clear_widgets(to_destroy);
+ }
+ });
+ },
+ get_breadcrumbs: function () {
+ return _.flatten(_.map(this.widgets, function (widget) {
+ if (widget instanceof ViewManager) {
+ return widget.view_stack.map(function (view, index) {
+ return {
+ title: view.controller.get('title') || widget.title,
+ index: index,
+ widget: widget,
+ };
+ });
+ } else {
+ return {title: widget.get('title'), widget: widget };
+ }
+ }), true);
+ },
+ get_title: function () {
+ if (this.widgets.length === 1) {
+ // horrible hack to display the action title instead of "New" for the actions
+ // that use a form view to edit something that do not correspond to a real model
+ // for example, point of sale "Your Session" or most settings form,
+ var widget = this.widgets[0];
+ if (widget instanceof ViewManager && widget.view_stack.length === 1) {
+ return widget.title;
+ }
+ }
+ return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
+ },
+ get_widgets: function () {
+ return this.widgets.slice(0);
+ },
+ history_back: function() {
+ var widget = _.last(this.widgets);
+ if (widget instanceof ViewManager) {
+ var nbr_views = widget.view_stack.length;
+ if (nbr_views > 1) {
+ return this.select_widget(widget, nbr_views - 2);
+ }
+ }
+ if (this.widgets.length > 1) {
+ widget = this.widgets[this.widgets.length - 2];
+ var index = widget.view_stack && widget.view_stack.length - 1;
+ return this.select_widget(widget, index);
+ }
+ return $.Deferred().reject();
+ },
+ select_widget: function(widget, index) {
+ var self = this;
+ if (this.webclient.has_uncommitted_changes()) {
+ return $.Deferred().reject();
+ }
+ var widget_index = this.widgets.indexOf(widget),
+ def = $.when(widget.select_view && widget.select_view(index));
+
+ return def.done(function () {
+ if (widget.__on_reverse_breadcrumb) {
+ widget.__on_reverse_breadcrumb();
+ }
+ _.each(self.widgets.splice(widget_index + 1), function (w) {
+ w.destroy();
+ });
+ self.inner_widget = _.last(self.widgets);
+ if (self.inner_widget.display_breadcrumbs) {
+ self.inner_widget.display_breadcrumbs();
+ }
+ if (self.inner_widget.do_show) {
+ self.inner_widget.do_show();
+ }
+ });
+ },
+ clear_widgets: function(widgets) {
+ _.invoke(widgets || this.widgets, 'destroy');
+ if (!widgets) {
+ this.widgets = [];
+ this.inner_widget = null;
+ }
+ },
+ do_push_state: function(state) {
+ if (!this.webclient || !this.webclient.do_push_state || this.dialog) {
+ return;
+ }
+ state = state || {};
+ if (this.inner_action) {
+ if (this.inner_action._push_me === false) {
+ // this action has been explicitly marked as not pushable
+ return;
+ }
+ state.title = this.get_title();
+ if(this.inner_action.type == 'ir.actions.act_window') {
+ state.model = this.inner_action.res_model;
+ }
+ if (this.inner_action.menu_id) {
+ state.menu_id = this.inner_action.menu_id;
+ }
+ if (this.inner_action.id) {
+ state.action = this.inner_action.id;
+ } else if (this.inner_action.type == 'ir.actions.client') {
+ state.action = this.inner_action.tag;
+ var params = {};
+ _.each(this.inner_action.params, function(v, k) {
+ if(_.isString(v) || _.isNumber(v)) {
+ params[k] = v;
+ }
+ });
+ state = _.extend(params || {}, state);
+ }
+ if (this.inner_action.context) {
+ var active_id = this.inner_action.context.active_id;
+ if (active_id) {
+ state.active_id = active_id;
+ }
+ var active_ids = this.inner_action.context.active_ids;
+ if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
+ // We don't push active_ids if it's a single element array containing the active_id
+ // This makes the url shorter in most cases.
+ state.active_ids = this.inner_action.context.active_ids.join(',');
+ }
+ }
+ }
+ this.webclient.do_push_state(state);
+ },
+ do_load_state: function(state, warm) {
+ var self = this,
+ action_loaded;
+ if (state.action) {
+ if (_.isString(state.action) && core.action_registry.contains(state.action)) {
+ var action_client = {
+ type: "ir.actions.client",
+ tag: state.action,
+ params: state,
+ _push_me: state._push_me,
+ };
+ if (warm) {
+ this.null_action();
+ }
+ action_loaded = this.do_action(action_client);
+ } else {
+ var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
+ if (run_action) {
+ var add_context = {};
+ if (state.active_id) {
+ add_context.active_id = state.active_id;
+ }
+ if (state.active_ids) {
+ // The jQuery BBQ plugin does some parsing on values that are valid integers.
+ // It means that if there's only one item, it will do parseInt() on it,
+ // otherwise it will keep the comma seperated list as string.
+ add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
+ return parseInt(id, 10) || id;
+ });
+ } else if (state.active_id) {
+ add_context.active_ids = [state.active_id];
+ }
+ add_context.params = state;
+ if (warm) {
+ this.null_action();
+ }
+ action_loaded = this.do_action(state.action, { additional_context: add_context });
+ $.when(action_loaded || null).done(function() {
+ self.webclient.menu.is_bound.done(function() {
+ if (self.inner_action && self.inner_action.id) {
+ self.webclient.menu.open_action(self.inner_action.id);
+ }
+ });
+ });
+ }
+ }
+ } else if (state.model && state.id) {
+ // TODO handle context & domain ?
+ if (warm) {
+ this.null_action();
+ }
+ var action = {
+ res_model: state.model,
+ res_id: state.id,
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']]
+ };
+ action_loaded = this.do_action(action);
+ } else if (state.sa) {
+ // load session action
+ if (warm) {
+ this.null_action();
+ }
+ action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).then(function(action) {
+ if (action) {
+ return self.do_action(action);
+ }
+ });
+ }
+
+ $.when(action_loaded || null).done(function() {
+ if (self.inner_widget && self.inner_widget.do_load_state) {
+ self.inner_widget.do_load_state(state, warm);
+ }
+ });
+ },
+ /**
+ * Execute an OpenERP action
+ *
+ * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
+ * @param {Object} [options]
+ * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
+ * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action
+ * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
+ * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
+ * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
+ * @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
+ * @param {Object} [options.additional_context] Additional context to be merged with the action's context.
+ * @return {jQuery.Deferred} Action loaded
+ */
+ do_action: function(action, options) {
+ options = _.defaults(options || {}, {
+ clear_breadcrumbs: false,
+ on_reverse_breadcrumb: function() {},
+ hide_breadcrumb: false,
+ on_close: function() {},
+ action_menu_id: null,
+ additional_context: {},
+ });
+
+ if (action === false) {
+ action = { type: 'ir.actions.act_window_close' };
+ } else if (_.isString(action) && core.action_registry.contains(action)) {
+ var action_client = { type: "ir.actions.client", tag: action, params: {} };
+ return this.do_action(action_client, options);
+ } else if (_.isNumber(action) || _.isString(action)) {
+ var self = this;
+ var additional_context = {
+ active_id : options.additional_context.active_id,
+ active_ids : options.additional_context.active_ids,
+ active_model : options.additional_context.active_model
+ };
+ return self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) {
+ return self.do_action(result, options);
+ });
+ }
+
+ core.bus.trigger('action', action);
+
+ // Ensure context & domain are evaluated and can be manipulated/used
+ var ncontext = new CompoundContext(options.additional_context, action.context || {});
+ action.context = pyeval.eval('context', ncontext);
+ if (action.context.active_id || action.context.active_ids) {
+ // Here we assume that when an `active_id` or `active_ids` is used
+ // in the context, we are in a `related` action, so we disable the
+ // searchview's default custom filters.
+ action.context.search_disable_custom_filters = true;
+ }
+ if (action.domain) {
+ action.domain = pyeval.eval(
+ 'domain', action.domain, action.context || {});
+ }
+
+ if (!action.type) {
+ console.error("No type for action", action);
+ return $.Deferred().reject();
+ }
+ var type = action.type.replace(/\./g,'_');
+ var popup = action.target === 'new';
+ var inline = action.target === 'inline' || action.target === 'inlineview';
+ var form = _.str.startsWith(action.view_mode, 'form');
+ action.flags = _.defaults(action.flags || {}, {
+ views_switcher : !popup && !inline,
+ search_view : !popup && !inline,
+ action_buttons : !popup && !inline,
+ sidebar : !popup && !inline,
+ pager : (!popup || !form) && !inline,
+ display_title : !popup,
+ headless: (popup || inline) && form,
+ search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
+ });
+ action.menu_id = options.action_menu_id;
+ action.context.params = _.extend({ 'action' : action.id }, action.context.params);
+ if (!(type in this)) {
+ console.error("Action manager can't handle action of type " + action.type, action);
+ return $.Deferred().reject();
+ }
+ return this[type](action, options);
+ },
+ null_action: function() {
+ this.dialog_stop();
+ this.clear_widgets();
+ },
+ /**
+ *
+ * @param {Object} executor
+ * @param {Object} executor.action original action
+ * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
+ * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
+ * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
+ * @param {Object} options
+ * @return {*}
+ */
+ ir_actions_common: function(executor, options) {
+ var widget;
+ if (executor.action.target === 'new') {
+ var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null;
+ if (pre_dialog){
+ // prevent previous dialog to consider itself closed,
+ // right now, as we're opening a new one (prevents
+ // reload of original form view)
+ pre_dialog.off('closing', null, pre_dialog.on_close);
+ }
+ if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
+ this.dialog_widget.destroy();
+ }
+ // explicitly passing a closing action to dialog_stop() prevents
+ // it from reloading the original form view
+ this.dialog_stop(executor.action);
+ this.dialog = new Dialog(this, {
+ title: executor.action.name,
+ dialogClass: executor.klass,
+ });
+
+ // chain on_close triggers with previous dialog, if any
+ this.dialog.on_close = function(){
+ options.on_close.apply(null, arguments);
+ if (pre_dialog && pre_dialog.on_close){
+ // no parameter passed to on_close as this will
+ // only be called when the last dialog is truly
+ // closing, and *should* trigger a reload of the
+ // underlying form view (see comments above)
+ pre_dialog.on_close();
+ }
+ };
+ this.dialog.on("closing", null, this.dialog.on_close);
+ widget = executor.widget();
+ if (widget instanceof ViewManager) {
+ _.extend(widget.flags, {
+ $buttons: this.dialog.$buttons,
+ footer_to_buttons: true,
+ });
+ if (widget.action.view_mode === 'form') {
+ widget.flags.headless = true;
+ }
+ }
+ this.dialog_widget = widget;
+ this.dialog_widget.setParent(this.dialog);
+ var initialized = this.dialog_widget.appendTo(this.dialog.$el);
+ this.dialog.open();
+ return $.when(initialized);
+ }
+ if (this.inner_widget && this.webclient.has_uncommitted_changes()) {
+ return $.Deferred().reject();
+ }
+ widget = executor.widget();
+ this.dialog_stop(executor.action);
+ return this.push_widget(widget, executor.action, options);
+ },
+ ir_actions_act_window: function (action, options) {
+ var self = this;
+
+ return this.ir_actions_common({
+ widget: function () {
+ return new ViewManager(self, null, null, null, action);
+ },
+ action: action,
+ klass: 'oe_act_window',
+ }, options);
+ },
+ ir_actions_client: function (action, options) {
+ var self = this;
+ var ClientWidget = core.action_registry.get(action.tag);
+ if (!ClientWidget) {
+ return self.do_warn("Action Error", "Could not find client action '" + action.tag + "'.");
+ }
+
+ if (!(ClientWidget.prototype instanceof Widget)) {
+ var next;
+ if ((next = ClientWidget(this, action))) {
+ return this.do_action(next, options);
+ }
+ return $.when();
+ }
+
+ return this.ir_actions_common({
+ widget: function () { return new ClientWidget(self, action); },
+ action: action,
+ klass: 'oe_act_client',
+ }, options).then(function () {
+ if (action.tag !== 'reload') {self.do_push_state({});}
+ });
+ },
+ ir_actions_act_window_close: function (action, options) {
+ if (!this.dialog) {
+ options.on_close();
+ }
+ this.dialog_stop();
+ return $.when();
+ },
+ ir_actions_server: function (action, options) {
+ var self = this;
+ this.rpc('/web/action/run', {
+ action_id: action.id,
+ context: action.context || {}
+ }).done(function (action) {
+ self.do_action(action, options);
+ });
+ },
+ ir_actions_report_xml: function(action, options) {
+ var self = this;
+ framework.blockUI();
+ action = _.clone(action);
+ var eval_contexts = ([session.user_context] || []).concat([action.context]);
+ action.context = pyeval.eval('contexts',eval_contexts);
+
+ // iOS devices doesn't allow iframe use the way we do it,
+ // opening a new window seems the best way to workaround
+ if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
+ var params = {
+ action: JSON.stringify(action),
+ token: new Date().getTime()
+ };
+ var url = self.session.url('/web/report', params);
+ framework.unblockUI();
+ $('<a href="'+url+'" target="_blank"></a>')[0].click();
+ return;
+ }
+ var c = crash_manager;
+ return $.Deferred(function (d) {
+ self.session.get_file({
+ url: '/web/report',
+ data: {action: JSON.stringify(action)},
+ complete: framework.unblockUI,
+ success: function(){
+ if (!self.dialog) {
+ options.on_close();
+ }
+ self.dialog_stop();
+ d.resolve();
+ },
+ error: function () {
+ c.rpc_error.apply(c, arguments);
+ d.reject();
+ }
+ });
+ });
+ },
+ ir_actions_act_url: function (action) {
+ if (action.target === 'self') {
+ framework.redirect(action.url);
+ } else {
+ window.open(action.url, '_blank');
+ }
+ return $.when();
+ },
+});
+
+return ActionManager;
+
+});
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js
index 5fad3d0a140..67834ebf6b2 100644
--- a/addons/web/static/src/js/boot.js
+++ b/addons/web/static/src/js/boot.js
@@ -6,50 +6,129 @@
* @name openerp
* @namespace openerp
*/
+
(function() {
- // copy everything in the openerp namespace to openerp.web
- openerp.web = _.clone(openerp);
-
- var inited = false;
-
- _.extend(openerp, {
- // Per session namespace
- // openerp.<module> will map to
- // openerp.instances.sessionname.<module> using a closure
- instances: {instance0: openerp},
- // links to the global openerp
- _openerp: openerp,
- // this unique id will be replaced by hostname_databasename by
- // openerp.web.Session on the first connection
- _session_id: "instance0",
- _modules: openerp._modules || ['web'],
- web_mobile: {},
- /**
- * OpenERP instance constructor
- *
- * @param {Array|String} modules list of modules to initialize
- */
- init: function(modules) {
- if (modules === undefined) {
- modules = openerp._modules;
+ "use strict";
+
+ var jobs = [];
+ var factories = Object.create(null);
+ var job_names = [];
+ var job_deps = [];
+
+ var services = Object.create({
+ qweb: new QWeb2.Engine(),
+ $: $,
+ _: _,
+ });
+
+ var debug = ($.deparam($.param.querystring()).debug !== undefined);
+
+ var odoo = window.odoo = {
+ testing: typeof QUnit === "object",
+ debug: debug,
+
+ __DEBUG__: {
+ get_dependencies: function (name, transitive) {
+ var deps = name instanceof Array ? name: [name],
+ changed;
+ do {
+ changed = false;
+ _.each(job_deps, function (dep) {
+ if (_.contains(deps, dep.to) && (!_.contains(deps, dep.from))) {
+ deps.push(dep.from);
+ changed = true;
+ }
+ });
+ } while (changed && transitive)
+ return deps;
+ },
+ get_dependents: function (name) {
+ return _.pluck(_.where(job_deps, {from: name}), 'to');
+ },
+ factories: factories,
+ services: services,
+ },
+ define: function () {
+ var args = Array.prototype.slice.call(arguments),
+ name = typeof args[0] === 'string' ? args.shift() : _.uniqueId('__job'),
+ deps = args[0] instanceof Array ? args.shift() : [],
+ factory = args[0];
+
+ if (odoo.debug) {
+ if (!(deps instanceof Array)) {
+ throw new Error ('Dependencies should be defined by an array', deps);
+ }
+ if (typeof factory !== 'function') {
+ throw new Error ('Factory should be defined by a function', factory);
+ }
+ if (typeof name !== 'string') {
+ throw new Error("Invalid name definition (should be a string", name);
+ }
+ if (name in factories) {
+ throw new Error("Service " + name + " already defined");
+ }
}
- modules = _.without(modules, "web");
- if (inited)
- throw new Error("OpenERP was already inited");
- inited = true;
- for(var i=0; i < modules.length; i++) {
- var fct = openerp[modules[i]];
- if (typeof(fct) === "function") {
- openerp[modules[i]] = {};
- for (var k in fct) {
- openerp[modules[i]][k] = fct[k];
+
+ factory.deps = deps;
+ factories[name] = factory;
+
+ jobs.push({
+ name: name,
+ factory: factory,
+ deps: deps,
+ });
+
+ job_names.push(name);
+ _.each(deps, function (dep) {
+ job_deps.push({from:dep, to:name});
+ });
+
+ this.process_jobs(jobs, services);
+ },
+ init: function () {
+ odoo.__DEBUG__.remaining_jobs = jobs;
+ odoo.__DEBUG__.web_client = services['web.web_client'];
+
+ if (jobs.length) {
+ console.warn('Warning: not all jobs could be started.', jobs);
+ }
+ // _.each(factories, function (value, key) {
+ // delete factories[key];
+ // });
+ },
+ process_jobs: function (jobs, services) {
+ var job, require;
+ while (jobs.length && (job = _.find(jobs, is_ready))) {
+ require = make_require(job);
+
+ services[job.name] = job.factory.call(null, require);
+ if (require.__require_calls !== job.deps.length) {
+ console.warn('Job ' + job.name + ' did not require all its dependencies');
+ }
+ jobs.splice(jobs.indexOf(job), 1);
+ }
+ return services;
+
+ function is_ready (job) {
+ return _.every(job.factory.deps, function (name) { return name in services; });
+ }
+
+ function make_require (job) {
+ var deps = _.pick(services, job.deps);
+
+ function require (name) {
+ if (!(name in deps)) {
+ console.error('Undefined dependency: ', name);
+ } else {
+ require.__require_calls++;
}
- fct(openerp, openerp[modules[i]]);
+ return deps[name];
}
+
+ require.__require_calls = 0;
+ return require;
}
- openerp._modules = ['web'].concat(modules);
- return openerp;
}
- });
-})();
+ };
+})();
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
deleted file mode 100644
index 8eabafb5f2d..00000000000
--- a/addons/web/static/src/js/chrome.js
+++ /dev/null
@@ -1,1643 +0,0 @@
-/*---------------------------------------------------------
- * OpenERP Web chrome
- *---------------------------------------------------------*/
-(function() {
-
-var instance = openerp;
-openerp.web.chrome = {};
-
-var QWeb = instance.web.qweb,
- _t = instance.web._t,
- _lt = instance.web._lt;
-
-instance.web.Notification = instance.web.Widget.extend({
- template: 'Notification',
- init: function() {
- this._super.apply(this, arguments);
- instance.web.notification = this;
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$el.notify({
- speed: 500,
- expires: 2500
- });
- },
- notify: function(title, text, sticky) {
- sticky = !!sticky;
- var opts = {};
- if (sticky) {
- opts.expires = false;
- }
- return this.$el.notify('create', {
- title: title,
- text: text
- }, opts);
- },
- warn: function(title, text, sticky) {
- sticky = !!sticky;
- var opts = {};
- if (sticky) {
- opts.expires = false;
- }
- return this.$el.notify('create', 'oe_notification_alert', {
- title: title,
- text: text
- }, opts);
- }
-});
-
-var opened_modal = [];
-
-instance.web.action_notify = function(element, action) {
- element.do_notify(action.params.title, action.params.text, action.params.sticky);
-};
-instance.web.client_actions.add("action_notify", "instance.web.action_notify");
-
-instance.web.action_warn = function(element, action) {
- element.do_warn(action.params.title, action.params.text, action.params.sticky);
-};
-instance.web.client_actions.add("action_warn", "instance.web.action_warn");
-
-/**
- A useful class to handle dialogs.
-
- Attributes:
- - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
- during the lifecycle of the dialog.
-*/
-instance.web.Dialog = instance.web.Widget.extend({
- dialog_title: "",
- /**
- Constructor.
-
- @param {Widget} parent
- @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
- dictionary can contain the following keys:
- - size: one of the following: 'large', 'medium', 'small'
- - dialogClass: class to add to the body of dialog
- - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
- label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
- corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
- in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
- feature does not allow that kind of personalization.
- - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
- @param {jQuery object} content Some content to replace this.$el .
- */
- init: function (parent, options, content) {
- var self = this;
- this._super(parent);
- this.content_to_set = content;
- this.dialog_options = {
- destroy_on_close: true,
- size: 'large', //'medium', 'small'
- buttons: null,
- };
- if (options) {
- _.extend(this.dialog_options, options);
- }
- this.on("closing", this, this._closing);
- this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
- },
- renderElement: function() {
- if (this.content_to_set) {
- this.setElement(this.content_to_set);
- } else if (this.template) {
- this._super();
- }
- },
- /**
- Opens the popup. Inits the dialog if it is not already inited.
-
- @return this
- */
- open: function() {
- if (!this.dialog_inited) {
- this.init_dialog();
- }
- this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
- $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal is opened
- //add to list of currently opened modal
- opened_modal.push(this.$dialog_box);
- return this;
- },
- _add_buttons: function(buttons) {
- var self = this;
- var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
- _.each(buttons, function(fn, text) {
- // buttons can be object or array
- var pre_text = fn.pre_text || "";
- var post_text = fn.post_text || "";
- var oe_link_class = fn.oe_link_class;
- if (!_.isFunction(fn)) {
- text = fn.text;
- fn = fn.click;
- }
- var $but = $(QWeb.render('WidgetButton', { widget : { pre_text: pre_text, post_text: post_text, string: text, node: { attrs: {'class': oe_link_class} }}}));
- $customButons.append($but);
- $but.filter('button').on('click', function(ev) {
- fn.call(self.$el, ev);
- });
- });
- },
- /**
- Initializes the popup.
-
- @return The result returned by start().
- */
- init_dialog: function() {
- var self = this;
- var options = _.extend({}, this.dialog_options);
- options.title = options.title || this.dialog_title;
- if (options.buttons) {
- this._add_buttons(options.buttons);
- delete(options.buttons);
- }
- this.renderElement();
- this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
- this.$el.modal({
- 'backdrop': false,
- 'keyboard': true,
- });
- if (options.size !== 'large'){
- var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
- if (options.size === 'small'){
- dialog_class_size.addClass('modal-sm');
- }
- }
-
- this.$el.appendTo(this.$dialog_box.find(".modal-body"));
- var $dialog_content = this.$dialog_box.find('.modal-content');
- if (options.dialogClass){
- $dialog_content.find(".modal-body").addClass(options.dialogClass);
- }
- $dialog_content.openerpClass();
-
- this.$dialog_box.on('hidden.bs.modal', this, function() {
- self.close();
- });
- this.$dialog_box.modal('show');
-
- this.dialog_inited = true;
- var res = this.start();
- return res;
- },
- /**
- Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
- */
- close: function(reason) {
- if (this.dialog_inited && !this.__tmp_dialog_hiding) {
- $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
- if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal
- this.__tmp_dialog_hiding = true;
- this.$dialog_box.modal('hide');
- this.__tmp_dialog_hiding = undefined;
- }
- this.trigger("closing", reason);
- }
- },
- _closing: function() {
- if (this.__tmp_dialog_destroying)
- return;
- if (this.dialog_options.destroy_on_close) {
- this.__tmp_dialog_closing = true;
- this.destroy();
- this.__tmp_dialog_closing = undefined;
- }
- },
- /**
- Destroys the popup, also closes it.
- */
- destroy: function (reason) {
- this.$buttons.remove();
- var self = this;
- _.each(this.getChildren(), function(el) {
- el.destroy();
- });
- if (! this.__tmp_dialog_closing) {
- this.__tmp_dialog_destroying = true;
- this.close(reason);
- this.__tmp_dialog_destroying = undefined;
- }
- if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
- //we need this to put the instruction to remove modal from DOM at the end
- //of the queue, otherwise it might already have been removed before the modal-backdrop
- //is removed when pressing escape key
- var $element = this.$dialog_box;
- setTimeout(function () {
- //remove modal from list of opened modal since we just destroy it
- var modal_list_index = $.inArray($element, opened_modal);
- if (modal_list_index > -1){
- opened_modal.splice(modal_list_index,1)[0].remove();
- }
- if (opened_modal.length > 0){
- //we still have other opened modal so we should focus it
- opened_modal[opened_modal.length-1].focus();
- //keep class modal-open (deleted by bootstrap hide fnct) on body
- //to allow scrolling inside the modal
- $('body').addClass('modal-open');
- }
- },0);
- }
- this._super();
- }
-});
-
-instance.web.CrashManager = instance.web.Class.extend({
- init: function() {
- this.active = true;
- },
-
- rpc_error: function(error) {
- if (!this.active) {
- return;
- }
- if (error.code == -32098) {
- $.blockUI({ message: '' , overlayCSS: {'z-index': 9999, backgroundColor: '#FFFFFF', opacity: 0.0, cursor: 'wait'}});
- var $indicator = $('<div class="oe_indicator">' + _t("Trying to reconnect... ") + '<i class="fa fa-refresh fa-spin"></i></div>');
- $indicator.prependTo("body");
- var timeinterval = setInterval(function(){
- openerp.jsonRpc('/web/webclient/version_info').then(function() {
- clearInterval(timeinterval);
- $indicator.html(_t("You are back online"));
- $indicator.delay(2000).fadeOut('slow',function(){
- $indicator.remove();
- });
- $.unblockUI();
- });
- }, 2000);
- return;
- }
- var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
- if (handler) {
- new (handler)(this, error).display();
- return;
- }
- if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
- this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
- return;
- }
- var map_title ={
- user_error: _lt('Warning'),
- warning: _lt('Warning'),
- access_error: _lt('Access Error'),
- missing_error: _lt('Missing Record'),
- validation_error: _lt('Validation Error'),
- except_orm: _lt('Global Business Error'),
- access_denied: _lt('Access Denied'),
- };
- if (_.has(map_title, error.data.exception_type)) {
- if(error.data.exception_type == 'except_orm'){
- if(error.data.arguments[1]) {
- error = _.extend({}, error,
- {
- data: _.extend({}, error.data,
- {
- message: error.data.arguments[1],
- title: error.data.arguments[0] !== 'Warning' ? (" - " + error.data.arguments[0]) : '',
- })
- });
- }
- else {
- error = _.extend({}, error,
- {
- data: _.extend({}, error.data,
- {
- message: error.data.arguments[0],
- title: '',
- })
- });
- }
- }
- else {
- error = _.extend({}, error,
- {
- data: _.extend({}, error.data,
- {
- message: error.data.arguments[0],
- title: map_title[error.data.exception_type] !== 'Warning' ? (" - " + map_title[error.data.exception_type]) : '',
- })
- });
- }
-
- this.show_warning(error);
- //InternalError
-
- } else {
- this.show_error(error);
- }
- },
- show_warning: function(error) {
- if (!this.active) {
- return;
- }
- new instance.web.Dialog(this, {
- size: 'medium',
- title: "Odoo " + (_.str.capitalize(error.type) || _t("Warning")),
- subtitle: error.data.title,
- buttons: [
- {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
- ],
- }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
- },
- show_error: function(error) {
- if (!this.active) {
- return;
- }
- var buttons = {};
- buttons[_t("Ok")] = function() {
- this.parents('.modal').modal('hide');
- };
- new instance.web.Dialog(this, {
- title: "Odoo " + _.str.capitalize(error.type),
- buttons: buttons
- }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
- },
- show_message: function(exception) {
- this.show_error({
- type: _t("Client Error"),
- message: exception,
- data: {debug: ""}
- });
- },
-});
-
-/**
- An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
-*/
-instance.web.ExceptionHandler = {
- /**
- @param parent The parent.
- @param error The error object as returned by the JSON-RPC implementation.
- */
- init: function(parent, error) {},
- /**
- Called to inform to display the widget, if necessary. A typical way would be to implement
- this interface in a class extending instance.web.Dialog and simply display the dialog in this
- method.
- */
- display: function() {},
-};
-
-/**
- The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
- instance.web.ExceptionHandler.
-*/
-instance.web.crash_manager_registry = new instance.web.Registry();
-
-/**
- * Handle redirection warnings, which behave more or less like a regular
- * warning, with an additional redirection button.
- */
-instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
- init: function(parent, error) {
- this._super(parent);
- this.error = error;
- },
- display: function() {
- var self = this;
- error = this.error;
- error.data.message = error.data.arguments[0];
-
- new instance.web.Dialog(this, {
- size: 'medium',
- title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
- buttons: [
- {text: error.data.arguments[2],
- oe_link_class : 'oe_highlight',
- post_text : _t("or"),
- click: function() {
- window.location.href='#action='+error.data.arguments[1]
- self.destroy();
- }},
- {text: _t("Cancel"), oe_link_class: 'oe_link', click: function() { self.$el.parents('.modal').modal('hide'); self.destroy();}}
- ],
- }, QWeb.render('CrashManager.warning', {error: error})).open();
- }
-});
-instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
-
-instance.web.Loading = instance.web.Widget.extend({
- template: _t("Loading"),
- init: function(parent) {
- this._super(parent);
- this.count = 0;
- this.blocked_ui = false;
- this.session.on("request", this, this.request_call);
- this.session.on("response", this, this.response_call);
- this.session.on("response_failed", this, this.response_call);
- },
- destroy: function() {
- this.on_rpc_event(-this.count);
- this._super();
- },
- request_call: function() {
- this.on_rpc_event(1);
- },
- response_call: function() {
- this.on_rpc_event(-1);
- },
- on_rpc_event : function(increment) {
- var self = this;
- if (!this.count && increment === 1) {
- // Block UI after 3s
- this.long_running_timer = setTimeout(function () {
- self.blocked_ui = true;
- instance.web.blockUI();
- }, 3000);
- }
-
- this.count += increment;
- if (this.count > 0) {
- if (instance.session.debug) {
- this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
- } else {
- this.$el.text(_t("Loading"));
- }
- this.$el.show();
- this.getParent().$el.addClass('oe_wait');
- } else {
- this.count = 0;
- clearTimeout(this.long_running_timer);
- // Don't unblock if blocked by somebody else
- if (self.blocked_ui) {
- this.blocked_ui = false;
- instance.web.unblockUI();
- }
- this.$el.fadeOut();
- this.getParent().$el.removeClass('oe_wait');
- }
- }
-});
-
-instance.web.DatabaseManager = instance.web.Widget.extend({
- init: function(parent) {
- this._super(parent);
- this.unblockUIFunction = instance.web.unblockUI;
- $.validator.addMethod('matches', function (s, _, re) {
- return new RegExp(re).test(s);
- }, _t("Invalid database name"));
- },
- start: function() {
- var self = this;
- $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
- var fetch_db = this.rpc("/web/database/get_list", {}).then(
- function(result) {
- self.db_list = result;
- },
- function (_, ev) {
- ev.preventDefault();
- self.db_list = null;
- });
- var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
- self.lang_list = result;
- });
- return $.when(fetch_db, fetch_langs).always(self.do_render);
- },
- do_render: function() {
- var self = this;
- instance.webclient.toggle_bars(true);
- self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
- $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
- $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
- $('ul.oe_secondary_submenu > li:first').addClass('active');
- $('ul.oe_secondary_submenu > li').bind('click', function (event) {
- var menuitem = $(this);
- menuitem.addClass('active').siblings().removeClass('active');
- var form_id =menuitem.find('a').attr('href');
- $(form_id).show().siblings().hide();
- event.preventDefault();
- });
- $('#back-to-login').click(self.do_exit);
- self.$el.find("td").addClass("oe_form_group_cell");
- self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
- self.$el.find("label").addClass("oe_form_label");
- self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
- self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
- self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
- self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
- self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
- self.$el.find("form[name=change_pwd_form]").validate({
- messages: {
- old_pwd: _t("Please enter your previous password"),
- new_pwd: _t("Please enter your new password"),
- confirm_pwd: {
- required: _t("Please confirm your new password"),
- equalTo: _t("The confirmation does not match the password")
- }
- },
- submitHandler: self.do_change_password
- });
- },
- destroy: function () {
- this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
- this._super();
- },
- /**
- * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
- * from unblocking the UI
- */
- blockUI: function () {
- instance.web.blockUI();
- instance.web.unblockUI = function () {};
- },
- /**
- * Reinstates $.unblockUI so third parties can play with blockUI, and
- * unblocks the UI
- */
- unblockUI: function () {
- instance.web.unblockUI = this.unblockUIFunction;
- instance.web.unblockUI();
- },
- /**
- * Displays an error dialog resulting from the various RPC communications
- * failing over themselves
- *
- * @param {Object} error error description
- * @param {String} error.title title of the error dialog
- * @param {String} error.error message of the error dialog
- */
- display_error: function (error) {
- return new instance.web.Dialog(this, {
- size: 'medium',
- title: error.title,
- buttons: [
- {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
- ]
- }, $('<div>').html(error.error)).open();
- },
- do_create: function(form) {
- var self = this;
- var fields = $(form).serializeArray();
- self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
- if (result) {
- instance.web.redirect('/web');
- } else {
- alert("Failed to create database");
- }
- });
- },
- do_duplicate: function(form) {
- var self = this;
- var fields = $(form).serializeArray();
- self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
- if (result.error) {
- self.display_error(result);
- return;
- }
- self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
- self.start();
- });
- },
- do_drop: function(form) {
- var self = this;
- var $form = $(form),
- fields = $form.serializeArray(),
- $db_list = $form.find('[name=drop_db]'),
- db = $db_list.val();
- if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
- return;
- }
- self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
- if (result.error) {
- self.display_error(result);
- return;
- }
- self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
- self.start();
- });
- },
- do_backup: function(form) {
- var self = this;
- self.blockUI();
- self.session.get_file({
- form: form,
- success: function () {
- self.do_notify(_t("Backed"), _t("Database backed up successfully"));
- },
- error: function(error){
- if (error && error[1]) {
- self.display_error(error[1][0]);
- }
- },
- complete: function() {
- self.unblockUI();
- }
- });
- },
- do_restore: function(form) {
- var self = this;
- self.blockUI();
- $(form).ajaxSubmit({
- url: '/web/database/restore',
- type: 'POST',
- resetForm: true,
- success: function (body) {
- // If empty body, everything went fine
- if (!body) { return; }
-
- if (body.indexOf('403 Forbidden') !== -1) {
- self.display_error({
- title: _t("Access Denied"),
- error: _t("Incorrect super-administrator password")
- });
- } else {
- self.display_error({
- title: _t("Restore Database"),
- error: _t("Could not restore the database")
- });
- }
- },
- complete: function() {
- self.unblockUI();
- self.do_notify(_t("Restored"), _t("Database restored successfully"));
- }
- });
- },
- do_change_password: function(form) {
- var self = this;
- self.rpc("/web/database/change_password", {
- 'fields': $(form).serializeArray()
- }).done(function(result) {
- if (result.error) {
- self.display_error(result);
- return;
- }
- self.unblockUI();
- self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
- });
- },
- do_exit: function () {
- this.$el.remove();
- instance.web.redirect('/web');
- }
-});
-instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
-
-instance.web.login = function() {
- instance.web.redirect('/web/login');
-};
-instance.web.client_actions.add("login", "instance.web.login");
-
-instance.web.logout = function() {
- instance.web.redirect('/web/session/logout');
-};
-instance.web.client_actions.add("logout", "instance.web.logout");
-
-
-/**
- * Redirect to url by replacing window.location
- * If wait is true, sleep 1s and wait for the server i.e. after a restart.
- */
-instance.web.redirect = function(url, wait) {
- // Dont display a dialog if some xmlhttprequest are in progress
- if (instance.client && instance.client.crashmanager) {
- instance.client.crashmanager.active = false;
- }
-
- var load = function() {
- var old = "" + window.location;
- var old_no_hash = old.split("#")[0];
- var url_no_hash = url.split("#")[0];
- location.assign(url);
- if (old_no_hash === url_no_hash) {
- location.reload(true);
- }
- };
-
- var wait_server = function() {
- instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
- setTimeout(wait_server, 250);
- });
- };
-
- if (wait) {
- setTimeout(wait_server, 1000);
- } else {
- load();
- }
-};
-
-/**
- * Client action to reload the whole interface.
- * If params.menu_id, it opens the given menu entry.
- * If params.wait, reload will wait the openerp server to be reachable before reloading
- */
-instance.web.Reload = function(parent, action) {
- var params = action.params || {};
- var menu_id = params.menu_id || false;
- var l = window.location;
-
- var sobj = $.deparam(l.search.substr(1));
- if (params.url_search) {
- sobj = _.extend(sobj, params.url_search);
- }
- var search = '?' + $.param(sobj);
-
- var hash = l.hash;
- if (menu_id) {
- hash = "#menu_id=" + menu_id;
- }
- var url = l.protocol + "//" + l.host + l.pathname + search + hash;
-
- instance.web.redirect(url, params.wait);
-};
-instance.web.client_actions.add("reload", "instance.web.Reload");
-
-/**
- * Client action to refresh the session context (making sure
- * HTTP requests will have the right one) then reload the
- * whole interface.
- */
-instance.web.ReloadContext = function(parent, action) {
- // side-effect of get_session_info is to refresh the session context
- instance.session.rpc("/web/session/get_session_info", {}).then(function() {
- instance.web.Reload(parent, action);
- });
-}
-instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
-
-/**
- * Client action to go back in breadcrumb history.
- * If can't go back in history stack, will go back to home.
- */
-instance.web.HistoryBack = function(parent) {
- parent.history_back().fail(function() {
- instance.web.Home(parent);
- });
-};
-instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
-
-/**
- * Client action to go back home.
- */
-instance.web.Home = function(parent, action) {
- var url = '/' + (window.location.search || '');
- instance.web.redirect(url, action && action.params && action.params.wait);
-};
-instance.web.client_actions.add("home", "instance.web.Home");
-
-instance.web.ChangePassword = instance.web.Widget.extend({
- template: "ChangePassword",
- start: function() {
- var self = this;
- this.getParent().dialog_title = _t("Change Password");
- var $button = self.$el.find('.oe_form_button');
- $button.appendTo(this.getParent().$buttons);
- $button.eq(2).click(function(){
- self.$el.parents('.modal').modal('hide');
- });
- $button.eq(0).click(function(){
- self.rpc("/web/session/change_password",{
- 'fields': $("form[name=change_password_form]").serializeArray()
- }).done(function(result) {
- if (result.error) {
- self.display_error(result);
- return;
- } else {
- instance.webclient.on_logout();
- }
- });
- });
- },
- display_error: function (error) {
- return new instance.web.Dialog(this, {
- size: 'medium',
- title: error.title,
- buttons: [
- {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
- ]
- }, $('<div>').html(error.error)).open();
- },
-});
-instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
-
-instance.web.SystrayItems = [];
-instance.web.SystrayMenu = instance.web.Widget.extend({
- /**
- * This widget renders the systray menu. It creates and renders widgets
- * pushed in instance.web.SystrayItems.
- */
- init: function(parent) {
- this._super(parent);
- this.items = [];
- this.load = $.Deferred();
- },
- start: function() {
- var self = this;
- self._super.apply(this, arguments);
- self.load_items();
- return $.when.apply($, self.items).done(function () {
- self.load.resolve();
- });
- },
- load_items: function() {
- var self = this;
- _.each(instance.web.SystrayItems, function(widgetCls) {
- var cur_systray_item = new widgetCls(self);
- self.items.push(cur_systray_item.appendTo(self.$el));
- });
- },
-});
-
-instance.web.Menu = instance.web.Widget.extend({
- init: function() {
- var self = this;
- this._super.apply(this, arguments);
- this.is_bound = $.Deferred();
- this.data = {data:{children:[]}};
- this.on("menu_bound", this, function() {
- // launch the fetch of needaction counters, asynchronous
- var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
- var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
- if (!_.isEmpty(all_menu_ids)) {
- this.do_load_needaction(all_menu_ids);
- }
- });
- },
- start: function() {
- this._super.apply(this, arguments);
- return this.bind_menu();
- },
- do_reload: function() {
- var self = this;
- self.bind_menu();
- },
- bind_menu: function() {
- var self = this;
- this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
- this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
- this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
- // Hide second level submenus
- this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
- if (self.current_menu) {
- self.open_menu(self.current_menu);
- }
- this.trigger('menu_bound');
- var lazyreflow = _.debounce(this.reflow.bind(this), 200);
- instance.web.bus.on('resize', this, function() {
- if ($(window).width() < 768 ) {
- lazyreflow('all_outside');
- } else {
- lazyreflow();
- }
- });
- instance.web.bus.trigger('resize');
-
- this.is_bound.resolve();
- },
- do_load_needaction: function (menu_ids) {
- var self = this;
- menu_ids = _.compact(menu_ids);
- if (_.isEmpty(menu_ids)) {
- return $.when();
- }
- return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
- self.on_needaction_loaded(r);
- });
- },
- on_needaction_loaded: function(data) {
- var self = this;
- this.needaction_data = data;
- _.each(this.needaction_data, function (item, menu_id) {
- var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
- $item.find('.badge').remove();
- if (item.needaction_counter && item.needaction_counter > 0) {
- $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
- }
- });
- },
- /**
- * Reflow the menu items and dock overflowing items into a "More" menu item.
- * Automatically called when 'menu_bound' event is triggered and on window resizing.
- *
- * @param {string} behavior If set to 'all_outside', all the items are displayed.
- * If not set, only the overflowing items are hidden.
- */
- reflow: function(behavior) {
- var self = this;
- var $more_container = this.$('#menu_more_container').hide();
- var $more = this.$('#menu_more');
- var $systray = this.$el.parents().find('.oe_systray');
-
- $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
-
- // 'all_outside' beahavior should display all the items, so hide the more menu and exit
- if (behavior === 'all_outside') {
- // Show list of menu items
- self.$el.show();
- this.$el.find('li').show();
- $more_container.hide();
- return;
- }
-
- // Hide all menu items
- var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
- // Show list of menu items (which is empty for now since all menu items are hidden)
- self.$el.show();
- $toplevel_items.each(function() {
- var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
- self.$el.parent().children(':visible').each(function() {
- remaining_space -= $(this).outerWidth();
- });
-
- if ($(this).width() >= remaining_space) {
- return false; // the current item will be appended in more_container
- }
- $(this).show(); // show the current item in menu bar
- });
- $more.append($toplevel_items.filter(':hidden').show());
- $more_container.toggle(!!$more.children().length);
- // Hide toplevel item if there is only one
- var $toplevel = self.$el.children("li:visible");
- if ($toplevel.length === 1) {
- $toplevel.hide();
- }
- },
- /**
- * Opens a given menu by id, as if a user had browsed to that menu by hand
- * except does not trigger any event on the way
- *
- * @param {Number} id database id of the terminal menu to select
- */
- open_menu: function (id) {
- this.current_menu = id;
- this.session.active_id = id;
- var $clicked_menu, $sub_menu, $main_menu;
- $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
- this.trigger('open_menu', id, $clicked_menu);
-
- if (this.$secondary_menus.has($clicked_menu).length) {
- $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
- $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
- } else {
- $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
- $main_menu = $clicked_menu;
- }
-
- // Activate current main menu
- this.$el.find('.active').removeClass('active');
- $main_menu.parent().addClass('active');
-
- // Show current sub menu
- this.$secondary_menus.find('.oe_secondary_menu').hide();
- $sub_menu.show();
-
- // Hide/Show the leftbar menu depending of the presence of sub-items
- this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
-
- // Activate current menu item and show parents
- this.$secondary_menus.find('.active').removeClass('active');
- if ($main_menu !== $clicked_menu) {
- $clicked_menu.parents().show();
- if ($clicked_menu.is('.oe_menu_toggler')) {
- $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
- } else {
- $clicked_menu.parent().addClass('active');
- }
- }
- // add a tooltip to cropped menu items
- this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
- $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
- });
- },
- /**
- * Call open_menu with the first menu_item matching an action_id
- *
- * @param {Number} id the action_id to match
- */
- open_action: function (id) {
- var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
- var menu_id = $menu.data('menu');
- if (menu_id) {
- this.open_menu(menu_id);
- }
- },
- /**
- * Process a click on a menu item
- *
- * @param {Number} id the menu_id
- * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
- */
- menu_click: function(id, needaction) {
- if (!id) { return; }
-
- // find back the menuitem in dom to get the action
- var $item = this.$el.find('a[data-menu=' + id + ']');
- if (!$item.length) {
- $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
- }
- var action_id = $item.data('action-id');
- // If first level menu doesnt have action trigger first leaf
- if (!action_id) {
- if(this.$el.has($item).length) {
- var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
- var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
- if($items.length) {
- action_id = $items.data('action-id');
- id = $items.data('menu');
- }
- }
- }
- if (action_id) {
- this.trigger('menu_click', {
- action_id: action_id,
- needaction: needaction,
- id: id,
- previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
- }, $item);
- } else {
- console.log('Menu no action found web test 04 will fail');
- }
- this.open_menu(id);
- },
- do_reload_needaction: function () {
- var self = this;
- if (self.current_menu) {
- self.do_load_needaction([self.current_menu]).then(function () {
- self.trigger("need_action_reloaded");
- });
- }
- },
- /**
- * Jquery event handler for menu click
- *
- * @param {Event} ev the jquery event
- */
- on_top_menu_click: function(ev) {
- ev.preventDefault();
- var self = this;
- var id = $(ev.currentTarget).data('menu');
-
- // Fetch the menu leaves ids in order to check if they need a 'needaction'
- var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
- var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
- var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
-
- self.do_load_needaction(menu_ids).then(function () {
- self.trigger("need_action_reloaded");
- });
- this.$el.parents().find(".oe_secondary_menus_container").scrollTop(0,0);
-
- this.on_menu_click(ev);
- },
- on_menu_click: function(ev) {
- ev.preventDefault();
- var needaction = $(ev.target).is('div#menu_counter');
- this.menu_click($(ev.currentTarget).data('menu'), needaction);
- },
-});
-
-instance.web.UserMenu = instance.web.Widget.extend({
- template: "UserMenu",
- init: function(parent) {
- this._super(parent);
- this.update_promise = $.Deferred().resolve();
- },
- start: function() {
- var self = this;
- this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
- ev.preventDefault();
- var f = self['on_menu_' + $(this).data('menu')];
- if (f) {
- f($(this));
- }
- });
- this.$el.parent().show()
- return this._super.apply(this, arguments);
- },
- do_update: function () {
- var self = this;
- var fct = function() {
- var $avatar = self.$el.find('.oe_topbar_avatar');
- $avatar.attr('src', $avatar.data('default-src'));
- if (!self.session.uid)
- return;
- var func = new instance.web.Model("res.users").get_func("read");
- return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
- var topbar_name = res.name;
- if(instance.session.debug)
- topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
- if(res.company_id[0] > 1)
- topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
- self.$el.find('.oe_topbar_name').text(topbar_name);
- if (!instance.session.debug) {
- topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
- }
- var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
- $avatar.attr('src', avatar_src);
-
- openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic
- });
- };
- this.update_promise = this.update_promise.then(fct, fct);
- },
- on_menu_help: function() {
- window.open('http://help.odoo.com', '_blank');
- },
- on_menu_logout: function() {
- this.trigger('user_logout');
- },
- on_menu_settings: function() {
- var self = this;
- if (!this.getParent().has_uncommitted_changes()) {
- self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
- result.res_id = instance.session.uid;
- self.getParent().action_manager.do_action(result);
- });
- }
- },
- on_menu_account: function() {
- var self = this;
- if (!this.getParent().has_uncommitted_changes()) {
- var P = new instance.web.Model('ir.config_parameter');
- P.call('get_param', ['database.uuid']).then(function(dbuuid) {
- var state = {
- 'd': instance.session.db,
- 'u': window.location.protocol + '//' + window.location.host,
- };
- var params = {
- response_type: 'token',
- client_id: dbuuid || '',
- state: JSON.stringify(state),
- scope: 'userinfo',
- };
- instance.web.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params));
- }).fail(function(result, ev){
- ev.preventDefault();
- instance.web.redirect('https://accounts.odoo.com/web');
- });
- }
- },
- on_menu_about: function() {
- var self = this;
- self.rpc("/web/webclient/version_info", {}).done(function(res) {
- var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
- $help.find('a.oe_activate_debug_mode').click(function (e) {
- e.preventDefault();
- window.location = $.param.querystring( window.location.href, 'debug');
- });
- new instance.web.Dialog(this, {
- size: 'medium',
- dialogClass: 'oe_act_window',
- title: _t("About"),
- }, $help).open();
- });
- },
-});
-
-instance.web.FullscreenWidget = instance.web.Widget.extend({
- /**
- * Widgets extending the FullscreenWidget will be displayed fullscreen,
- * and will have a fixed 1:1 zoom level on mobile devices.
- */
- start: function(){
- if(!$('#oe-fullscreenwidget-viewport').length){
- $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
- }
- instance.webclient.set_content_full_screen(true);
- return this._super();
- },
- destroy: function(){
- instance.webclient.set_content_full_screen(false);
- $('#oe-fullscreenwidget-viewport').remove();
- return this._super();
- },
-
-});
-
-instance.web.Client = instance.web.Widget.extend({
- init: function(parent, origin) {
- instance.client = instance.webclient = this;
- this.client_options = {};
- this._super(parent);
- this.origin = origin;
- },
- start: function() {
- var self = this;
- return instance.session.session_bind(this.origin).then(function() {
- self.bind_events();
- return self.show_common();
- });
- },
- bind_events: function() {
- var self = this;
- $('.oe_systray').show();
- this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
- $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
- });
- this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
- ev.preventDefault();
- var $toggle = $(this);
- var doc_width = $(document).width();
- var $menu = $toggle.siblings('.oe_dropdown_menu');
- $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
- var state = $menu.is('.oe_opened');
- setTimeout(function() {
- // Do not alter propagation
- $toggle.add($menu).toggleClass('oe_opened', !state);
- if (!state) {
- // Move $menu if outside window's edge
- var offset = $menu.offset();
- var menu_width = $menu.width();
- var x = doc_width - offset.left - menu_width - 2;
- if (x < 0) {
- $menu.offset({ left: offset.left + x }).width(menu_width);
- }
- }
- }, 0);
- });
- instance.web.bus.on('click', this, function(ev) {
- $('.tooltip').remove();
- if (!$(ev.target).is('input[type=file]')) {
- self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
- }
- });
- },
- on_logo_click: function(ev){
- if (!this.has_uncommitted_changes()) {
- return;
- } else {
- ev.preventDefault();
- }
- },
- show_common: function() {
- var self = this;
- this.crashmanager = new instance.web.CrashManager();
- instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
- self.notification = new instance.web.Notification(this);
- self.notification.appendTo(self.$el.find('.openerp'));
- self.loading = new instance.web.Loading(self);
- self.loading.appendTo(self.$('.openerp_webclient_container'));
- self.action_manager = new instance.web.ActionManager(self);
- self.action_manager.replace(self.$('.oe_application'));
- },
- toggle_bars: function(value) {
- this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
- },
- has_uncommitted_changes: function() {
- return false;
- },
-});
-
-instance.web.WebClient = instance.web.Client.extend({
- init: function(parent, client_options) {
- this._super(parent);
- if (client_options) {
- _.extend(this.client_options, client_options);
- }
- this._current_state = null;
- this.menu_dm = new instance.web.DropMisordered();
- this.action_mutex = new $.Mutex();
- this.set('title_part', {"zopenerp": "Odoo"});
- },
- start: function() {
- var self = this;
- this.on("change:title_part", this, this._title_changed);
- this._title_changed();
-
- return $.when(this._super()).then(function() {
- if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
- self.to_kitten();
- }
- if (self.session.session_is_valid()) {
- self.show_application();
- }
- if (self.client_options.action) {
- self.action_manager.do_action(self.client_options.action);
- delete(self.client_options.action);
- }
- });
- },
- to_kitten: function() {
- this.kitten = true;
- $("body").addClass("kitten-mode-activated");
- $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
- if ($.blockUI) {
- var imgkit = Math.floor(Math.random() * 2 + 1);
- $.blockUI.defaults.message = '<img src="/web/static/src/img/k-waiting' + imgkit + '.gif" class="loading-kitten">';
- }
- },
- /**
- Sets the first part of the title of the window, dedicated to the current action.
- */
- set_title: function(title) {
- this.set_title_part("action", title);
- },
- /**
- Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
- a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
- title of the window separated by '-'.
- */
- set_title_part: function(part, title) {
- var tmp = _.clone(this.get("title_part"));
- tmp[part] = title;
- this.set("title_part", tmp);
- },
- _title_changed: function() {
- var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
- var tmp = "";
- _.each(parts, function(part) {
- var str = this.get("title_part")[part];
- if (str) {
- tmp = tmp ? tmp + " - " + str : str;
- }
- }, this);
- document.title = tmp;
- },
- show_common: function() {
- var self = this;
- this._super();
- window.onerror = function (message, file, line) {
- self.crashmanager.show_error({
- type: _t("Client Error"),
- message: message,
- data: {debug: file + ':' + line}
- });
- };
- },
- show_application: function() {
- var self = this;
- self.toggle_bars(true);
-
- self.update_logo();
- this.$('.oe_logo_edit_admin').click(function(ev) {
- self.logo_edit(ev);
- });
- this.$('.oe_logo img').click(function(ev) {
- self.on_logo_click(ev);
- });
-
- // Menu is rendered server-side thus we don't want the widget to create any dom
- self.menu = new instance.web.Menu(self);
- self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
- self.menu.on('menu_click', this, this.on_menu_action);
-
- // Create the user menu (rendered client-side)
- self.user_menu = new instance.web.UserMenu(self);
- var user_menu_loaded = self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
- self.user_menu.on('user_logout', self, self.on_logout);
- self.user_menu.do_update();
-
- // Create the systray menu (rendered server-side)
- self.systray_menu = new instance.web.SystrayMenu(self);
- self.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
- var systray_menu_loaded = self.systray_menu.start();
-
- // Start the menu once both systray and user menus are rendered
- // to prevent overflows while loading
- $.when(systray_menu_loaded, user_menu_loaded).done(function() {
- self.menu.start();
- });
-
- self.bind_hashchange();
- self.set_title();
- self.check_timezone();
- if (self.client_options.action_post_login) {
- self.action_manager.do_action(self.client_options.action_post_login);
- delete(self.client_options.action_post_login);
- }
- },
- update_logo: function() {
- var company = this.session.company_id;
- var img = this.session.url('/web/binary/company_logo' + '?db=' + this.session.db + (company ? '&company=' + company : ''));
- this.$('.oe_logo img').attr('src', '').attr('src', img);
- this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
- },
- logo_edit: function(ev) {
- var self = this;
- ev.preventDefault();
- self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
- self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
- result.res_id = res['company_id'][0];
- result.target = "new";
- result.views = [[false, 'form']];
- result.flags = {
- action_buttons: true,
- headless: true,
- };
- self.action_manager.do_action(result);
- var form = self.action_manager.dialog_widget.views.form.controller;
- form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
- form.on('record_saved', self, function() {
- self.action_manager.dialog_stop();
- self.update_logo();
- });
- });
- });
- return false;
- },
- check_timezone: function() {
- var self = this;
- return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
- var user_offset = result[0]['tz_offset'];
- var offset = -(new Date().getTimezoneOffset());
- // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
- var browser_offset = (offset < 0) ? "-" : "+";
- browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
- browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
- if (browser_offset !== user_offset) {
- var $icon = $(QWeb.render('WebClient.timezone_systray'));
- $icon.on('click', function() {
- var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
- user_timezone: instance.session.user_context.tz || 'UTC',
- user_offset: user_offset,
- browser_offset: browser_offset,
- }), true);
- notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
- notification.close();
- }).find('a').on('click', function() {
- notification.close();
- self.user_menu.on_menu_settings();
- return false;
- });
- });
- $icon.prependTo(window.$('.oe_systray'));
- }
- });
- },
- destroy_content: function() {
- _.each(_.clone(this.getChildren()), function(el) {
- el.destroy();
- });
- this.$el.children().remove();
- },
- do_reload: function() {
- var self = this;
- return this.session.session_reload().then(function () {
- instance.session.load_modules(true).then(
- self.menu.proxy('do_reload')); });
- },
- do_notify: function() {
- var n = this.notification;
- return n.notify.apply(n, arguments);
- },
- do_warn: function() {
- var n = this.notification;
- return n.warn.apply(n, arguments);
- },
- on_logout: function() {
- var self = this;
- if (!this.has_uncommitted_changes()) {
- self.action_manager.do_action('logout');
- }
- },
- bind_hashchange: function() {
- var self = this;
- $(window).bind('hashchange', this.on_hashchange);
-
- var state = $.bbq.getState(true);
- if (_.isEmpty(state) || state.action == "login") {
- self.menu.is_bound.done(function() {
- new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
- if(data.action_id) {
- self.action_manager.do_action(data.action_id[0]);
- self.menu.open_action(data.action_id[0]);
- } else {
- var first_menu_id = self.menu.$el.find("a:first").data("menu");
- if(first_menu_id) {
- self.menu.menu_click(first_menu_id);
- } }
- });
- });
- } else {
- $(window).trigger('hashchange');
- }
- },
- on_hashchange: function(event) {
- var self = this;
- var stringstate = event.getState(false);
- if (!_.isEqual(this._current_state, stringstate)) {
- var state = event.getState(true);
- if(!state.action && state.menu_id) {
- self.menu.is_bound.done(function() {
- self.menu.menu_click(state.menu_id);
- });
- } else {
- state._push_me = false; // no need to push state back...
- this.action_manager.do_load_state(state, !!this._current_state);
- }
- }
- this._current_state = stringstate;
- },
- do_push_state: function(state) {
- this.set_title(state.title);
- delete state.title;
- var url = '#' + $.param(state);
- this._current_state = $.deparam($.param(state), false); // stringify all values
- $.bbq.pushState(url);
- this.trigger('state_pushed', state);
- },
- on_menu_action: function(options) {
- var self = this;
- return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
- .then(function (result) {
- return self.action_mutex.exec(function() {
- if (options.needaction) {
- result.context = new instance.web.CompoundContext(result.context, {
- search_default_message_unread: true,
- search_disable_custom_filters: true,
- });
- }
- var completed = $.Deferred();
- $.when(self.action_manager.do_action(result, {
- clear_breadcrumbs: true,
- action_menu_id: self.menu.current_menu,
- })).fail(function() {
- self.menu.open_menu(options.previous_menu_id);
- }).always(function() {
- completed.resolve();
- });
- setTimeout(function() {
- completed.resolve();
- }, 2000);
- // We block the menu when clicking on an element until the action has correctly finished
- // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
- return completed;
- });
- });
- },
- set_content_full_screen: function(fullscreen) {
- $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
- this.$('.oe_webclient').toggleClass(
- 'oe_content_full_screen', fullscreen);
- },
- has_uncommitted_changes: function() {
- var $e = $.Event('clear_uncommitted_changes');
- instance.web.bus.trigger('clear_uncommitted_changes', $e);
- if ($e.isDefaultPrevented()) {
- return true;
- } else {
- return this._super.apply(this, arguments);
- }
- },
-});
-
-instance.web.EmbeddedClient = instance.web.Client.extend({
- _template: 'EmbedClient',
- init: function(parent, origin, dbname, login, key, action_id, options) {
- this._super(parent, origin);
- this.bind_credentials(dbname, login, key);
- this.action_id = action_id;
- this.options = options || {};
- },
- start: function() {
- var self = this;
- return $.when(this._super()).then(function() {
- return self.authenticate().then(function() {
- if (!self.action_id) {
- return;
- }
- return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
- var action = result;
- action.flags = _.extend({
- //views_switcher : false,
- search_view : false,
- action_buttons : false,
- sidebar : false
- //pager : false
- }, self.options, action.flags || {});
-
- self.do_action(action);
- });
- });
- });
- },
-
- do_action: function(/*...*/) {
- var am = this.action_manager;
- return am.do_action.apply(am, arguments);
- },
-
- authenticate: function() {
- var s = instance.session;
- if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
- return $.when();
- }
- return instance.session.session_authenticate(this.dbname, this.login, this.key);
- },
-
- bind_credentials: function(dbname, login, key) {
- this.dbname = dbname;
- this.login = login;
- this.key = key;
- },
-
-});
-
-instance.web.embed = function (origin, dbname, login, key, action, options) {
- $('head').append($('<link>', {
- 'rel': 'stylesheet',
- 'type': 'text/css',
- 'href': origin +'/web/css/web.assets_webclient'
- }));
- var currentScript = document.currentScript;
- if (!currentScript) {
- var sc = document.getElementsByTagName('script');
- currentScript = sc[sc.length-1];
- }
- var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
- client.insertAfter(currentScript);
-};
-
-})();
diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js
deleted file mode 100644
index ef7523ee440..00000000000
--- a/addons/web/static/src/js/core.js
+++ /dev/null
@@ -1,787 +0,0 @@
-
-(function() {
-
-if (typeof(console) === "undefined") {
- // Even IE9 only exposes console object if debug window opened
- window.console = {};
- ('log error debug info warn assert clear dir dirxml trace group'
- + ' groupCollapsed groupEnd time timeEnd profile profileEnd count'
- + ' exception').split(/\s+/).forEach(function(property) {
- console[property] = _.identity;
- });
-}
-
-var instance = openerp;
-openerp.web.core = {};
-
-var ControllerMixin = {
- /**
- * Informs the action manager to do an action. This supposes that
- * the action manager can be found amongst the ancestors of the current widget.
- * If that's not the case this method will simply return `false`.
- */
- do_action: function() {
- var parent = this.getParent();
- if (parent) {
- return parent.do_action.apply(parent, arguments);
- }
- return false;
- },
- do_notify: function() {
- if (this.getParent()) {
- return this.getParent().do_notify.apply(this,arguments);
- }
- return false;
- },
- do_warn: function() {
- if (this.getParent()) {
- return this.getParent().do_warn.apply(this,arguments);
- }
- return false;
- },
- rpc: function(url, data, options) {
- return this.alive(openerp.session.rpc(url, data, options));
- }
-};
-
-/**
- A class containing common utility methods useful when working with OpenERP as well as the PropertiesMixin.
-*/
-openerp.web.Controller = openerp.web.Class.extend(openerp.web.PropertiesMixin, ControllerMixin, {
- /**
- * Constructs the object and sets its parent if a parent is given.
- *
- * @param {openerp.web.Controller} parent Binds the current instance to the given Controller instance.
- * When that controller is destroyed by calling destroy(), the current instance will be
- * destroyed too. Can be null.
- */
- init: function(parent) {
- openerp.web.PropertiesMixin.init.call(this);
- this.setParent(parent);
- this.session = openerp.session;
- },
-});
-
-openerp.web.Widget.include(_.extend({}, ControllerMixin, {
- init: function() {
- this._super.apply(this, arguments);
- this.session = openerp.session;
- },
-}));
-
-instance.web.Registry = instance.web.Class.extend({
- /**
- * Stores a mapping of arbitrary key (strings) to object paths (as strings
- * as well).
- *
- * Resolves those paths at query time in order to always fetch the correct
- * object, even if those objects have been overloaded/replaced after the
- * registry was created.
- *
- * An object path is simply a dotted name from the instance root to the
- * object pointed to (e.g. ``"instance.web.Session"`` for an OpenERP
- * session object).
- *
- * @constructs instance.web.Registry
- * @param {Object} mapping a mapping of keys to object-paths
- */
- init: function (mapping) {
- this.parent = null;
- this.map = mapping || {};
- },
- /**
- * Retrieves the object matching the provided key string.
- *
- * @param {String} key the key to fetch the object for
- * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception
- * @returns {Class} the stored class, to initialize or null if not found
- */
- get_object: function (key, silent_error) {
- var path_string = this.map[key];
- if (path_string === undefined) {
- if (this.parent) {
- return this.parent.get_object(key, silent_error);
- }
- if (silent_error) { return void 'nooo'; }
- return null;
- }
-
- var object_match = instance;
- var path = path_string.split('.');
- // ignore first section
- for(var i=1; i<path.length; ++i) {
- object_match = object_match[path[i]];
-
- if (object_match === undefined) {
- if (silent_error) { return void 'noooooo'; }
- return null;
- }
- }
- return object_match;
- },
- /**
- * Checks if the registry contains an object mapping for this key.
- *
- * @param {String} key key to look for
- */
- contains: function (key) {
- if (key === undefined) { return false; }
- if (key in this.map) {
- return true;
- }
- if (this.parent) {
- return this.parent.contains(key);
- }
- return false;
- },
- /**
- * Tries a number of keys, and returns the first object matching one of
- * the keys.
- *
- * @param {Array} keys a sequence of keys to fetch the object for
- * @returns {Class} the first class found matching an object
- */
- get_any: function (keys) {
- for (var i=0; i<keys.length; ++i) {
- var key = keys[i];
- if (!this.contains(key)) {
- continue;
- }
-
- return this.get_object(key);
- }
- return null;
- },
- /**
- * Adds a new key and value to the registry.
- *
- * This method can be chained.
- *
- * @param {String} key
- * @param {String} object_path fully qualified dotted object path
- * @returns {instance.web.Registry} itself
- */
- add: function (key, object_path) {
- this.map[key] = object_path;
- return this;
- },
- /**
- * Creates and returns a copy of the current mapping, with the provided
- * mapping argument added in (replacing existing keys if needed)
- *
- * Parent and child remain linked, a new key in the parent (which is not
- * overwritten by the child) will appear in the child.
- *
- * @param {Object} [mapping={}] a mapping of keys to object-paths
- */
- extend: function (mapping) {
- var child = new instance.web.Registry(mapping);
- child.parent = this;
- return child;
- },
- /**
- * @deprecated use Registry#extend
- */
- clone: function (mapping) {
- console.warn('Registry#clone is deprecated, use Registry#extend');
- return this.extend(mapping);
- }
-});
-
-instance.web.py_eval = function(expr, context) {
- return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null}));
-};
-
-/*
- Some retro-compatibility.
-*/
-instance.web.JsonRPC = instance.web.Session;
-
-/** Session openerp specific RPC class */
-instance.web.Session.include( /** @lends instance.web.Session# */{
- init: function() {
- this._super.apply(this, arguments);
- this.debug = ($.deparam($.param.querystring()).debug !== undefined);
- // TODO: session store in cookie should be optional
- this.name = instance._session_id;
- this.qweb_mutex = new $.Mutex();
- },
- /**
- * Setup a sessionm
- */
- session_bind: function(origin) {
- var self = this;
- this.setup(origin);
- instance.web.qweb.default_dict['_s'] = this.origin;
- this.uid = null;
- this.username = null;
- this.user_context= {};
- this.db = null;
- this.module_list = instance._modules.slice();
- this.module_loaded = {};
- _(this.module_list).each(function (mod) {
- self.module_loaded[mod] = true;
- });
- this.active_id = null;
- return this.session_init();
- },
- /**
- * Init a session, reloads from cookie, if it exists
- */
- session_init: function () {
- var self = this;
- return this.session_reload().then(function(result) {
- var modules = instance._modules.join(',');
- var deferred = self.load_qweb(modules);
- if(self.session_is_valid()) {
- return deferred.then(function() { return self.load_modules(); });
- }
- return $.when(
- deferred,
- self.rpc('/web/webclient/bootstrap_translations', {mods: instance._modules}).then(function(trans) {
- instance.web._t.database.set_bundle(trans);
- })
- );
- });
- },
- session_is_valid: function() {
- var db = $.deparam.querystring().db;
- if (db && this.db !== db) {
- return false;
- }
- return !!this.uid;
- },
- /**
- * The session is validated by restoration of a previous session
- */
- session_authenticate: function() {
- var self = this;
- return $.when(this._super.apply(this, arguments)).then(function() {
- return self.load_modules();
- });
- },
- session_logout: function() {
- $.bbq.removeState();
- return this.rpc("/web/session/destroy", {});
- },
- get_cookie: function (name) {
- if (!this.name) { return null; }
- var nameEQ = this.name + '|' + name + '=';
- var cookies = document.cookie.split(';');
- for(var i=0; i<cookies.length; ++i) {
- var cookie = cookies[i].replace(/^\s*/, '');
- if(cookie.indexOf(nameEQ) === 0) {
- try {
- return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
- } catch(err) {
- // wrong cookie, delete it
- this.set_cookie(name, '', -1);
- }
- }
- }
- return null;
- },
- /**
- * Create a new cookie with the provided name and value
- *
- * @private
- * @param name the cookie's name
- * @param value the cookie's value
- * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
- */
- set_cookie: function (name, value, ttl) {
- if (!this.name) { return; }
- ttl = ttl || 24*60*60*365;
- document.cookie = [
- this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
- 'path=/',
- 'max-age=' + ttl,
- 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
- ].join(';');
- },
- /**
- * Load additional web addons of that instance and init them
- *
- */
- load_modules: function() {
- var self = this;
- return this.rpc('/web/session/modules', {}).then(function(result) {
- var all_modules = _.uniq(self.module_list.concat(result));
- var to_load = _.difference(result, self.module_list).join(',');
- self.module_list = all_modules;
-
- var loaded = self.load_translations();
- var locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US';
- var file_list = [ locale ];
- if(to_load.length) {
- loaded = $.when(
- loaded,
- self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)),
- self.load_qweb(to_load),
- self.rpc('/web/webclient/jslist', {mods: to_load}).done(function(files) {
- file_list = file_list.concat(files);
- })
- );
- }
- return loaded.then(function () {
- return self.load_js(file_list);
- }).done(function() {
- self.on_modules_loaded();
- self.trigger('module_loaded');
- });
- });
- },
- load_translations: function() {
- return instance.web._t.database.load_translations(this, this.module_list, this.user_context.lang);
- },
- load_css: function (files) {
- var self = this;
- _.each(files, function (file) {
- openerp.loadCSS(self.url(file, null));
- });
- },
- load_js: function(files) {
- var self = this;
- var d = $.Deferred();
- if (files.length !== 0) {
- var file = files.shift();
- var url = self.url(file, null);
- openerp.loadJS(url).done(d.resolve);
- } else {
- d.resolve();
- }
- return d;
- },
- load_qweb: function(mods) {
- var self = this;
- self.qweb_mutex.exec(function() {
- return self.rpc('/web/proxy/load', {path: '/web/webclient/qweb?mods=' + mods}).then(function(xml) {
- if (!xml) { return; }
- instance.web.qweb.add_template(_.str.trim(xml));
- });
- });
- return self.qweb_mutex.def;
- },
- on_modules_loaded: function() {
- for(var j=0; j<this.module_list.length; j++) {
- var mod = this.module_list[j];
- if(this.module_loaded[mod])
- continue;
- instance[mod] = {};
- // init module mod
- var fct = instance._openerp[mod];
- if(typeof(fct) === "function") {
- instance._openerp[mod] = {};
- for (var k in fct) {
- instance._openerp[mod][k] = fct[k];
- }
- fct(instance, instance._openerp[mod]);
- }
- this.module_loaded[mod] = true;
- }
- },
- /**
- * Cooperative file download implementation, for ajaxy APIs.
- *
- * Requires that the server side implements an httprequest correctly
- * setting the `fileToken` cookie to the value provided as the `token`
- * parameter. The cookie *must* be set on the `/` path and *must not* be
- * `httpOnly`.
- *
- * It would probably also be a good idea for the response to use a
- * `Content-Disposition: attachment` header, especially if the MIME is a
- * "known" type (e.g. text/plain, or for some browsers application/json
- *
- * @param {Object} options
- * @param {String} [options.url] used to dynamically create a form
- * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data
- * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
- * @param {Function} [options.success] callback in case of download success
- * @param {Function} [options.error] callback in case of request error, provided with the error body
- * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
- */
- get_file: function (options) {
- // need to detect when the file is done downloading (not used
- // yet, but we'll need it to fix the UI e.g. with a throbber
- // while dump is being generated), iframe load event only fires
- // when the iframe content loads, so we need to go smarter:
- // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
- var timer, token = new Date().getTime(),
- cookie_name = 'fileToken', cookie_length = cookie_name.length,
- CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
- remove_form = false;
-
-
- // iOS devices doesn't allow iframe use the way we do it,
- // opening a new window seems the best way to workaround
- if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
- var params = _.extend({}, options.data || {}, {token: token});
- var url = this.url(options.url, params);
- instance.web.unblockUI();
- return window.open(url);
- }
-
- var $form, $form_data = $('<div>');
-
- var complete = function () {
- if (options.complete) { options.complete(); }
- clearTimeout(timer);
- $form_data.remove();
- $target.remove();
- if (remove_form && $form) { $form.remove(); }
- };
- var $target = $('<iframe style="display: none;">')
- .attr({id: id, name: id})
- .appendTo(document.body)
- .load(function () {
- try {
- if (options.error) {
- var body = this.contentDocument.body;
- var nodes = body.children.length === 0 ? body.childNodes : body.children;
- var node = nodes[1] || nodes[0];
- options.error(JSON.parse(node.textContent));
- }
- } finally {
- complete();
- }
- });
-
- if (options.form) {
- $form = $(options.form);
- } else {
- remove_form = true;
- $form = $('<form>', {
- action: options.url,
- method: 'POST'
- }).appendTo(document.body);
- }
-
- var hparams = _.extend({}, options.data || {}, {token: token});
- if (this.override_session)
- hparams.session_id = this.session_id;
- _.each(hparams, function (value, key) {
- var $input = $form.find('[name=' + key +']');
- if (!$input.length) {
- $input = $('<input type="hidden" name="' + key + '">')
- .appendTo($form_data);
- }
- $input.val(value);
- });
-
- $form
- .append($form_data)
- .attr('target', id)
- .get(0).submit();
-
- var waitLoop = function () {
- var cookies = document.cookie.split(';');
- // setup next check
- timer = setTimeout(waitLoop, CHECK_INTERVAL);
- for (var i=0; i<cookies.length; ++i) {
- var cookie = cookies[i].replace(/^\s*/, '');
- if (!cookie.indexOf(cookie_name === 0)) { continue; }
- var cookie_val = cookie.substring(cookie_length + 1);
- if (parseInt(cookie_val, 10) !== token) { continue; }
-
- // clear cookie
- document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
- cookie_name, new Date().toGMTString());
- if (options.success) { options.success(); }
- complete();
- return;
- }
- };
- timer = setTimeout(waitLoop, CHECK_INTERVAL);
- },
- synchronized_mode: function(to_execute) {
- var synch = this.synch;
- this.synch = true;
- try {
- return to_execute();
- } finally {
- this.synch = synch;
- }
- }
-});
-
-
-/**
- * Event Bus used to bind events scoped in the current instance
- */
-instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, {
- init: function() {
- instance.web.EventDispatcherMixin.init.call(this, parent);
- var self = this;
- // TODO fme: allow user to bind keys for some global actions.
- // check gtk bindings
- // http://unixpapa.com/js/key.html
- _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) {
- $('html').on(evtype, function(ev) {
- self.trigger(evtype, ev);
- });
- });
- _.each('resize,scroll'.split(','), function(evtype) {
- $(window).on(evtype, function(ev) {
- self.trigger(evtype, ev);
- });
- });
- }
-});
-instance.web.bus = new instance.web.Bus();
-
-instance.web.TranslationDataBase.include({
- set_bundle: function(translation_bundle) {
- this._super(translation_bundle);
- if (translation_bundle.lang_parameters) {
- this.parameters.grouping = py.eval(this.parameters.grouping);
- }
- },
-});
-
-/** Custom jQuery plugins */
-$.fn.getAttributes = function() {
- var o = {};
- if (this.length) {
- for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) {
- attr = attrs.item(i);
- o[attr.nodeName] = attr.value;
- }
- }
- return o;
-};
-$.fn.openerpClass = function(additionalClass) {
- // This plugin should be applied on top level elements
- additionalClass = additionalClass || '';
- if (!!$.browser.msie) {
- additionalClass += ' openerp_ie';
- }
- return this.each(function() {
- $(this).addClass('openerp ' + additionalClass);
- });
-};
-$.fn.openerpBounce = function() {
- return this.each(function() {
- $(this).css('box-sizing', 'content-box').effect('bounce', {distance: 18, times: 5}, 250);
- });
-};
-
-/** Jquery extentions */
-$.Mutex = openerp.Mutex;
-
-$.async_when = function() {
- var async = false;
- var def = $.Deferred();
- $.when.apply($, arguments).done(function() {
- var args = arguments;
- var action = function() {
- def.resolve.apply(def, args);
- };
- if (async)
- action();
- else
- setTimeout(action, 0);
- }).fail(function() {
- var args = arguments;
- var action = function() {
- def.reject.apply(def, args);
- };
- if (async)
- action();
- else
- setTimeout(action, 0);
- });
- async = true;
- return def;
-};
-
-// special tweak for the web client
-var old_async_when = $.async_when;
-$.async_when = function() {
- if (instance.session.synch)
- return $.when.apply(this, arguments);
- else
- return old_async_when.apply(this, arguments);
-};
-
-/** Setup default session */
-instance.session = new instance.web.Session();
-
-/**
- * Lazy translation function, only performs the translation when actually
- * printed (e.g. inserted into a template)
- *
- * Useful when defining translatable strings in code evaluated before the
- * translation database is loaded, as class attributes or at the top-level of
- * an OpenERP Web module
- *
- * @param {String} s string to translate
- * @returns {Object} lazy translation object
- */
-instance.web._lt = function (s) {
- return {toString: function () { return instance.web._t(s); }};
-};
-instance.web.qweb.debug = instance.session.debug;
-_.extend(instance.web.qweb.default_dict, {
- '__debug__': instance.session.debug,
- 'moment': function(date) { return new moment(date); },
-});
-instance.web.qweb.preprocess_node = function() {
- // Note that 'this' is the Qweb Node
- switch (this.node.nodeType) {
- case Node.TEXT_NODE:
- case Node.CDATA_SECTION_NODE:
- // Text and CDATAs
- var translation = this.node.parentNode.attributes['t-translation'];
- if (translation && translation.value === 'off') {
- return;
- }
- var match = /^(\s*)([\s\S]+?)(\s*)$/.exec(this.node.data);
- if (match) {
- this.node.data = match[1] + instance.web._t(match[2]) + match[3];
- }
- break;
- case Node.ELEMENT_NODE:
- // Element
- var attr, attrs = ['label', 'title', 'alt', 'placeholder'];
- while ((attr = attrs.pop())) {
- if (this.attributes[attr]) {
- this.attributes[attr] = instance.web._t(this.attributes[attr]);
- }
- }
- }
-};
-
-/** Setup jQuery timeago */
-var _t = instance.web._t;
-/*
- * Strings in timeago are "composed" with prefixes, words and suffixes. This
- * makes their detection by our translating system impossible. Use all literal
- * strings we're using with a translation mark here so the extractor can do its
- * job.
- */
-{
- _t('less than a minute ago');
- _t('about a minute ago');
- _t('%d minutes ago');
- _t('about an hour ago');
- _t('%d hours ago');
- _t('a day ago');
- _t('%d days ago');
- _t('about a month ago');
- _t('%d months ago');
- _t('about a year ago');
- _t('%d years ago');
-}
-
-instance.session.on('module_loaded', this, function () {
- // provide timeago.js with our own translator method
- $.timeago.settings.translator = instance.web._t;
-});
-
-/** Setup blockui */
-if ($.blockUI) {
- $.blockUI.defaults.baseZ = 1100;
- $.blockUI.defaults.message = '<div class="openerp oe_blockui_spin_container" style="background-color: transparent;">';
- $.blockUI.defaults.css.border = '0';
- $.blockUI.defaults.css["background-color"] = '';
-}
-
-var messages_by_seconds = function() {
- return [
- [0, _t("Loading...")],
- [20, _t("Still loading...")],
- [60, _t("Still loading...<br />Please be patient.")],
- [120, _t("Don't leave yet,<br />it's still loading...")],
- [300, _t("You may not believe it,<br />but the application is actually loading...")],
- [420, _t("Take a minute to get a coffee,<br />because it's loading...")],
- [3600, _t("Maybe you should consider reloading the application by pressing F5...")]
- ];
-};
-
-instance.web.Throbber = instance.web.Widget.extend({
- template: "Throbber",
- start: function() {
- var opts = {
- lines: 13, // The number of lines to draw
- length: 7, // The length of each line
- width: 4, // The line thickness
- radius: 10, // The radius of the inner circle
- rotate: 0, // The rotation offset
- color: '#FFF', // #rgb or #rrggbb
- speed: 1, // Rounds per second
- trail: 60, // Afterglow percentage
- shadow: false, // Whether to render a shadow
- hwaccel: false, // Whether to use hardware acceleration
- className: 'spinner', // The CSS class to assign to the spinner
- zIndex: 2e9, // The z-index (defaults to 2000000000)
- top: 'auto', // Top position relative to parent in px
- left: 'auto' // Left position relative to parent in px
- };
- this.spin = new Spinner(opts).spin(this.$el[0]);
- this.start_time = new Date().getTime();
- this.act_message();
- },
- act_message: function() {
- var self = this;
- setTimeout(function() {
- if (self.isDestroyed())
- return;
- var seconds = (new Date().getTime() - self.start_time) / 1000;
- var mes;
- _.each(messages_by_seconds(), function(el) {
- if (seconds >= el[0])
- mes = el[1];
- });
- self.$(".oe_throbber_message").html(mes);
- self.act_message();
- }, 1000);
- },
- destroy: function() {
- if (this.spin)
- this.spin.stop();
- this._super();
- },
-});
-instance.web.Throbber.throbbers = [];
-
-instance.web.blockUI = function() {
- var tmp = $.blockUI.apply($, arguments);
- var throbber = new instance.web.Throbber();
- instance.web.Throbber.throbbers.push(throbber);
- throbber.appendTo($(".oe_blockui_spin_container"));
- return tmp;
-};
-instance.web.unblockUI = function() {
- _.each(instance.web.Throbber.throbbers, function(el) {
- el.destroy();
- });
- return $.unblockUI.apply($, arguments);
-};
-
-
-/* Bootstrap defaults overwrite */
-$.fn.tooltip.Constructor.DEFAULTS.placement = 'auto top';
-$.fn.tooltip.Constructor.DEFAULTS.html = true;
-$.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover focus click';
-$.fn.tooltip.Constructor.DEFAULTS.container = 'body';
-//overwrite bootstrap tooltip method to prevent showing 2 tooltip at the same time
-var bootstrap_show_function = $.fn.tooltip.Constructor.prototype.show;
-$.fn.modal.Constructor.prototype.enforceFocus = function () { };
-$.fn.tooltip.Constructor.prototype.show = function () {
- $('.tooltip').remove();
- //the following fix the bug when using placement
- //auto and the parent element does not exist anymore resulting in
- //an error. This should be remove once we updade bootstrap to a version that fix the bug
- //edit: bug has been fixed here : https://github.com/twbs/bootstrap/pull/13752
- var e = $.Event('show.bs.' + this.type);
- var inDom = $.contains(document.documentElement, this.$element[0]);
- if (e.isDefaultPrevented() || !inDom) return;
- return bootstrap_show_function.call(this);
-};
-
-/**
- * Registry for all the client actions key: tag value: widget
- */
-instance.web.client_actions = new instance.web.Registry();
-
-})();
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
deleted file mode 100644
index 9fd9ff86b02..00000000000
--- a/addons/web/static/src/js/formats.js
+++ /dev/null
@@ -1,398 +0,0 @@
-
-(function() {
-
-var instance = openerp;
-openerp.web.formats = {};
-
-var _t = instance.web._t;
-
-/**
- * Intersperses ``separator`` in ``str`` at the positions indicated by
- * ``indices``.
- *
- * ``indices`` is an array of relative offsets (from the previous insertion
- * position, starting from the end of the string) at which to insert
- * ``separator``.
- *
- * There are two special values:
- *
- * ``-1``
- * indicates the insertion should end now
- * ``0``
- * indicates that the previous section pattern should be repeated (until all
- * of ``str`` is consumed)
- *
- * @param {String} str
- * @param {Array<Number>} indices
- * @param {String} separator
- * @returns {String}
- */
-instance.web.intersperse = function (str, indices, separator) {
- separator = separator || '';
- var result = [], last = str.length;
-
- for(var i=0; i<indices.length; ++i) {
- var section = indices[i];
- if (section === -1 || last <= 0) {
- // Done with string, or -1 (stops formatting string)
- break;
- } else if(section === 0 && i === 0) {
- // repeats previous section, which there is none => stop
- break;
- } else if (section === 0) {
- // repeat previous section forever
- //noinspection AssignmentToForLoopParameterJS
- section = indices[--i];
- }
- result.push(str.substring(last-section, last));
- last -= section;
- }
-
- var s = str.substring(0, last);
- if (s) { result.push(s); }
- return result.reverse().join(separator);
-};
-/**
- * Insert "thousands" separators in the provided number (which is actually
- * a string)
- *
- * @param {String} num
- * @returns {String}
- */
-instance.web.insert_thousand_seps = function (num) {
- var negative = num[0] === '-';
- num = (negative ? num.slice(1) : num);
- return (negative ? '-' : '') + instance.web.intersperse(
- num, _t.database.parameters.grouping, _t.database.parameters.thousands_sep);
-};
-
-/**
- * Check with a scary heuristic if the value is a bin_size or not.
- * If not, compute an approximate size out of the base64 encoded string.
- *
- * @param {String} value original format
- */
-instance.web.binary_to_binsize = function (value) {
- if (!value) {
- return instance.web.human_size(0);
- }
- if (value.substr(0, 10).indexOf(' ') == -1) {
- // Computing approximate size out of base64 encoded string
- // http://en.wikipedia.org/wiki/Base64#MIME
- return instance.web.human_size(value.length / 1.37);
- } else {
- // already bin_size
- return value;
- }
-};
-
-/**
- * Returns a human readable size
- *
- * @param {Number} numner of bytes
- */
-instance.web.human_size = function(size) {
- var units = _t("Bytes,Kb,Mb,Gb,Tb,Pb,Eb,Zb,Yb").split(',');
- var i = 0;
- while (size >= 1024) {
- size /= 1024;
- ++i;
- }
- return size.toFixed(2) + ' ' + units[i];
-};
-
-/**
- * Formats a single atomic value based on a field descriptor
- *
- * @param {Object} value read from OpenERP
- * @param {Object} descriptor union of orm field and view field
- * @param {Object} [descriptor.widget] widget to use to display the value
- * @param {Object} descriptor.type fallback if no widget is provided, or if the provided widget is unknown
- * @param {Object} [descriptor.digits] used for the formatting of floats
- * @param {String} [value_if_empty=''] returned if the ``value`` argument is considered empty
- */
-instance.web.format_value = function (value, descriptor, value_if_empty) {
- var l10n = _t.database.parameters;
- var date_format = instance.web.normalize_format(l10n.date_format);
- var time_format = instance.web.normalize_format(l10n.time_format);
- // If NaN value, display as with a `false` (empty cell)
- if (typeof value === 'number' && isNaN(value)) {
- value = false;
- }
- //noinspection FallthroughInSwitchStatementJS
- switch (value) {
- case '':
- if (descriptor.type === 'char' || descriptor.type === 'text') {
- return '';
- }
- console.warn('Field', descriptor, 'had an empty string as value, treating as false...');
- return value_if_empty === undefined ? '' : value_if_empty;
- case false:
- case undefined:
- case Infinity:
- case -Infinity:
- return value_if_empty === undefined ? '' : value_if_empty;
- }
- switch (descriptor.widget || descriptor.type || (descriptor.field && descriptor.field.type)) {
- case 'id':
- return value.toString();
- case 'integer':
- return instance.web.insert_thousand_seps(
- _.str.sprintf('%d', value));
- case 'float':
- var digits = descriptor.digits ? descriptor.digits : [69,2];
- digits = typeof digits === "string" ? py.eval(digits) : digits;
- var precision = digits[1];
- var formatted = _.str.sprintf('%.' + precision + 'f', value).split('.');
- formatted[0] = instance.web.insert_thousand_seps(formatted[0]);
- return formatted.join(l10n.decimal_point);
- case 'float_time':
- var pattern = '%02d:%02d';
- if (value < 0) {
- value = Math.abs(value);
- pattern = '-' + pattern;
- }
- var hour = Math.floor(value);
- var min = Math.round((value % 1) * 60);
- if (min == 60){
- min = 0;
- hour = hour + 1;
- }
- return _.str.sprintf(pattern, hour, min);
- case 'many2one':
- // name_get value format
- return value[1] ? value[1].split("\n")[0] : value[1];
- case 'one2many':
- case 'many2many':
- if (typeof value === 'string') {
- return value;
- }
- return _.str.sprintf(_t("(%d records)"), value.length);
- case 'datetime':
- if (typeof(value) == "string")
- value = moment(instance.web.auto_str_to_date(value));
- else {
- value = moment(value);
- }
- return value.format(date_format + ' ' + time_format);
- case 'date':
- if (typeof(value) == "string")
- value = moment(instance.web.auto_str_to_date(value));
- else {
- value = moment(value);
- }
- return value.format(date_format);
- case 'time':
- if (typeof(value) == "string")
- value = moment(instance.web.auto_str_to_date(value));
- else {
- value = moment(value);
- }
- return value.format(time_format);
- case 'selection': case 'statusbar':
- // Each choice is [value, label]
- if(_.isArray(value)) {
- return value[1];
- }
- var result = _(descriptor.selection).detect(function (choice) {
- return choice[0] === value;
- });
- if (result) { return result[1]; }
- return;
- default:
- return value;
- }
-};
-
-instance.web.parse_value = function (value, descriptor, value_if_empty) {
- var date_pattern = instance.web.normalize_format(_t.database.parameters.date_format),
- time_pattern = instance.web.normalize_format(_t.database.parameters.time_format);
- var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'),
- time_pattern_wo_zero = time_pattern.replace('HH','H').replace('mm','m').replace('ss','s');
- switch (value) {
- case false:
- case "":
- return value_if_empty === undefined ? false : value_if_empty;
- }
- var tmp;
- switch (descriptor.widget || descriptor.type || (descriptor.field && descriptor.field.type)) {
- case 'integer':
- do {
- tmp = value;
- value = value.replace(instance.web._t.database.parameters.thousands_sep, "");
- } while(tmp !== value);
- tmp = Number(value);
- // do not accept not numbers or float values
- if (isNaN(tmp) || tmp % 1)
- throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
- return tmp;
- case 'float':
- tmp = Number(value);
- if (!isNaN(tmp))
- return tmp;
-
- var tmp2 = value;
- do {
- tmp = tmp2;
- tmp2 = tmp.replace(instance.web._t.database.parameters.thousands_sep, "");
- } while(tmp !== tmp2);
- var reformatted_value = tmp.replace(instance.web._t.database.parameters.decimal_point, ".");
- var parsed = Number(reformatted_value);
- if (isNaN(parsed))
- throw new Error(_.str.sprintf(_t("'%s' is not a correct float"), value));
- return parsed;
- case 'float_time':
- var factor = 1;
- if (value[0] === '-') {
- value = value.slice(1);
- factor = -1;
- }
- var float_time_pair = value.split(":");
- if (float_time_pair.length != 2)
- return factor * instance.web.parse_value(value, {type: "float"});
- var hours = instance.web.parse_value(float_time_pair[0], {type: "integer"});
- var minutes = instance.web.parse_value(float_time_pair[1], {type: "integer"});
- return factor * (hours + (minutes / 60));
- case 'progressbar':
- return instance.web.parse_value(value, {type: "float"});
- case 'datetime':
- var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero], true)
- if (datetime.isValid())
- return instance.web.datetime_to_str(datetime.toDate());
- throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
- case 'date':
- var date = moment(value, [date_pattern, date_pattern_wo_zero], true)
- if (date.isValid())
- return instance.web.date_to_str(date.toDate());
- throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value));
- case 'time':
- var time = moment(value, [time_pattern, time_pattern_wo_zero], true);
- if (time.isValid())
- return instance.web.time_to_str(time.toDate());
- throw new Error(_.str.sprintf(_t("'%s' is not a correct time"), value));
- }
- return value;
-};
-
-instance.web.auto_str_to_date = function(value, type) {
- try {
- return instance.web.str_to_datetime(value);
- } catch(e) {}
- try {
- return instance.web.str_to_date(value);
- } catch(e) {}
- try {
- return instance.web.str_to_time(value);
- } catch(e) {}
- throw new Error(_.str.sprintf(_t("'%s' is not a correct date, datetime nor time"), value));
-};
-
-instance.web.auto_date_to_str = function(value, type) {
- switch(type) {
- case 'datetime':
- return instance.web.datetime_to_str(value);
- case 'date':
- return instance.web.date_to_str(value);
- case 'time':
- return instance.web.time_to_str(value);
- default:
- throw new Error(_.str.sprintf(_t("'%s' is not convertible to date, datetime nor time"), type));
- }
-};
-
-/**
- * performs a half up rounding with arbitrary precision, correcting for float loss of precision
- * See the corresponding float_round() in server/tools/float_utils.py for more info
- * @param {Number} the value to be rounded
- * @param {Number} a precision parameter. eg: 0.01 rounds to two digits.
- */
-instance.web.round_precision = function(value, precision){
- if (!value) {
- return 0;
- } else if (!precision || precision < 0) {
- precision = 1;
- }
- var normalized_value = value / precision;
- var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
- var epsilon = Math.pow(2, epsilon_magnitude - 53);
- normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
- var rounded_value = Math.round(normalized_value);
- return rounded_value * precision;
-};
-
-/**
- * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision
- * See the corresponding float_round() in server/tools/float_utils.py for more info
- * @param {Number} the value to be rounded
- * @param {Number} the number of decimals. eg: round_decimals(3.141592,2) -> 3.14
- */
-instance.web.round_decimals = function(value, decimals){
- return instance.web.round_precision(value, Math.pow(10,-decimals));
-};
-
-/**
- * Convert Python strftime to escaped moment.js format.
- *
- * @param {String} value original format
- */
-instance.web.normalize_format = function (value) {
- if (_normalize_format_cache[value] === undefined) {
- var isletter = /[a-zA-Z]/,
- output = [],
- inToken = false,
- table = instance.web.normalize_format_table;
-
- for (var index=0; index < value.length; ++index) {
- var character = value[index];
- if (character === '%' && !inToken) {
- inToken = true;
- continue;
- }
- if (isletter.test(character)) {
- if (inToken && table[character] !== undefined) {
- character = table[character];
- } else {
- character = '[' + character + ']'; // moment.js escape
- }
- }
- output.push(character);
- inToken = false;
- }
- _normalize_format_cache[value] = output.join('');
- }
- return _normalize_format_cache[value];
-};
-instance.web.normalize_format_table = {
- // Python strftime to moment.js conversion table
- // See openerp/addons/base/res/res_lang_view.xml
- // for details about supported directives
- 'a': 'ddd',
- 'A': 'dddd',
- 'b': 'MMM',
- 'B': 'MMMM',
- 'd': 'DD',
- 'H': 'HH',
- 'I': 'hh',
- 'j': 'DDDD',
- 'm': 'MM',
- 'M': 'mm',
- 'p': 'A',
- 'S': 'ss',
- 'U': 'ww',
- 'W': 'WW',
- 'w': 'd',
- 'y': 'YY',
- 'Y': 'YYYY',
- // unsupported directives
- 'c': 'ddd MMM D HH:mm:ss YYYY',
- 'x': 'MM/DD/YY',
- 'X': 'HH:mm:ss'
-};
-var _normalize_format_cache = {};
-
-instance.web.float_is_zero = function(value, decimals){
- epsilon = Math.pow(10, -decimals);
- return Math.abs(instance.web.round_precision(value, epsilon)) < epsilon;
-};
-
-})();
diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js
new file mode 100644
index 00000000000..00da4610f9c
--- /dev/null
+++ b/addons/web/static/src/js/framework/ajax.js
@@ -0,0 +1,286 @@
+odoo.define('web.ajax', ['web.time'], function (require) {
+"use strict";
+
+var time = require('web.time');
+
+function genericJsonRpc (fct_name, params, fct) {
+ var data = {
+ jsonrpc: "2.0",
+ method: fct_name,
+ params: params,
+ id: Math.floor(Math.random() * 1000 * 1000 * 1000)
+ };
+ var xhr = fct(data);
+ var result = xhr.pipe(function(result) {
+ if (result.error !== undefined) {
+ console.error("Server application error", result.error);
+ return $.Deferred().reject("server", result.error);
+ } else {
+ return result.result;
+ }
+ }, function() {
+ //console.error("JsonRPC communication error", _.toArray(arguments));
+ var def = $.Deferred();
+ return def.reject.apply(def, ["communication"].concat(_.toArray(arguments)));
+ });
+ // FIXME: jsonp?
+ result.abort = function () { if (xhr.abort) xhr.abort(); };
+ return result;
+}
+
+function jsonRpc(url, fct_name, params, settings) {
+ return genericJsonRpc(fct_name, params, function(data) {
+ return $.ajax(url, _.extend({}, settings, {
+ url: url,
+ dataType: 'json',
+ type: 'POST',
+ data: JSON.stringify(data, time.date_to_utc),
+ contentType: 'application/json'
+ }));
+ });
+};
+
+function jsonpRpc(url, fct_name, params, settings) {
+ settings = settings || {};
+ return genericJsonRpc(fct_name, params, function(data) {
+ var payload_str = JSON.stringify(data, time.date_to_utc);
+ var payload_url = $.param({r:payload_str});
+ var force2step = settings.force2step || false;
+ delete settings.force2step;
+ var session_id = settings.session_id || null;
+ delete settings.session_id;
+ if (payload_url.length < 2000 && ! force2step) {
+ return $.ajax(url, _.extend({}, settings, {
+ url: url,
+ dataType: 'jsonp',
+ jsonp: 'jsonp',
+ type: 'GET',
+ cache: false,
+ data: {r: payload_str, session_id: session_id}
+ }));
+ } else {
+ var args = {session_id: session_id, id: data.id};
+ var ifid = _.uniqueId('oe_rpc_iframe');
+ var html = "<iframe src='javascript:false;' name='" + ifid + "' id='" + ifid + "' style='display:none'></iframe>";
+ var $iframe = $(html);
+ var nurl = 'jsonp=1&' + $.param(args);
+ nurl = url.indexOf("?") !== -1 ? url + "&" + nurl : url + "?" + nurl;
+ var $form = $('<form>')
+ .attr('method', 'POST')
+ .attr('target', ifid)
+ .attr('enctype', "multipart/form-data")
+ .attr('action', nurl)
+ .append($('<input type="hidden" name="r" />').attr('value', payload_str))
+ .hide()
+ .appendTo($('body'));
+ var cleanUp = function() {
+ if ($iframe) {
+ $iframe.unbind("load").remove();
+ }
+ $form.remove();
+ };
+ var deferred = $.Deferred();
+ // the first bind is fired up when the iframe is added to the DOM
+ $iframe.bind('load', function() {
+ // the second bind is fired up when the result of the form submission is received
+ $iframe.unbind('load').bind('load', function() {
+ $.ajax({
+ url: url,
+ dataType: 'jsonp',
+ jsonp: 'jsonp',
+ type: 'GET',
+ cache: false,
+ data: {session_id: session_id, id: data.id}
+ }).always(function() {
+ cleanUp();
+ }).done(function() {
+ deferred.resolve.apply(deferred, arguments);
+ }).fail(function() {
+ deferred.reject.apply(deferred, arguments);
+ });
+ });
+ // now that the iframe can receive data, we fill and submit the form
+ $form.submit();
+ });
+ // append the iframe to the DOM (will trigger the first load)
+ $form.after($iframe);
+ if (settings.timeout) {
+ realSetTimeout(function() {
+ deferred.reject({});
+ }, settings.timeout);
+ }
+ return deferred;
+ }
+ });
+};
+
+// helper
+function realSetTimeout (fct, millis) {
+ var finished = new Date().getTime() + millis;
+ var wait = function() {
+ var current = new Date().getTime();
+ if (current < finished) {
+ setTimeout(wait, finished - current);
+ } else {
+ fct();
+ }
+ };
+ setTimeout(wait, millis);
+}
+
+function loadCSS(url) {
+ if (!$('link[href="' + url + '"]').length) {
+ $('head').append($('<link>', {
+ 'href': url,
+ 'rel': 'stylesheet',
+ 'type': 'text/css'
+ }));
+ }
+};
+
+function loadJS(url) {
+ var def = $.Deferred();
+ if ($('script[src="' + url + '"]').length) {
+ def.resolve();
+ } else {
+ var script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.src = url;
+ script.onload = script.onreadystatechange = function() {
+ if ((script.readyState && script.readyState != "loaded" && script.readyState != "complete") || script.onload_done) {
+ return;
+ }
+ script.onload_done = true;
+ def.resolve(url);
+ };
+ script.onerror = function () {
+ console.error("Error loading file", script.src);
+ def.reject(url);
+ };
+ var head = document.head || document.getElementsByTagName('head')[0];
+ head.appendChild(script);
+ }
+ return def;
+};
+
+/**
+ * Cooperative file download implementation, for ajaxy APIs.
+ *
+ * Requires that the server side implements an httprequest correctly
+ * setting the `fileToken` cookie to the value provided as the `token`
+ * parameter. The cookie *must* be set on the `/` path and *must not* be
+ * `httpOnly`.
+ *
+ * It would probably also be a good idea for the response to use a
+ * `Content-Disposition: attachment` header, especially if the MIME is a
+ * "known" type (e.g. text/plain, or for some browsers application/json
+ *
+ * @param {Object} options
+ * @param {String} [options.url] used to dynamically create a form
+ * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data
+ * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
+ * @param {Function} [options.success] callback in case of download success
+ * @param {Function} [options.error] callback in case of request error, provided with the error body
+ * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
+ */
+function get_file(options) {
+ // need to detect when the file is done downloading (not used
+ // yet, but we'll need it to fix the UI e.g. with a throbber
+ // while dump is being generated), iframe load event only fires
+ // when the iframe content loads, so we need to go smarter:
+ // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
+ var timer, token = new Date().getTime(),
+ cookie_name = 'fileToken', cookie_length = cookie_name.length,
+ CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
+ remove_form = false;
+
+
+ // iOS devices doesn't allow iframe use the way we do it,
+ // opening a new window seems the best way to workaround
+ if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
+ var params = _.extend({}, options.data || {}, {token: token});
+ var url = this.url(options.url, params);
+ return window.open(url);
+ }
+
+ var $form, $form_data = $('<div>');
+
+ var complete = function () {
+ if (options.complete) { options.complete(); }
+ clearTimeout(timer);
+ $form_data.remove();
+ $target.remove();
+ if (remove_form && $form) { $form.remove(); }
+ };
+ var $target = $('<iframe style="display: none;">')
+ .attr({id: id, name: id})
+ .appendTo(document.body)
+ .load(function () {
+ try {
+ if (options.error) {
+ var body = this.contentDocument.body;
+ var nodes = body.children.length === 0 ? body.childNodes : body.children;
+ var node = nodes[1] || nodes[0];
+ options.error(JSON.parse(node.textContent));
+ }
+ } finally {
+ complete();
+ }
+ });
+
+ if (options.form) {
+ $form = $(options.form);
+ } else {
+ remove_form = true;
+ $form = $('<form>', {
+ action: options.url,
+ method: 'POST'
+ }).appendTo(document.body);
+ }
+
+ var hparams = _.extend({}, options.data || {}, {token: token});
+ _.each(hparams, function (value, key) {
+ var $input = $form.find('[name=' + key +']');
+ if (!$input.length) {
+ $input = $('<input type="hidden" name="' + key + '">')
+ .appendTo($form_data);
+ }
+ $input.val(value);
+ });
+
+ $form
+ .append($form_data)
+ .attr('target', id)
+ .get(0).submit();
+
+ var waitLoop = function () {
+ var cookies = document.cookie.split(';');
+ // setup next check
+ timer = setTimeout(waitLoop, CHECK_INTERVAL);
+ for (var i=0; i<cookies.length; ++i) {
+ var cookie = cookies[i].replace(/^\s*/, '');
+ if (!cookie.indexOf(cookie_name === 0)) { continue; }
+ var cookie_val = cookie.substring(cookie_length + 1);
+ if (parseInt(cookie_val, 10) !== token) { continue; }
+
+ // clear cookie
+ document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
+ cookie_name, new Date().toGMTString());
+ if (options.success) { options.success(); }
+ complete();
+ return;
+ }
+ };
+ timer = setTimeout(waitLoop, CHECK_INTERVAL);
+};
+
+
+return {
+ jsonRpc: jsonRpc,
+ jsonpRpc: jsonpRpc,
+ loadCSS: loadCSS,
+ loadJS: loadJS,
+ get_file: get_file,
+};
+
+});
diff --git a/addons/web/static/src/js/framework/class.js b/addons/web/static/src/js/framework/class.js
new file mode 100644
index 00000000000..4de1afc66d9
--- /dev/null
+++ b/addons/web/static/src/js/framework/class.js
@@ -0,0 +1,154 @@
+odoo.define('web.Class', function () {
+"use strict";
+/**
+ * Improved John Resig's inheritance, based on:
+ *
+ * Simple JavaScript Inheritance
+ * By John Resig http://ejohn.org/
+ * MIT Licensed.
+ *
+ * Adds "include()"
+ *
+ * Defines The Class object. That object can be used to define and inherit classes using
+ * the extend() method.
+ *
+ * Example:
+ *
+ * var Person = openerp.Class.extend({
+ * init: function(isDancing){
+ * this.dancing = isDancing;
+ * },
+ * dance: function(){
+ * return this.dancing;
+ * }
+ * });
+ *
+ * The init() method act as a constructor. This class can be instancied this way:
+ *
+ * var person = new Person(true);
+ * person.dance();
+ *
+ * The Person class can also be extended again:
+ *
+ * var Ninja = Person.extend({
+ * init: function(){
+ * this._super( false );
+ * },
+ * dance: function(){
+ * // Call the inherited version of dance()
+ * return this._super();
+ * },
+ * swingSword: function(){
+ * return true;
+ * }
+ * });
+ *
+ * When extending a class, each re-defined method can use this._super() to call the previous
+ * implementation of that method.
+ */
+var initializing = false;
+var fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
+
+// The web Class implementation (does nothing)
+function OdooClass(){};
+
+/**
+ * Subclass an existing class
+ *
+ * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
+ */
+OdooClass.extend = function() {
+ var _super = this.prototype;
+ // Support mixins arguments
+ var args = _.toArray(arguments);
+ args.unshift({});
+ var prop = _.extend.apply(_,args);
+
+ // Instantiate a web class (but only create the instance,
+ // don't run the init constructor)
+ initializing = true;
+ var This = this;
+ var prototype = new This();
+ initializing = false;
+
+ // Copy the properties over onto the new prototype
+ _.each(prop, function(val, name) {
+ // Check if we're overwriting an existing function
+ prototype[name] = typeof prop[name] == "function" &&
+ fnTest.test(prop[name]) ?
+ (function(name, fn) {
+ return function() {
+ var tmp = this._super;
+
+ // Add a new ._super() method that is the same
+ // method but on the super-class
+ this._super = _super[name];
+
+ // The method only need to be bound temporarily, so
+ // we remove it when we're done executing
+ var ret = fn.apply(this, arguments);
+ this._super = tmp;
+
+ return ret;
+ };
+ })(name, prop[name]) :
+ prop[name];
+ });
+
+ // The dummy class constructor
+ function Class() {
+ if(this.constructor !== OdooClass){
+ throw new Error("You can only instanciate objects with the 'new' operator");
+ }
+ // All construction is actually done in the init method
+ this._super = null;
+ if (!initializing && this.init) {
+ var ret = this.init.apply(this, arguments);
+ if (ret) { return ret; }
+ }
+ return this;
+ }
+ Class.include = function (properties) {
+ _.each(properties, function(val, name) {
+ if (typeof properties[name] !== 'function'
+ || !fnTest.test(properties[name])) {
+ prototype[name] = properties[name];
+ } else if (typeof prototype[name] === 'function'
+ && prototype.hasOwnProperty(name)) {
+ prototype[name] = (function (name, fn, previous) {
+ return function () {
+ var tmp = this._super;
+ this._super = previous;
+ var ret = fn.apply(this, arguments);
+ this._super = tmp;
+ return ret;
+ };
+ })(name, properties[name], prototype[name]);
+ } else if (typeof _super[name] === 'function') {
+ prototype[name] = (function (name, fn) {
+ return function () {
+ var tmp = this._super;
+ this._super = _super[name];
+ var ret = fn.apply(this, arguments);
+ this._super = tmp;
+ return ret;
+ };
+ })(name, properties[name]);
+ }
+ });
+ };
+
+ // Populate our constructed prototype object
+ Class.prototype = prototype;
+
+ // Enforce the constructor to be what we expect
+ Class.constructor = Class;
+
+ // And make this class extendable
+ Class.extend = this.extend;
+
+ return Class;
+};
+
+return OdooClass;
+});
diff --git a/addons/web/static/src/js/framework/core.js b/addons/web/static/src/js/framework/core.js
new file mode 100644
index 00000000000..5bfe975c7fd
--- /dev/null
+++ b/addons/web/static/src/js/framework/core.js
@@ -0,0 +1,191 @@
+odoo.define('web.core', ['web.Class', 'web.mixins', 'web.Registry', 'web.translation', 'qweb', '_', '$'], function (require) {
+"use strict";
+
+var Class = require('web.Class');
+var mixins = require('web.mixins');
+var Registry = require('web.Registry');
+var translation = require('web.translation');
+
+var qweb = require('qweb');
+var _ = require('_');
+var $ = require('$');
+
+var debug = $.deparam($.param.querystring()).debug !== undefined;
+
+var _t = translation._t;
+var _lt = translation._lt;
+
+/**
+ * Event Bus used to bind events scoped in the current instance
+ */
+var Bus = Class.extend(mixins.EventDispatcherMixin, {
+ init: function() {
+ mixins.EventDispatcherMixin.init.call(this, parent);
+ var self = this;
+ // TODO fme: allow user to bind keys for some global actions.
+ // check gtk bindings
+ // http://unixpapa.com/js/key.html
+ _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) {
+ $('html').on(evtype, function(ev) {
+ self.trigger(evtype, ev);
+ });
+ });
+ _.each('resize,scroll'.split(','), function(evtype) {
+ $(window).on(evtype, function(ev) {
+ self.trigger(evtype, ev);
+ });
+ });
+ },
+});
+
+// Underscore customization
+//-------------------------------------------------------------------------
+_.str.toBoolElse = function (str, elseValues, trueValues, falseValues) {
+ var ret = _.str.toBool(str, trueValues, falseValues);
+ if (_.isUndefined(ret)) {
+ return elseValues;
+ }
+ return ret;
+};
+
+// IE patch
+//-------------------------------------------------------------------------
+if (typeof(console) === "undefined") {
+ // Even IE9 only exposes console object if debug window opened
+ window.console = {};
+ ('log error debug info warn assert clear dir dirxml trace group'
+ + ' groupCollapsed groupEnd time timeEnd profile profileEnd count'
+ + ' exception').split(/\s+/).forEach(function(property) {
+ console[property] = _.identity;
+ });
+}
+
+/**
+ Some hack to make placeholders work in ie9.
+*/
+if (!('placeholder' in document.createElement('input'))) {
+ document.addEventListener("DOMNodeInserted",function(event){
+ var nodename = event.target.nodeName.toLowerCase();
+ if ( nodename === "input" || nodename == "textarea" ) {
+ $(event.target).placeholder();
+ }
+ });
+}
+
+
+
+/**
+ * Lazy translation function, only performs the translation when actually
+ * printed (e.g. inserted into a template)
+ *
+ * Useful when defining translatable strings in code evaluated before the
+ * translation database is loaded, as class attributes or at the top-level of
+ * an OpenERP Web module
+ *
+ * @param {String} s string to translate
+ * @returns {Object} lazy translation object
+ */
+qweb.debug = debug;
+_.extend(qweb.default_dict, {
+ '__debug__': debug,
+ 'moment': function(date) { return new moment(date); },
+});
+
+qweb.preprocess_node = function() {
+ // Note that 'this' is the Qweb Node
+ switch (this.node.nodeType) {
+ case Node.TEXT_NODE:
+ case Node.CDATA_SECTION_NODE:
+ // Text and CDATAs
+ var translation = this.node.parentNode.attributes['t-translation'];
+ if (translation && translation.value === 'off') {
+ return;
+ }
+ var match = /^(\s*)([\s\S]+?)(\s*)$/.exec(this.node.data);
+ if (match) {
+ this.node.data = match[1] + _t(match[2]) + match[3];
+ }
+ break;
+ case Node.ELEMENT_NODE:
+ // Element
+ var attr, attrs = ['label', 'title', 'alt', 'placeholder'];
+ while ((attr = attrs.pop())) {
+ if (this.attributes[attr]) {
+ this.attributes[attr] = _t(this.attributes[attr]);
+ }
+ }
+ }
+};
+
+/** Setup jQuery timeago */
+/*
+ * Strings in timeago are "composed" with prefixes, words and suffixes. This
+ * makes their detection by our translating system impossible. Use all literal
+ * strings we're using with a translation mark here so the extractor can do its
+ * job.
+ */
+{
+ _t('less than a minute ago');
+ _t('about a minute ago');
+ _t('%d minutes ago');
+ _t('about an hour ago');
+ _t('%d hours ago');
+ _t('a day ago');
+ _t('%d days ago');
+ _t('about a month ago');
+ _t('%d months ago');
+ _t('about a year ago');
+ _t('%d years ago');
+}
+
+$.async_when = function() {
+ var async = false;
+ var def = $.Deferred();
+ $.when.apply($, arguments).done(function() {
+ var args = arguments;
+ var action = function() {
+ def.resolve.apply(def, args);
+ };
+ if (async)
+ action();
+ else
+ setTimeout(action, 0);
+ }).fail(function() {
+ var args = arguments;
+ var action = function() {
+ def.reject.apply(def, args);
+ };
+ if (async)
+ action();
+ else
+ setTimeout(action, 0);
+ });
+ async = true;
+ return def;
+};
+
+return {
+ debug: debug,
+ qweb: qweb,
+
+ // core classes and functions
+ Class: Class,
+ mixins: mixins,
+ bus: new Bus (),
+ _t: _t,
+ _lt: _lt,
+
+ // registries
+ view_registry: new Registry(),
+ crash_registry: new Registry(),
+ action_registry : new Registry(),
+ form_widget_registry: new Registry(),
+ form_tag_registry: new Registry(),
+ form_custom_registry: new Registry(),
+ list_widget_registry: new Registry(),
+ search_widgets_registry: new Registry(),
+ search_filters_registry: new Registry(),
+};
+
+
+});
diff --git a/addons/web/static/src/js/framework/crash_manager.js b/addons/web/static/src/js/framework/crash_manager.js
new file mode 100644
index 00000000000..416ef4b4136
--- /dev/null
+++ b/addons/web/static/src/js/framework/crash_manager.js
@@ -0,0 +1,195 @@
+odoo.define('web.crash_manager', ['web.CrashManager'], function (require) {
+ var CrashManager = require('web.CrashManager');
+ return new CrashManager();
+});
+
+odoo.define('web.CrashManager', ['web.ajax', 'web.core', 'web.Dialog', 'web.session'], function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var session = require('web.session');
+
+var QWeb = core.qweb;
+var _t = core._t;
+var _lt = core._lt;
+
+var map_title ={
+ user_error: _lt('Warning'),
+ warning: _lt('Warning'),
+ access_error: _lt('Access Error'),
+ missing_error: _lt('Missing Record'),
+ validation_error: _lt('Validation Error'),
+ except_orm: _lt('Global Business Error'),
+ access_denied: _lt('Access Denied'),
+};
+
+var CrashManager = core.Class.extend({
+ init: function() {
+ this.active = true;
+ },
+ enable: function () {
+ this.active = true;
+ },
+ disable: function () {
+ this.active = false;
+ },
+ rpc_error: function(error) {
+ if (!this.active) {
+ return;
+ }
+ if (error.code == -32098) {
+ $.blockUI({ message: '' , overlayCSS: {'z-index': 9999, backgroundColor: '#FFFFFF', opacity: 0.0, cursor: 'wait'}});
+ var $indicator = $('<div class="oe_indicator">' + _t("Trying to reconnect... ") + '<i class="fa fa-refresh fa-spin"></i></div>');
+ $indicator.prependTo("body");
+ var timeinterval = setInterval(function(){
+ ajax.jsonRpc('/web/webclient/version_info').then(function() {
+ clearInterval(timeinterval);
+ $indicator.html(_t("You are back online"));
+ $indicator.delay(2000).fadeOut('slow',function(){
+ $indicator.remove();
+ });
+ $.unblockUI();
+ });
+ }, 2000);
+ return;
+ }
+ var handler = core.crash_registry.get(error.data.name, true);
+ if (handler) {
+ new (handler)(this, error).display();
+ return;
+ }
+ if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
+ this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
+ return;
+ }
+ if (_.has(map_title, error.data.exception_type)) {
+ if(error.data.exception_type == 'except_orm'){
+ if(error.data.arguments[1]) {
+ error = _.extend({}, error,
+ {
+ data: _.extend({}, error.data,
+ {
+ message: error.data.arguments[1],
+ title: error.data.arguments[0] !== 'Warning' ? (" - " + error.data.arguments[0]) : '',
+ })
+ });
+ }
+ else {
+ error = _.extend({}, error,
+ {
+ data: _.extend({}, error.data,
+ {
+ message: error.data.arguments[0],
+ title: '',
+ })
+ });
+ }
+ }
+ else {
+ error = _.extend({}, error,
+ {
+ data: _.extend({}, error.data,
+ {
+ message: error.data.arguments[0],
+ title: map_title[error.data.exception_type] !== 'Warning' ? (" - " + map_title[error.data.exception_type]) : '',
+ })
+ });
+ }
+
+ this.show_warning(error);
+ //InternalError
+
+ } else {
+ this.show_error(error);
+ }
+ },
+ show_warning: function(error) {
+ if (!this.active) {
+ return;
+ }
+ new Dialog(this, {
+ size: 'medium',
+ title: "Odoo " + (_.str.capitalize(error.type) || _t("Warning")),
+ subtitle: error.data.title,
+ buttons: [
+ {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
+ ],
+ }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
+ },
+ show_error: function(error) {
+ if (!this.active) {
+ return;
+ }
+ var buttons = {};
+ buttons[_t("Ok")] = function() {
+ this.parents('.modal').modal('hide');
+ };
+ new Dialog(this, {
+ title: "Odoo " + _.str.capitalize(error.type),
+ buttons: buttons
+ }, QWeb.render('CrashManager.error', {session: session, error: error})).open();
+ },
+ show_message: function(exception) {
+ this.show_error({
+ type: _t("Client Error"),
+ message: exception,
+ data: {debug: ""}
+ });
+ },
+});
+
+/**
+ An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
+*/
+var ExceptionHandler = {
+ /**
+ @param parent The parent.
+ @param error The error object as returned by the JSON-RPC implementation.
+ */
+ init: function(parent, error) {},
+ /**
+ Called to inform to display the widget, if necessary. A typical way would be to implement
+ this interface in a class extending instance.web.Dialog and simply display the dialog in this
+ method.
+ */
+ display: function() {},
+};
+
+
+/**
+ * Handle redirection warnings, which behave more or less like a regular
+ * warning, with an additional redirection button.
+ */
+var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
+ init: function(parent, error) {
+ this._super(parent);
+ this.error = error;
+ },
+ display: function() {
+ var self = this;
+ var error = this.error;
+ error.data.message = error.data.arguments[0];
+
+ new Dialog(this, {
+ size: 'medium',
+ title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
+ buttons: [
+ {text: error.data.arguments[2],
+ oe_link_class : 'oe_highlight',
+ post_text : _t("or"),
+ click: function() {
+ window.location.href='#action='+error.data.arguments[1];
+ self.destroy();
+ }},
+ {text: _t("Cancel"), oe_link_class: 'oe_link', click: function() { self.$el.parents('.modal').modal('hide'); self.destroy();}}
+ ],
+ }, QWeb.render('CrashManager.warning', {error: error})).open();
+ }
+});
+
+core.crash_registry.add('openerp.exceptions.RedirectWarning', RedirectWarningHandler);
+
+return CrashManager;
+});
diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/framework/data.js
index 82edbc2f35b..f8f091be80d 100644
--- a/addons/web/static/src/js/data.js
+++ b/addons/web/static/src/js/framework/data.js
@@ -1,8 +1,14 @@
+odoo.define('web.data', ['web.core', 'web.Model', 'web.session', 'web.pyeval'], function (require) {
+"use strict";
-(function() {
+var core = require('web.core');
+var Model = require('web.Model');
+var session = require('web.session');
+var pyeval = require('web.pyeval');
-var instance = openerp;
-openerp.web.data = {};
+var Class = core.Class;
+var mixins = core.mixins;
+var _t = core._t;
/**
* Serializes the sort criterion array of a dataset into a form which can be
@@ -11,7 +17,7 @@ openerp.web.data = {};
* @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting
* @returns {String} SQL-like sorting string (``ORDER BY``) clause
*/
-instance.web.serialize_sort = function (criterion) {
+function serialize_sort(criterion) {
return _.map(criterion,
function (criteria) {
if (criteria[0] === '-') {
@@ -19,20 +25,20 @@ instance.web.serialize_sort = function (criterion) {
}
return criteria + ' ASC';
}).join(', ');
-};
+}
/**
* Reverse of the serialize_sort function: convert an array of SQL-like sort
* descriptors into a list of fields prefixed with '-' if necessary.
*/
- instance.web.deserialize_sort = function (criterion) {
+function deserialize_sort(criterion) {
return _.map(criterion, function (criteria) {
var split = _.without(criteria.split(' '), '');
return (split[1] && split[1].toLowerCase() === 'desc' ? '-' : '') + split[0];
});
-};
+}
-instance.web.Query = instance.web.Class.extend({
+var Query = Class.extend({
init: function (model, fields) {
this._model = model;
this._fields = fields;
@@ -45,7 +51,7 @@ instance.web.Query = instance.web.Class.extend({
},
clone: function (to_set) {
to_set = to_set || {};
- var q = new instance.web.Query(this._model, this._fields);
+ var q = new Query(this._model, this._fields);
q._context = this._context;
q._filter = this._filter;
q._lazy = this._lazy;
@@ -57,11 +63,11 @@ instance.web.Query = instance.web.Class.extend({
if (!to_set.hasOwnProperty(key)) { continue; }
switch(key) {
case 'filter':
- q._filter = new instance.web.CompoundDomain(
+ q._filter = new CompoundDomain(
q._filter, to_set.filter);
break;
case 'context':
- q._context = new instance.web.CompoundContext(
+ q._context = new CompoundContext(
q._context, to_set.context);
break;
case 'lazy':
@@ -76,16 +82,16 @@ instance.web.Query = instance.web.Class.extend({
_execute: function (options) {
var self = this;
options = options || {};
- return instance.session.rpc('/web/dataset/search_read', {
+ return session.rpc('/web/dataset/search_read', {
model: this._model.name,
fields: this._fields || false,
- domain: instance.web.pyeval.eval('domains',
+ domain: pyeval.eval('domains',
[this._model.domain(this._filter)]),
- context: instance.web.pyeval.eval('contexts',
+ context: pyeval.eval('contexts',
[this._model.context(this._context)]),
offset: this._offset,
limit: this._limit,
- sort: instance.web.serialize_sort(this._order_by)
+ sort: serialize_sort(this._order_by)
}, options).then(function (results) {
self._count = results.length;
return results.records;
@@ -132,7 +138,7 @@ instance.web.Query = instance.web.Class.extend({
* @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
*/
group_by: function (grouping) {
- var ctx = instance.web.pyeval.eval(
+ var ctx = pyeval.eval(
'context', this._model.context(this._context));
// undefined passed in explicitly (!)
@@ -143,7 +149,7 @@ instance.web.Query = instance.web.Class.extend({
if (!(grouping instanceof Array)) {
grouping = _.toArray(arguments);
}
- if (_.isEmpty(grouping) && !ctx['group_by_no_leaf']) {
+ if (_.isEmpty(grouping) && !ctx.group_by_no_leaf) {
return null;
}
var raw_fields = _.map(grouping.concat(this._fields || []), function (field) {
@@ -159,7 +165,7 @@ instance.web.Query = instance.web.Class.extend({
offset: this._offset,
lazy: this._lazy,
limit: this._limit,
- orderby: instance.web.serialize_sort(this._order_by) || false
+ orderby: serialize_sort(this._order_by) || false
}).then(function (results) {
return _(results).map(function (result) {
// FIX: querygroup initialization
@@ -167,7 +173,7 @@ instance.web.Query = instance.web.Class.extend({
result.__context.group_by = result.__context.group_by || [];
_.defaults(result.__context, ctx);
var grouping_fields = self._lazy ? [grouping[0]] : grouping;
- return new instance.web.QueryGroup(
+ return new QueryGroup(
self._model.name, grouping_fields, result);
});
});
@@ -243,7 +249,7 @@ instance.web.Query = instance.web.Class.extend({
}
});
-instance.web.QueryGroup = instance.web.Class.extend({
+var QueryGroup = Class.extend({
init: function (model, grouping_fields, read_group_group) {
// In cases where group_by_no_leaf and no group_by, the result of
// read_group has aggregate fields but no __context or __domain.
@@ -263,7 +269,7 @@ instance.web.QueryGroup = instance.web.Class.extend({
aggregates[key] = value || 0;
});
- this.model = new instance.web.Model(
+ this.model = new Model(
model, fixed_group.__context, fixed_group.__domain);
var group_size = fixed_group[count_key] || fixed_group.__count || 0;
@@ -282,7 +288,7 @@ instance.web.QueryGroup = instance.web.Class.extend({
length: group_size,
value: value,
// A group is open-able if it's not a leaf in group_by_no_leaf mode
- has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),
+ has_children: !(leaf_group && fixed_group.__context.group_by_no_leaf),
aggregates: aggregates
};
@@ -298,136 +304,7 @@ instance.web.QueryGroup = instance.web.Class.extend({
}
});
-instance.web.Model.include({
- /**
- new openerp.web.Model([session,] model_name[, context[, domain]])
-
- @constructs instance.web.Model
- @extends instance.web.Class
-
- @param {openerp.web.Session} [session] The session object used to communicate with
- the server.
- @param {String} model_name name of the OpenERP model this object is bound to
- @param {Object} [context]
- @param {Array} [domain]
- */
- init: function() {
- var session, model_name, context, domain;
- var args = _.toArray(arguments);
- args.reverse();
- session = args.pop();
- if (session && ! (session instanceof openerp.web.Session)) {
- model_name = session;
- session = null;
- } else {
- model_name = args.pop();
- }
- context = args.length > 0 ? args.pop() : null;
- domain = args.length > 0 ? args.pop() : null;
-
- this._super(session, model_name, context, domain);
- this._context = context || {};
- this._domain = domain || [];
- },
- session: function() {
- if (! this._session)
- return instance.session;
- return this._super();
- },
- /**
- * @deprecated does not allow to specify kwargs, directly use call() instead
- */
- get_func: function (method_name) {
- var self = this;
- return function () {
- return self.call(method_name, _.toArray(arguments));
- };
- },
- /**
- * Call a method (over RPC) on the bound OpenERP model.
- *
- * @param {String} method name of the method to call
- * @param {Array} [args] positional arguments
- * @param {Object} [kwargs] keyword arguments
- * @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<>} call result
- */
- call: function (method, args, kwargs, options) {
- args = args || [];
- kwargs = kwargs || {};
- if (!_.isArray(args)) {
- // call(method, kwargs)
- kwargs = args;
- args = [];
- }
- instance.web.pyeval.ensure_evaluated(args, kwargs);
- return this._super(method, args, kwargs, options);
- },
- /**
- * Fetches a Query instance bound to this model, for searching
- *
- * @param {Array<String>} [fields] fields to ultimately fetch during the search
- * @returns {instance.web.Query}
- */
- query: function (fields) {
- return new instance.web.Query(this, fields);
- },
- /**
- * Executes a signal on the designated workflow, on the bound OpenERP model
- *
- * @param {Number} id workflow identifier
- * @param {String} signal signal to trigger on the workflow
- */
- exec_workflow: function (id, signal) {
- return this.session().rpc('/web/dataset/exec_workflow', {
- model: this.name,
- id: id,
- signal: signal
- });
- },
- /**
- * Fetches the model's domain, combined with the provided domain if any
- *
- * @param {Array} [domain] to combine with the model's internal domain
- * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
- */
- domain: function (domain) {
- if (!domain) { return this._domain; }
- return new instance.web.CompoundDomain(
- this._domain, domain);
- },
- /**
- * Fetches the combination of the user's context and the domain context,
- * combined with the provided context if any
- *
- * @param {Object} [context] to combine with the model's internal context
- * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
- */
- context: function (context) {
- return new instance.web.CompoundContext(
- instance.session.user_context, this._context, context || {});
- },
- /**
- * Button action caller, needs to perform cleanup if an action is returned
- * from the button (parsing of context and domain, and fixup of the views
- * collection for act_window actions)
- *
- * FIXME: remove when evaluator integrated
- */
- call_button: function (method, args) {
- instance.web.pyeval.ensure_evaluated(args, {});
- return this.session().rpc('/web/dataset/call_button', {
- model: this.name,
- method: method,
- // Should not be necessary anymore. Integrate remote in this?
- domain_id: null,
- context_id: args.length - 1,
- args: args || []
- });
- },
-});
-
-instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, {
+var DataSet = Class.extend(mixins.PropertiesMixin, {
/**
* Collection of OpenERP records, used to share records and the current selection between views.
*
@@ -436,12 +313,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
* @param {String} model the OpenERP model this dataset will manage
*/
init: function(parent, model, context) {
- instance.web.PropertiesMixin.init.call(this);
+ mixins.PropertiesMixin.init.call(this);
this.model = model;
this.context = context || {};
this.index = null;
this._sort = [];
- this._model = new instance.web.Model(model, context);
+ this._model = new Model(model, context);
},
previous: function () {
this.index -= 1;
@@ -500,7 +377,7 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
if (options.check_access_rule === true){
method = 'search_read';
ids_arg = [['id', 'in', ids]];
- context = new instance.web.CompoundContext(context, {active_test: false});
+ context = new CompoundContext(context, {active_test: false});
}
return this._model.call(method,
[ids_arg, fields || false],
@@ -689,7 +566,7 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
*/
sort: function (field, force_reverse) {
if (!field) {
- return instance.web.serialize_sort(this._sort);
+ return serialize_sort(this._sort);
}
var reverse = force_reverse || (this._sort[0] === field);
this._sort.splice.apply(
@@ -706,7 +583,7 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
* Odoo's ORM (such as 'name desc', 'product_id', 'order_date asc')
*/
set_sort: function (fields_list) {
- this._sort = instance.web.deserialize_sort(fields_list);
+ this._sort = deserialize_sort(fields_list);
},
size: function () {
return this.ids.length;
@@ -729,10 +606,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
*/
resequence: function (ids, options) {
options = options || {};
- return instance.session.rpc('/web/dataset/resequence', {
+ return session.rpc('/web/dataset/resequence', {
model: this.model,
ids: ids,
- context: instance.web.pyeval.eval(
+ context: pyeval.eval(
'context', this.get_context(options.context)),
}).then(function (results) {
return results;
@@ -740,9 +617,8 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
},
});
-instance.web.DataSetStatic = instance.web.DataSet.extend({
+var DataSetStatic = DataSet.extend({
init: function(parent, model, context, ids) {
- var self = this;
this._super(parent, model, context);
// all local records
this.ids = ids || [];
@@ -770,7 +646,7 @@ instance.web.DataSetStatic = instance.web.DataSet.extend({
},
});
-instance.web.DataSetSearch = instance.web.DataSet.extend({
+var DataSetSearch = DataSet.extend({
/**
* @constructs instance.web.DataSetSearch
* @extends instance.web.DataSet
@@ -785,7 +661,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({
this.domain = domain || [];
this._length = null;
this.ids = [];
- this._model = new instance.web.Model(model, context, domain);
+ this._model = new Model(model, context, domain);
},
/**
* Read a slice of the records represented by this DataSet, based on its
@@ -833,7 +709,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({
},
unlink: function(ids, callback, error_callback) {
var self = this;
- return this._super(ids).done(function(result) {
+ return this._super(ids).done(function() {
self.remove_ids( ids);
self.trigger("dataset_changed", ids, callback, error_callback);
});
@@ -846,7 +722,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({
}
});
-instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
+var BufferedDataSet = DataSetStatic.extend({
virtual_id_prefix: "one2many_v_id_",
debug_mode: true,
init: function() {
@@ -969,8 +845,8 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
sign = -1;
field = field.slice(1);
}
- if(!a[field] && a[field] !== 0){ return sign}
- if(!b[field] && b[field] !== 0){ return (sign == -1) ? 1 : -1}
+ if(!a[field] && a[field] !== 0){ return sign; }
+ if(!b[field] && b[field] !== 0){ return (sign == -1) ? 1 : -1; }
//m2o should be searched based on value[1] not based whole value(i.e. [id, value])
if(_.isArray(a[field]) && a[field].length == 2 && _.isString(a[field][1])){
return sign * compare(a[field][1], b[field][1]);
@@ -1051,9 +927,10 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
this.trigger("dataset_changed", n_ids);
},
});
-instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
-instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
+BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
+
+var ProxyDataSet = DataSetSearch.extend({
init: function() {
this._super.apply(this, arguments);
this.create_function = null;
@@ -1099,7 +976,7 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
},
});
-instance.web.CompoundContext = instance.web.Class.extend({
+var CompoundContext = Class.extend({
init: function () {
this.__ref = "compound_context";
this.__contexts = [];
@@ -1121,11 +998,11 @@ instance.web.CompoundContext = instance.web.Class.extend({
return this.__eval_context;
},
eval: function() {
- return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true});
+ return pyeval.eval('context', this, undefined, {no_user_context: true});
},
});
-instance.web.CompoundDomain = instance.web.Class.extend({
+var CompoundDomain = Class.extend({
init: function () {
this.__ref = "compound_domain";
this.__domains = [];
@@ -1147,198 +1024,94 @@ instance.web.CompoundDomain = instance.web.Class.extend({
return this.__eval_context;
},
eval: function() {
- return instance.web.pyeval.eval('domain', this);
+ return pyeval.eval('domain', this);
},
});
-instance.web.DropMisordered = instance.web.Class.extend({
- /**
- * @constructs instance.web.DropMisordered
- * @extends instance.web.Class
- *
- * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored
- */
- init: function (failMisordered) {
- // local sequence number, for requests sent
- this.lsn = 0;
- // remote sequence number, seqnum of last received request
- this.rsn = -1;
- this.failMisordered = failMisordered || false;
- },
- /**
- * Adds a deferred (usually an async request) to the sequencer
- *
- * @param {$.Deferred} deferred to ensure add
- * @returns {$.Deferred}
- */
- add: function (deferred) {
- var res = $.Deferred();
-
- var self = this, seq = this.lsn++;
- deferred.done(function () {
- if (seq > self.rsn) {
- self.rsn = seq;
- res.resolve.apply(res, arguments);
- } else if (self.failMisordered) {
- res.reject();
+function compute_domain (expr, fields) {
+ if (! (expr instanceof Array))
+ return !! expr;
+ var stack = [];
+ for (var i = expr.length - 1; i >= 0; i--) {
+ var ex = expr[i];
+ if (ex.length == 1) {
+ var top = stack.pop();
+ switch (ex) {
+ case '|':
+ stack.push(stack.pop() || top);
+ continue;
+ case '&':
+ stack.push(stack.pop() && top);
+ continue;
+ case '!':
+ stack.push(!top);
+ continue;
+ default:
+ throw new Error(_.str.sprintf(
+ _t("Unknown operator %s in domain %s"),
+ ex, JSON.stringify(expr)));
}
- }).fail(function () {
- res.reject.apply(res, arguments);
- });
-
- return res.promise();
- }
-});
-
-instance.web.SimpleIndexedDB = instance.web.Class.extend({
- /**
- * A simple wrapper around IndexedDB that provides an asynchronous
- * localStorage-like Key-Value api that persists between browser
- * restarts.
- *
- * It will not work if the browser doesn't support a recent version
- * of IndexedDB, and it may fail if the user refuses db access.
- *
- * All instances of SimpleIndexedDB will by default refer to the same
- * IndexedDB database, if you want to pick another one, use the 'name'
- * option on instanciation.
- */
- init: function(opts){
- var self = this;
- var opts = opts || {};
-
- this.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
- this.db = undefined;
- this._ready = new $.Deferred();
-
- if(this.indexedDB && this.indexedDB.open){
- var request = this.indexedDB.open( opts.name || "SimpleIndexedDB" ,1);
-
- request.onerror = function(event){
- self.db = null;
- self._ready.reject(event.target.error);
- };
-
- request.onsuccess = function(event){
- self.db = request.result;
- self._ready.resolve();
- };
-
- request.onupgradeneeded = function(event){
- self.db = event.target.result;
-
- var objectStore = self.db.createObjectStore("keyvals", { keyPath:"key" });
- self._ready.resolve();
- };
- }else{
- this.db = null;
- this._ready.reject({type:'UnknownError', message:'IndexedDB is not supported by your Browser'});
}
- },
-
- /**
- * returns true if the browser supports the necessary IndexedDB API
- * (but doesn't tell if the db can be created, check ready() for that)
- */
- isSupportedByBrowser: function(){
- return this.indexedDB && this.indexedDB.open;
- },
-
- /**
- * returns a deferred that resolves when the db has been successfully
- * initialized. done/failed callbacks are optional.
- */
- ready: function(done,failed){
- this._ready.then(done,failed);
- return this._ready.promise();
- },
-
- /**
- * fetches the value associated to 'key' in the db. if the key doesn't
- * exists, it returns undefined. The returned value is provided as a
- * deferred, or as the first parameter of the optional 'done' callback.
- */
- getItem: function(key,done,failed){
- var self = this;
- var def = new $.Deferred();
- def.then(done,failed);
-
- this._ready.then(function(){
- var request = self.db.transaction(["keyvals"],"readwrite")
- .objectStore("keyvals")
- .get(key);
-
- request.onsuccess = function(){
- def.resolve(request.result ? request.result.value : undefined);
- };
-
- request.onerror = function(event){
- def.reject(event.target.error);
- };
- },function(){
- def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'});
- });
-
- return def.promise();
- },
-
- /**
- * Associates a value to 'key' in the db, overwriting previous value if
- * necessary. Contrary to localStorage, the value is not limited to strings and
- * can be any javascript object, even with cyclic references !
- *
- * Be sure to check for failure as the user may refuse to have data localy stored.
- */
- setItem: function(key,value,done,failed){
- var self = this;
- var def = new $.Deferred();
- def.then(done,failed);
-
- this._ready.then(function(){
- var request = self.db.transaction(["keyvals"],"readwrite")
- .objectStore("keyvals")
- .put( {key:key, value:value} );
-
- request.onsuccess = function(){
- def.resolve();
- };
-
- request.onerror = function(event){
- def.reject(event.target.error);
- };
- },function(){
- def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'});
- });
-
- return def.promise();
- },
-
- /**
- * Removes the value associated with 'key' from the db.
- */
- removeItem: function(key,done,failed){
- var self = this;
- var def = new $.Deferred();
- def.then(done,failed);
- this._ready.then(function(){
- var request = self.db.transaction(["keyvals"],"readwrite")
- .objectStore("keyvals")
- .delete(key);
+ var field = fields[ex[0]];
+ if (!field) {
+ throw new Error(_.str.sprintf(
+ _t("Unknown field %s in domain %s"),
+ ex[0], JSON.stringify(expr)));
+ }
+ var field_value = field.get_value ? field.get_value() : field.value;
+ var op = ex[1];
+ var val = ex[2];
- request.onsuccess = function(){
- def.resolve();
- };
+ switch (op.toLowerCase()) {
+ case '=':
+ case '==':
+ stack.push(_.isEqual(field_value, val));
+ break;
+ case '!=':
+ case '<>':
+ stack.push(!_.isEqual(field_value, val));
+ break;
+ case '<':
+ stack.push(field_value < val);
+ break;
+ case '>':
+ stack.push(field_value > val);
+ break;
+ case '<=':
+ stack.push(field_value <= val);
+ break;
+ case '>=':
+ stack.push(field_value >= val);
+ break;
+ case 'in':
+ if (!_.isArray(val)) val = [val];
+ stack.push(_(val).contains(field_value));
+ break;
+ case 'not in':
+ if (!_.isArray(val)) val = [val];
+ stack.push(!_(val).contains(field_value));
+ break;
+ default:
+ console.warn(
+ _t("Unsupported operator %s in domain %s"),
+ op, JSON.stringify(expr));
+ }
+ }
+ return _.all(stack, _.identity);
+}
- request.onerror = function(event){
- def.reject(event.target.error);
- };
- },function(){
- def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'});
- });
- return def.promise();
- },
-});
+return {
+ Query: Query,
+ DataSet: DataSet,
+ DataSetStatic: DataSetStatic,
+ DataSetSearch: DataSetSearch,
+ BufferedDataSet: BufferedDataSet,
+ ProxyDataSet: ProxyDataSet,
+ CompoundContext: CompoundContext,
+ CompoundDomain: CompoundDomain,
+ compute_domain: compute_domain,
+};
-})();
+}); \ No newline at end of file
diff --git a/addons/web/static/src/js/framework/dialog.js b/addons/web/static/src/js/framework/dialog.js
new file mode 100644
index 00000000000..9464c452124
--- /dev/null
+++ b/addons/web/static/src/js/framework/dialog.js
@@ -0,0 +1,194 @@
+odoo.define('web.Dialog', ['web.core', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+var opened_modals = [];
+
+/**
+ A useful class to handle dialogs.
+
+ Attributes:
+ - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
+ during the lifecycle of the dialog.
+*/
+var Dialog = Widget.extend({
+ dialog_title: "",
+ /**
+ Constructor.
+
+ @param {Widget} parent
+ @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
+ dictionary can contain the following keys:
+ - size: one of the following: 'large', 'medium', 'small'
+ - dialogClass: class to add to the body of dialog
+ - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
+ label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
+ corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
+ in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
+ feature does not allow that kind of personalization.
+ - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
+ @param {jQuery object} content Some content to replace this.$el .
+ */
+ init: function (parent, options, content) {
+ this._super(parent);
+ this.content_to_set = content;
+ this.dialog_options = {
+ destroy_on_close: true,
+ size: 'large', //'medium', 'small'
+ buttons: null,
+ };
+ if (options) {
+ _.extend(this.dialog_options, options);
+ }
+ this.on("closing", this, this._closing);
+ this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
+ },
+ renderElement: function() {
+ if (this.content_to_set) {
+ this.setElement(this.content_to_set);
+ } else if (this.template) {
+ this._super();
+ }
+ },
+ /**
+ Opens the popup. Inits the dialog if it is not already inited.
+
+ @return this
+ */
+ open: function() {
+ if (!this.dialog_inited) {
+ this.init_dialog();
+ }
+ this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
+ $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal is opened
+ //add to list of currently opened modal
+ opened_modals.push(this.$dialog_box);
+ return this;
+ },
+ _add_buttons: function(buttons) {
+ var self = this;
+ var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
+ _.each(buttons, function(fn, text) {
+ // buttons can be object or array
+ var pre_text = fn.pre_text || "";
+ var post_text = fn.post_text || "";
+ var oe_link_class = fn.oe_link_class;
+ if (!_.isFunction(fn)) {
+ text = fn.text;
+ fn = fn.click;
+ }
+ var $but = $(QWeb.render('WidgetButton', { widget : { pre_text: pre_text, post_text: post_text, string: text, node: { attrs: {'class': oe_link_class} }}}));
+ $customButons.append($but);
+ $but.filter('button').on('click', function(ev) {
+ fn.call(self.$el, ev);
+ });
+ });
+ },
+ /**
+ Initializes the popup.
+
+ @return The result returned by start().
+ */
+ init_dialog: function() {
+ var self = this;
+ var options = _.extend({}, this.dialog_options);
+ options.title = options.title || this.dialog_title;
+ if (options.buttons) {
+ this._add_buttons(options.buttons);
+ delete(options.buttons);
+ }
+ this.renderElement();
+ this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
+ this.$el.modal({
+ 'backdrop': false,
+ 'keyboard': true,
+ });
+ if (options.size !== 'large'){
+ var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
+ if (options.size === 'small'){
+ dialog_class_size.addClass('modal-sm');
+ }
+ }
+
+ this.$el.appendTo(this.$dialog_box.find(".modal-body"));
+ var $dialog_content = this.$dialog_box.find('.modal-content');
+ if (options.dialogClass){
+ $dialog_content.find(".modal-body").addClass(options.dialogClass);
+ }
+ $dialog_content.openerpClass();
+
+ this.$dialog_box.on('hidden.bs.modal', this, function() {
+ self.close();
+ });
+ this.$dialog_box.modal('show');
+
+ this.dialog_inited = true;
+ var res = this.start();
+ return res;
+ },
+ /**
+ Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
+ */
+ close: function(reason) {
+ if (this.dialog_inited && !this.__tmp_dialog_hiding) {
+ $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
+ if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal
+ this.__tmp_dialog_hiding = true;
+ this.$dialog_box.modal('hide');
+ this.__tmp_dialog_hiding = undefined;
+ }
+ this.trigger("closing", reason);
+ }
+ },
+ _closing: function() {
+ if (this.__tmp_dialog_destroying)
+ return;
+ if (this.dialog_options.destroy_on_close) {
+ this.__tmp_dialog_closing = true;
+ this.destroy();
+ this.__tmp_dialog_closing = undefined;
+ }
+ },
+ /**
+ Destroys the popup, also closes it.
+ */
+ destroy: function (reason) {
+ this.$buttons.remove();
+ _.each(this.getChildren(), function(el) {
+ el.destroy();
+ });
+ if (! this.__tmp_dialog_closing) {
+ this.__tmp_dialog_destroying = true;
+ this.close(reason);
+ this.__tmp_dialog_destroying = undefined;
+ }
+ if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
+ //we need this to put the instruction to remove modal from DOM at the end
+ //of the queue, otherwise it might already have been removed before the modal-backdrop
+ //is removed when pressing escape key
+ var $element = this.$dialog_box;
+ setTimeout(function () {
+ //remove modal from list of opened modal since we just destroy it
+ var modal_list_index = $.inArray($element, opened_modals);
+ if (modal_list_index > -1){
+ opened_modals.splice(modal_list_index,1)[0].remove();
+ }
+ if (opened_modals.length > 0){
+ //we still have other opened modal so we should focus it
+ opened_modals[opened_modals.length-1].focus();
+ //keep class modal-open (deleted by bootstrap hide fnct) on body
+ //to allow scrolling inside the modal
+ $('body').addClass('modal-open');
+ }
+ },0);
+ }
+ this._super();
+ }
+});
+
+return Dialog;
+
+});
diff --git a/addons/web/static/src/js/framework/formats.js b/addons/web/static/src/js/framework/formats.js
new file mode 100644
index 00000000000..4126447c76a
--- /dev/null
+++ b/addons/web/static/src/js/framework/formats.js
@@ -0,0 +1,189 @@
+odoo.define('web.formats', ['web.core', 'web.time', 'web.utils'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var time = require('web.time');
+var utils = require('web.utils');
+
+var _t = core._t;
+
+
+/**
+ * Formats a single atomic value based on a field descriptor
+ *
+ * @param {Object} value read from OpenERP
+ * @param {Object} descriptor union of orm field and view field
+ * @param {Object} [descriptor.widget] widget to use to display the value
+ * @param {Object} descriptor.type fallback if no widget is provided, or if the provided widget is unknown
+ * @param {Object} [descriptor.digits] used for the formatting of floats
+ * @param {String} [value_if_empty=''] returned if the ``value`` argument is considered empty
+ */
+function format_value (value, descriptor, value_if_empty) {
+ var l10n = _t.database.parameters;
+ var date_format = time.strftime_to_moment_format(l10n.date_format);
+ var time_format = time.strftime_to_moment_format(l10n.time_format);
+ // If NaN value, display as with a `false` (empty cell)
+ if (typeof value === 'number' && isNaN(value)) {
+ value = false;
+ }
+ //noinspection FallthroughInSwitchStatementJS
+ switch (value) {
+ case '':
+ if (descriptor.type === 'char' || descriptor.type === 'text') {
+ return '';
+ }
+ console.warn('Field', descriptor, 'had an empty string as value, treating as false...');
+ return value_if_empty === undefined ? '' : value_if_empty;
+ case false:
+ case undefined:
+ case Infinity:
+ case -Infinity:
+ return value_if_empty === undefined ? '' : value_if_empty;
+ }
+ switch (descriptor.widget || descriptor.type || (descriptor.field && descriptor.field.type)) {
+ case 'id':
+ return value.toString();
+ case 'integer':
+ return utils.insert_thousand_seps(
+ _.str.sprintf('%d', value));
+ case 'float':
+ var digits = descriptor.digits ? descriptor.digits : [69,2];
+ digits = typeof digits === "string" ? py.eval(digits) : digits;
+ var precision = digits[1];
+ var formatted = _.str.sprintf('%.' + precision + 'f', value).split('.');
+ formatted[0] = utils.insert_thousand_seps(formatted[0]);
+ return formatted.join(l10n.decimal_point);
+ case 'float_time':
+ var pattern = '%02d:%02d';
+ if (value < 0) {
+ value = Math.abs(value);
+ pattern = '-' + pattern;
+ }
+ var hour = Math.floor(value);
+ var min = Math.round((value % 1) * 60);
+ if (min == 60){
+ min = 0;
+ hour = hour + 1;
+ }
+ return _.str.sprintf(pattern, hour, min);
+ case 'many2one':
+ // name_get value format
+ return value[1] ? value[1].split("\n")[0] : value[1];
+ case 'one2many':
+ case 'many2many':
+ if (typeof value === 'string') {
+ return value;
+ }
+ return _.str.sprintf(_t("(%d records)"), value.length);
+ case 'datetime':
+ if (typeof(value) == "string")
+ value = moment(time.auto_str_to_date(value));
+ else {
+ value = moment(value);
+ }
+ return value.format(date_format + ' ' + time_format);
+ case 'date':
+ if (typeof(value) == "string")
+ value = moment(time.auto_str_to_date(value));
+ else {
+ value = moment(value);
+ }
+ return value.format(date_format);
+ case 'time':
+ if (typeof(value) == "string")
+ value = moment(time.auto_str_to_date(value));
+ else {
+ value = moment(value);
+ }
+ return value.format(time_format);
+ case 'selection': case 'statusbar':
+ // Each choice is [value, label]
+ if(_.isArray(value)) {
+ return value[1];
+ }
+ var result = _(descriptor.selection).detect(function (choice) {
+ return choice[0] === value;
+ });
+ if (result) { return result[1]; }
+ return;
+ default:
+ return value;
+ }
+}
+
+function parse_value (value, descriptor, value_if_empty) {
+ var date_pattern = time.strftime_to_moment_format(_t.database.parameters.date_format),
+ time_pattern = time.strftime_to_moment_format(_t.database.parameters.time_format);
+ var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'),
+ time_pattern_wo_zero = time_pattern.replace('HH','H').replace('mm','m').replace('ss','s');
+ switch (value) {
+ case false:
+ case "":
+ return value_if_empty === undefined ? false : value_if_empty;
+ }
+ var tmp;
+ switch (descriptor.widget || descriptor.type || (descriptor.field && descriptor.field.type)) {
+ case 'integer':
+ do {
+ tmp = value;
+ value = value.replace(_t.database.parameters.thousands_sep, "");
+ } while(tmp !== value);
+ tmp = Number(value);
+ // do not accept not numbers or float values
+ if (isNaN(tmp) || tmp % 1)
+ throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
+ return tmp;
+ case 'float':
+ tmp = Number(value);
+ if (!isNaN(tmp))
+ return tmp;
+
+ var tmp2 = value;
+ do {
+ tmp = tmp2;
+ tmp2 = tmp.replace(_t.database.parameters.thousands_sep, "");
+ } while(tmp !== tmp2);
+ var reformatted_value = tmp.replace(_t.database.parameters.decimal_point, ".");
+ var parsed = Number(reformatted_value);
+ if (isNaN(parsed))
+ throw new Error(_.str.sprintf(_t("'%s' is not a correct float"), value));
+ return parsed;
+ case 'float_time':
+ var factor = 1;
+ if (value[0] === '-') {
+ value = value.slice(1);
+ factor = -1;
+ }
+ var float_time_pair = value.split(":");
+ if (float_time_pair.length != 2)
+ return factor * parse_value(value, {type: "float"});
+ var hours = parse_value(float_time_pair[0], {type: "integer"});
+ var minutes = parse_value(float_time_pair[1], {type: "integer"});
+ return factor * (hours + (minutes / 60));
+ case 'progressbar':
+ return parse_value(value, {type: "float"});
+ case 'datetime':
+ var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero], true);
+ if (datetime.isValid())
+ return time.datetime_to_str(datetime.toDate());
+ throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
+ case 'date':
+ var date = moment(value, [date_pattern, date_pattern_wo_zero], true);
+ if (date.isValid())
+ return time.date_to_str(date.toDate());
+ throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value));
+ case 'time':
+ var _time = moment(value, [time_pattern, time_pattern_wo_zero], true);
+ if (_time.isValid())
+ return time.time_to_str(_time.toDate());
+ throw new Error(_.str.sprintf(_t("'%s' is not a correct time"), value));
+ }
+ return value;
+}
+
+return {
+ format_value: format_value,
+ parse_value: parse_value,
+};
+
+});
diff --git a/addons/web/static/src/js/framework/misc.js b/addons/web/static/src/js/framework/misc.js
new file mode 100644
index 00000000000..a3aaadbc46c
--- /dev/null
+++ b/addons/web/static/src/js/framework/misc.js
@@ -0,0 +1,339 @@
+odoo.define('web.framework', ['web.core', 'web.crash_manager', 'web.session', 'web.utils', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var session = require('web.session');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var Spinner = window.Spinner;
+
+var messages_by_seconds = function() {
+ return [
+ [0, _t("Loading...")],
+ [20, _t("Still loading...")],
+ [60, _t("Still loading...<br />Please be patient.")],
+ [120, _t("Don't leave yet,<br />it's still loading...")],
+ [300, _t("You may not believe it,<br />but the application is actually loading...")],
+ [420, _t("Take a minute to get a coffee,<br />because it's loading...")],
+ [3600, _t("Maybe you should consider reloading the application by pressing F5...")]
+ ];
+};
+
+var Throbber = Widget.extend({
+ template: "Throbber",
+ start: function() {
+ var opts = {
+ lines: 13, // The number of lines to draw
+ length: 7, // The length of each line
+ width: 4, // The line thickness
+ radius: 10, // The radius of the inner circle
+ rotate: 0, // The rotation offset
+ color: '#FFF', // #rgb or #rrggbb
+ speed: 1, // Rounds per second
+ trail: 60, // Afterglow percentage
+ shadow: false, // Whether to render a shadow
+ hwaccel: false, // Whether to use hardware acceleration
+ className: 'spinner', // The CSS class to assign to the spinner
+ zIndex: 2e9, // The z-index (defaults to 2000000000)
+ top: 'auto', // Top position relative to parent in px
+ left: 'auto' // Left position relative to parent in px
+ };
+ this.spin = new Spinner(opts).spin(this.$el[0]);
+ this.start_time = new Date().getTime();
+ this.act_message();
+ },
+ act_message: function() {
+ var self = this;
+ setTimeout(function() {
+ if (self.isDestroyed())
+ return;
+ var seconds = (new Date().getTime() - self.start_time) / 1000;
+ var mes;
+ _.each(messages_by_seconds(), function(el) {
+ if (seconds >= el[0])
+ mes = el[1];
+ });
+ self.$(".oe_throbber_message").html(mes);
+ self.act_message();
+ }, 1000);
+ },
+ destroy: function() {
+ if (this.spin)
+ this.spin.stop();
+ this._super();
+ },
+});
+
+
+// special tweak for the web client
+var old_async_when = $.async_when;
+$.async_when = function() {
+ if (session.synch)
+ return $.when.apply(this, arguments);
+ else
+ return old_async_when.apply(this, arguments);
+};
+
+
+/** Setup blockui */
+if ($.blockUI) {
+ $.blockUI.defaults.baseZ = 1100;
+ $.blockUI.defaults.message = '<div class="openerp oe_blockui_spin_container" style="background-color: transparent;">';
+ $.blockUI.defaults.css.border = '0';
+ $.blockUI.defaults.css["background-color"] = '';
+}
+
+
+var throbbers = [];
+
+function blockUI () {
+ var tmp = $.blockUI.apply($, arguments);
+ var throbber = new Throbber();
+ throbbers.push(throbber);
+ throbber.appendTo($(".oe_blockui_spin_container"));
+ return tmp;
+}
+
+function unblockUI () {
+ _.invoke(throbbers, 'destroy');
+ throbbers = [];
+ return $.unblockUI.apply($, arguments);
+}
+
+/**
+ * Redirect to url by replacing window.location
+ * If wait is true, sleep 1s and wait for the server i.e. after a restart.
+ */
+function redirect (url, wait) {
+ // Dont display a dialog if some xmlhttprequest are in progress
+ crash_manager.disable();
+
+ var load = function() {
+ var old = "" + window.location;
+ var old_no_hash = old.split("#")[0];
+ var url_no_hash = url.split("#")[0];
+ location.assign(url);
+ if (old_no_hash === url_no_hash) {
+ location.reload(true);
+ }
+ };
+
+ var wait_server = function() {
+ session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
+ setTimeout(wait_server, 250);
+ });
+ };
+
+ if (wait) {
+ setTimeout(wait_server, 1000);
+ } else {
+ load();
+ }
+}
+
+// * Client action to reload the whole interface.
+// * If params.menu_id, it opens the given menu entry.
+// * If params.wait, reload will wait the openerp server to be reachable before reloading
+
+function Reload(parent, action) {
+ var params = action.params || {};
+ var menu_id = params.menu_id || false;
+ var l = window.location;
+
+ var sobj = $.deparam(l.search.substr(1));
+ if (params.url_search) {
+ sobj = _.extend(sobj, params.url_search);
+ }
+ var search = '?' + $.param(sobj);
+
+ var hash = l.hash;
+ if (menu_id) {
+ hash = "#menu_id=" + menu_id;
+ }
+ var url = l.protocol + "//" + l.host + l.pathname + search + hash;
+
+ redirect(url, params.wait);
+}
+
+core.action_registry.add("reload", Reload);
+
+
+/**
+ * Client action to go back home.
+ */
+function Home (parent, action) {
+ var url = '/' + (window.location.search || '');
+ redirect(url, action && action.params && action.params.wait);
+}
+core.action_registry.add("home", Home);
+
+/**
+ * Client action to go back in breadcrumb history.
+ * If can't go back in history stack, will go back to home.
+ */
+function HistoryBack (parent) {
+ parent.history_back().fail(function () {
+ Home(parent);
+ });
+}
+core.action_registry.add("history_back", HistoryBack);
+
+function login() {
+ redirect('/web/login');
+}
+core.action_registry.add("login", login);
+
+function logout() {
+ redirect('/web/session/logout');
+}
+core.action_registry.add("logout", logout);
+
+/**
+ * Client action to refresh the session context (making sure
+ * HTTP requests will have the right one) then reload the
+ * whole interface.
+ */
+function ReloadContext (parent, action) {
+ // side-effect of get_session_info is to refresh the session context
+ session.rpc("/web/session/get_session_info", {}).then(function() {
+ Reload(parent, action);
+ });
+}
+core.action_registry.add("reload_context", ReloadContext);
+
+// nvd3 customization
+//-------------------------------------------------------------------------
+nv.dev = false; // sets nvd3 library in production mode
+
+// monkey patch nvd3 to allow removing eventhandler on windowresize events
+// see https://github.com/novus/nvd3/pull/396 for more details
+
+// Adds a resize listener to the window.
+nv.utils.onWindowResize = function(fun) {
+ if (fun === null) return;
+ window.addEventListener('resize', fun);
+};
+
+// Backwards compatibility with current API.
+nv.utils.windowResize = nv.utils.onWindowResize;
+
+// Removes a resize listener from the window.
+nv.utils.offWindowResize = function(fun) {
+ if (fun === null) return;
+ window.removeEventListener('resize', fun);
+};
+
+// monkey patch nvd3 to prevent crashes when user changes view and nvd3 tries
+// to remove tooltips after 500 ms... seriously nvd3, what were you thinking?
+nv.tooltip.cleanup = function () {
+ $('.nvtooltip').remove();
+};
+
+// Bootstrap customization
+//-------------------------------------------------------------------------
+/* Bootstrap defaults overwrite */
+$.fn.tooltip.Constructor.DEFAULTS.placement = 'auto top';
+$.fn.tooltip.Constructor.DEFAULTS.html = true;
+$.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover focus click';
+$.fn.tooltip.Constructor.DEFAULTS.container = 'body';
+//overwrite bootstrap tooltip method to prevent showing 2 tooltip at the same time
+var bootstrap_show_function = $.fn.tooltip.Constructor.prototype.show;
+$.fn.modal.Constructor.prototype.enforceFocus = function () { };
+$.fn.tooltip.Constructor.prototype.show = function () {
+ $('.tooltip').remove();
+ //the following fix the bug when using placement
+ //auto and the parent element does not exist anymore resulting in
+ //an error. This should be remove once we updade bootstrap to a version that fix the bug
+ //edit: bug has been fixed here : https://github.com/twbs/bootstrap/pull/13752
+ var e = $.Event('show.bs.' + this.type);
+ var inDom = $.contains(document.documentElement, this.$element[0]);
+ if (e.isDefaultPrevented() || !inDom) return;
+ return bootstrap_show_function.call(this);
+};
+
+// jquery customization
+//-------------------------------------------------------------------------
+jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
+ return function( elem ) {
+ return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
+ };
+});
+
+/** Custom jQuery plugins */
+$.fn.getAttributes = function() {
+ var o = {};
+ if (this.length) {
+ for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) {
+ attr = attrs.item(i);
+ o[attr.nodeName] = attr.value;
+ }
+ }
+ return o;
+};
+$.fn.openerpClass = function(additionalClass) {
+ // This plugin should be applied on top level elements
+ additionalClass = additionalClass || '';
+ if (!!$.browser.msie) {
+ additionalClass += ' openerp_ie';
+ }
+ return this.each(function() {
+ $(this).addClass('openerp ' + additionalClass);
+ });
+};
+$.fn.openerpBounce = function() {
+ return this.each(function() {
+ $(this).css('box-sizing', 'content-box').effect('bounce', {distance: 18, times: 5}, 250);
+ });
+};
+
+// jquery autocomplete tweak to allow html and classnames
+var proto = $.ui.autocomplete.prototype,
+ initSource = proto._initSource;
+
+function filter( array, term ) {
+ var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
+ return $.grep( array, function(value_) {
+ return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
+ });
+}
+
+$.extend( proto, {
+ _initSource: function() {
+ if ( this.options.html && $.isArray(this.options.source) ) {
+ this.source = function( request, response ) {
+ response( filter( this.options.source, request.term ) );
+ };
+ } else {
+ initSource.call( this );
+ }
+ },
+
+ _renderItem: function( ul, item) {
+ return $( "<li></li>" )
+ .data( "item.autocomplete", item )
+ .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
+ .appendTo( ul )
+ .addClass(item.classname);
+ }
+});
+
+
+
+
+return {
+ blockUI: blockUI,
+ unblockUI: unblockUI,
+ redirect: redirect,
+};
+
+});
+
+odoo.define('web.session', ['web.Session'], function (require) {
+ var Session = require('web.Session');
+ var modules = odoo._modules;
+ return new Session(undefined, undefined, {modules:modules});
+});
+
diff --git a/addons/web/static/src/js/framework/mixins.js b/addons/web/static/src/js/framework/mixins.js
new file mode 100644
index 00000000000..102b75a9e4b
--- /dev/null
+++ b/addons/web/static/src/js/framework/mixins.js
@@ -0,0 +1,328 @@
+odoo.define('web.mixins', ['web.Class', 'web.utils'], function (require) {
+"use strict";
+
+var Class = require('web.Class');
+var utils = require('web.utils');
+
+/**
+ * Mixin to structure objects' life-cycles folowing a parent-children
+ * relationship. Each object can a have a parent and multiple children.
+ * When an object is destroyed, all its children are destroyed too releasing
+ * any resource they could have reserved before.
+ */
+var ParentedMixin = {
+ __parentedMixin : true,
+ init: function() {
+ this.__parentedDestroyed = false;
+ this.__parentedChildren = [];
+ this.__parentedParent = null;
+ },
+ /**
+ * Set the parent of the current object. When calling this method, the
+ * parent will also be informed and will return the current object
+ * when its getChildren() method is called. If the current object did
+ * already have a parent, it is unregistered before, which means the
+ * previous parent will not return the current object anymore when its
+ * getChildren() method is called.
+ */
+ setParent : function(parent) {
+ if (this.getParent()) {
+ if (this.getParent().__parentedMixin) {
+ this.getParent().__parentedChildren = _.without(this
+ .getParent().getChildren(), this);
+ }
+ }
+ this.__parentedParent = parent;
+ if (parent && parent.__parentedMixin) {
+ parent.__parentedChildren.push(this);
+ }
+ },
+ /**
+ * Return the current parent of the object (or null).
+ */
+ getParent : function() {
+ return this.__parentedParent;
+ },
+ /**
+ * Return a list of the children of the current object.
+ */
+ getChildren : function() {
+ return _.clone(this.__parentedChildren);
+ },
+ /**
+ * Returns true if destroy() was called on the current object.
+ */
+ isDestroyed : function() {
+ return this.__parentedDestroyed;
+ },
+ /**
+ Utility method to only execute asynchronous actions if the current
+ object has not been destroyed.
+
+ @param {$.Deferred} promise The promise representing the asynchronous
+ action.
+ @param {bool} [reject=false] If true, the returned promise will be
+ rejected with no arguments if the current
+ object is destroyed. If false, the
+ returned promise will never be resolved
+ or rejected.
+ @returns {$.Deferred} A promise that will mirror the given promise if
+ everything goes fine but will either be rejected
+ with no arguments or never resolved if the
+ current object is destroyed.
+ */
+ alive: function(promise, reject) {
+ var self = this;
+ return $.Deferred(function (def) {
+ promise.then(function () {
+ if (!self.isDestroyed()) {
+ def.resolve.apply(def, arguments);
+ }
+ }, function () {
+ if (!self.isDestroyed()) {
+ def.reject.apply(def, arguments);
+ }
+ }).always(function () {
+ if (reject) {
+ // noop if def already resolved or rejected
+ def.reject();
+ }
+ // otherwise leave promise in limbo
+ });
+ }).promise();
+ },
+ /**
+ * Inform the object it should destroy itself, releasing any
+ * resource it could have reserved.
+ */
+ destroy : function() {
+ _.each(this.getChildren(), function(el) {
+ el.destroy();
+ });
+ this.setParent(undefined);
+ this.__parentedDestroyed = true;
+ },
+ /**
+ * Find the closest ancestor matching predicate
+ */
+ findAncestor: function (predicate) {
+ var ancestor = this;
+ while (ancestor && !(predicate(ancestor)) && ancestor.getParent) {
+ ancestor = ancestor.getParent();
+ }
+ return ancestor;
+ },
+};
+
+/**
+ * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
+ *
+ * This class just handle the dispatching of events, it is not meant to be extended,
+ * nor used directly. All integration with parenting and automatic unregistration of
+ * events is done in EventDispatcherMixin.
+ *
+ * Copyright notice for the following Class:
+ *
+ * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
+ * Backbone may be freely distributed under the MIT license.
+ * For all details and documentation:
+ * http://backbonejs.org
+ *
+ */
+var Events = Class.extend({
+ on : function(events, callback, context) {
+ var ev;
+ events = events.split(/\s+/);
+ var calls = this._callbacks || (this._callbacks = {});
+ while ((ev = events.shift())) {
+ var list = calls[ev] || (calls[ev] = {});
+ var tail = list.tail || (list.tail = list.next = {});
+ tail.callback = callback;
+ tail.context = context;
+ list.tail = tail.next = {};
+ }
+ return this;
+ },
+
+ off : function(events, callback, context) {
+ var ev, calls, node;
+ if (!events) {
+ delete this._callbacks;
+ } else if ((calls = this._callbacks)) {
+ events = events.split(/\s+/);
+ while ((ev = events.shift())) {
+ node = calls[ev];
+ delete calls[ev];
+ if (!callback || !node)
+ continue;
+ while ((node = node.next) && node.next) {
+ if (node.callback === callback
+ && (!context || node.context === context))
+ continue;
+ this.on(ev, node.callback, node.context);
+ }
+ }
+ }
+ return this;
+ },
+
+ callbackList: function() {
+ var lst = [];
+ _.each(this._callbacks || {}, function(el, eventName) {
+ var node = el;
+ while ((node = node.next) && node.next) {
+ lst.push([eventName, node.callback, node.context]);
+ }
+ });
+ return lst;
+ },
+
+ trigger : function(events) {
+ var event, node, calls, tail, args, all, rest;
+ if (!(calls = this._callbacks))
+ return this;
+ all = calls.all;
+ (events = events.split(/\s+/)).push(null);
+ // Save references to the current heads & tails.
+ while ((event = events.shift())) {
+ if (all)
+ events.push({
+ next : all.next,
+ tail : all.tail,
+ event : event
+ });
+ if (!(node = calls[event]))
+ continue;
+ events.push({
+ next : node.next,
+ tail : node.tail
+ });
+ }
+ rest = Array.prototype.slice.call(arguments, 1);
+ while ((node = events.pop())) {
+ tail = node.tail;
+ args = node.event ? [ node.event ].concat(rest) : rest;
+ while ((node = node.next) !== tail) {
+ node.callback.apply(node.context || this, args);
+ }
+ }
+ return this;
+ }
+});
+
+/**
+ Mixin containing an event system. Events are also registered by specifying the target object
+ (the object which will receive the event when it is raised). Both the event-emitting object
+ and the target object store or reference to each other. This is used to correctly remove all
+ reference to the event handler when any of the object is destroyed (when the destroy() method
+ from ParentedMixin is called). Removing those references is necessary to avoid memory leak
+ and phantom events (events which are raised and sent to a previously destroyed object).
+*/
+var EventDispatcherMixin = _.extend({}, ParentedMixin, {
+ __eventDispatcherMixin: true,
+ init: function() {
+ ParentedMixin.init.call(this);
+ this.__edispatcherEvents = new Events();
+ this.__edispatcherRegisteredEvents = [];
+ },
+ on: function(events, dest, func) {
+ var self = this;
+ if (typeof func !== "function") {
+ throw new Error("Event handler must be a function.");
+ }
+ events = events.split(/\s+/);
+ _.each(events, function(eventName) {
+ self.__edispatcherEvents.on(eventName, func, dest);
+ if (dest && dest.__eventDispatcherMixin) {
+ dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
+ }
+ });
+ return this;
+ },
+ off: function(events, dest, func) {
+ var self = this;
+ events = events.split(/\s+/);
+ _.each(events, function(eventName) {
+ self.__edispatcherEvents.off(eventName, func, dest);
+ if (dest && dest.__eventDispatcherMixin) {
+ dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
+ return !(el.name === eventName && el.func === func && el.source === self);
+ });
+ }
+ });
+ return this;
+ },
+ trigger: function() {
+ this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
+ return this;
+ },
+ destroy: function() {
+ var self = this;
+ _.each(this.__edispatcherRegisteredEvents, function(event) {
+ event.source.__edispatcherEvents.off(event.name, event.func, self);
+ });
+ this.__edispatcherRegisteredEvents = [];
+ _.each(this.__edispatcherEvents.callbackList(), function(cal) {
+ this.off(cal[0], cal[2], cal[1]);
+ }, this);
+ this.__edispatcherEvents.off();
+ ParentedMixin.destroy.call(this);
+ }
+});
+
+var PropertiesMixin = _.extend({}, EventDispatcherMixin, {
+ init: function() {
+ EventDispatcherMixin.init.call(this);
+ this.__getterSetterInternalMap = {};
+ },
+ set: function(arg1, arg2, arg3) {
+ var map;
+ var options;
+ if (typeof arg1 === "string") {
+ map = {};
+ map[arg1] = arg2;
+ options = arg3 || {};
+ } else {
+ map = arg1;
+ options = arg2 || {};
+ }
+ var self = this;
+ var changed = false;
+ _.each(map, function(val, key) {
+ var tmp = self.__getterSetterInternalMap[key];
+ if (tmp === val)
+ return;
+ // seriously, why are you doing this? it is obviously a stupid design.
+ // the properties mixin should not be concerned with handling fields details.
+ // this also has the side effect of introducing a dependency on utils. Todo:
+ // remove this, or move it elsewhere. Also, learn OO programming.
+ if (key === 'value' && self.field && self.field.type === 'float' && tmp && val){
+ var precision = self.field.digits ? self.field.digits[1] : 2;
+ if (utils.float_is_zero(tmp - val, precision)) {
+ return;
+ }
+ }
+ changed = true;
+ self.__getterSetterInternalMap[key] = val;
+ if (! options.silent)
+ self.trigger("change:" + key, self, {
+ oldValue: tmp,
+ newValue: val
+ });
+ });
+ if (changed)
+ self.trigger("change", self);
+ },
+ get: function(key) {
+ return this.__getterSetterInternalMap[key];
+ }
+});
+
+return {
+ ParentedMixin: ParentedMixin,
+ EventDispatcherMixin: EventDispatcherMixin,
+ PropertiesMixin: PropertiesMixin,
+};
+
+});
+
diff --git a/addons/web/static/src/js/framework/model.js b/addons/web/static/src/js/framework/model.js
new file mode 100644
index 00000000000..f1a1555fd6f
--- /dev/null
+++ b/addons/web/static/src/js/framework/model.js
@@ -0,0 +1,191 @@
+odoo.define('web.Model', ['web.Class', 'web.session', 'web.utils'], function (require) {
+"use strict";
+
+var Class = require('web.Class');
+var session = require('web.session');
+var utils = require('web.utils');
+
+var Model = Class.extend({
+ /**
+ new openerp.web.Model([session,] model_name[, context[, domain]])
+
+ @constructs instance.web.Model
+ @extends instance.web.Class
+
+ @param {openerp.web.Session} [session] The session object used to communicate with
+ the server.
+ @param {String} model_name name of the OpenERP model this object is bound to
+ @param {Object} [context]
+ @param {Array} [domain]
+ */
+ init: function(name, context, domain) {
+ this.name = name;
+ this._context = context || {};
+ this._domain = domain || [];
+ },
+ /**
+ * @deprecated does not allow to specify kwargs, directly use call() instead
+ */
+ get_func: function (method_name) {
+ var self = this;
+ return function () {
+ return self.call(method_name, _.toArray(arguments));
+ };
+ },
+ /**
+ * Call a method (over RPC) on the bound OpenERP model.
+ *
+ * @param {String} method name of the method to call
+ * @param {Array} [args] positional arguments
+ * @param {Object} [kwargs] keyword arguments
+ * @param {Object} [options] additional options for the rpc() method
+ * @returns {jQuery.Deferred<>} call result
+ */
+ call: function (method, args, kwargs, options) {
+ args = args || [];
+ kwargs = kwargs || {};
+ if (!_.isArray(args)) {
+ // call(method, kwargs)
+ kwargs = args;
+ args = [];
+ }
+ var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
+ return session.rpc(call_kw, {
+ model: this.name,
+ method: method,
+ args: args,
+ kwargs: kwargs
+ }, options);
+ },
+ /**
+ * Executes a signal on the designated workflow, on the bound OpenERP model
+ *
+ * @param {Number} id workflow identifier
+ * @param {String} signal signal to trigger on the workflow
+ */
+ exec_workflow: function (id, signal) {
+ return session.rpc('/web/dataset/exec_workflow', {
+ model: this.name,
+ id: id,
+ signal: signal
+ });
+ },
+ /**
+ * Performs a fields_view_get and apply postprocessing.
+ * return a {$.Deferred} resolved with the fvg
+ *
+ * @param {Object} args
+ * @param {String|Object} args.model instance.web.Model instance or string repr of the model
+ * @param {Object} [args.context] context if args.model is a string
+ * @param {Number} [args.view_id] id of the view to be loaded, default view if null
+ * @param {String} [args.view_type] type of view to be loaded if view_id is null
+ * @param {Boolean} [args.toolbar=false] get the toolbar definition
+ */
+ fields_view_get: function (options) {
+
+ return this.call('fields_view_get', {
+ view_id: options.view_id,
+ view_type: options.view_type,
+ context: options.context,
+ toolbar: options.toolbar || false
+ }).then(function postprocess(fvg) {
+ var doc = $.parseXML(fvg.arch).documentElement;
+ fvg.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
+ if ('id' in fvg.fields) {
+ // Special case for id's
+ var id_field = fvg.fields.id;
+ id_field.original_type = id_field.type;
+ id_field.type = 'id';
+ }
+ _.each(fvg.fields, function(field) {
+ _.each(field.views || {}, function(view) {
+ postprocess(view);
+ });
+ });
+ return fvg;
+ });
+ }
+});
+
+return Model;
+});
+
+odoo.define('web.DataModel', ['web.data', 'web.Model', 'web.pyeval', 'web.session'], function (require) {
+"use strict";
+
+var data = require('web.data');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+
+Model.include({
+ /**
+ * Fetches a Query instance bound to this model, for searching
+ *
+ * @param {Array<String>} [fields] fields to ultimately fetch during the search
+ * @returns {instance.web.Query}
+ */
+ query: function (fields) {
+ return new data.Query(this, fields);
+ },
+ /**
+ * Fetches the model's domain, combined with the provided domain if any
+ *
+ * @param {Array} [domain] to combine with the model's internal domain
+ * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
+ */
+ domain: function (domain) {
+ if (!domain) { return this._domain; }
+ return new data.CompoundDomain(this._domain, domain);
+ },
+ /**
+ * Fetches the combination of the user's context and the domain context,
+ * combined with the provided context if any
+ *
+ * @param {Object} [context] to combine with the model's internal context
+ * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
+ */
+ context: function (context) {
+ return new data.CompoundContext(session.user_context, this._context, context || {});
+ },
+ /**
+ * Call a method (over RPC) on the bound OpenERP model.
+ *
+ * @param {String} method name of the method to call
+ * @param {Array} [args] positional arguments
+ * @param {Object} [kwargs] keyword arguments
+ * @param {Object} [options] additional options for the rpc() method
+ * @returns {jQuery.Deferred<>} call result
+ */
+ call: function (method, args, kwargs, options) {
+ args = args || [];
+ kwargs = kwargs || {};
+ if (!_.isArray(args)) {
+ // call(method, kwargs)
+ kwargs = args;
+ args = [];
+ }
+ pyeval.ensure_evaluated(args, kwargs);
+ var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
+ return session.rpc(call_kw, {
+ model: this.name,
+ method: method,
+ args: args,
+ kwargs: kwargs
+ }, options);
+ },
+ call_button: function (method, args) {
+ pyeval.ensure_evaluated(args, {});
+ return session.rpc('/web/dataset/call_button', {
+ model: this.name,
+ method: method,
+ // Should not be necessary anymore. Integrate remote in this?
+ domain_id: null,
+ context_id: args.length - 1,
+ args: args || []
+ });
+ },
+
+});
+
+});
diff --git a/addons/web/static/src/js/framework/pyeval.js b/addons/web/static/src/js/framework/pyeval.js
new file mode 100644
index 00000000000..18d43db318b
--- /dev/null
+++ b/addons/web/static/src/js/framework/pyeval.js
@@ -0,0 +1,1018 @@
+odoo.define('web.pyeval', ['web.core', 'web.session', 'web.utils'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var session = require('web.session');
+var utils = require('web.utils');
+
+var _t = core._t;
+var py = window.py; // to silence linters
+/*
+ * py.js helpers and setup
+ */
+
+var obj = function () {};
+obj.prototype = py.object;
+var asJS = function (arg) {
+ if (arg instanceof obj) {
+ return arg.toJSON();
+ }
+ return arg;
+};
+
+var datetime = py.PY_call(py.object);
+
+var zero = py.float.fromJSON(0);
+
+// Port from pypy/lib_pypy/datetime.py
+var DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+var DAYS_BEFORE_MONTH = [null];
+var dbm = 0;
+
+for (var i=1; i<DAYS_IN_MONTH.length; ++i) {
+ DAYS_BEFORE_MONTH.push(dbm);
+ dbm += DAYS_IN_MONTH[i];
+}
+
+function is_leap (year) {
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
+}
+
+function days_before_year (year) {
+ var y = year - 1;
+ return y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400);
+}
+
+function days_in_month (year, month) {
+ if (month === 2 && is_leap(year)) {
+ return 29;
+ }
+ return DAYS_IN_MONTH[month];
+}
+
+function days_before_month (year, month) {
+ var post_leap_feb = month > 2 && is_leap(year);
+ return DAYS_BEFORE_MONTH[month] + (post_leap_feb ? 1 : 0);
+}
+
+function ymd2ord (year, month, day) {
+ var dim = days_in_month(year, month);
+ if (!(1 <= day && day <= dim)) {
+ throw new Error("ValueError: day must be in 1.." + dim);
+ }
+ return days_before_year(year) +
+ days_before_month(year, month) +
+ day;
+}
+
+var DI400Y = days_before_year(401);
+var DI100Y = days_before_year(101);
+var DI4Y = days_before_year(5);
+
+function ord2ymd (n) {
+ --n;
+ var n400, n100, n4, n1, n0;
+ utils.divmod(n, DI400Y, function (_n400, n) {
+ n400 = _n400;
+ utils.divmod(n, DI100Y, function (_n100, n) {
+ n100 = _n100;
+ utils.divmod(n, DI4Y, function (_n4, n) {
+ n4 = _n4;
+ utils.divmod(n, 365, function (_n1, n) {
+ n1 = _n1;
+ n0 = n;
+ });
+ });
+ });
+ });
+
+ n = n0;
+ var year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;
+ if (n1 == 4 || n100 == 100) {
+ utils.assert(n0 === 0);
+ return {
+ year: year - 1,
+ month: 12,
+ day: 31
+ };
+ }
+
+ var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);
+ utils.assert(leapyear == is_leap(year));
+ var month = (n + 50) >> 5;
+ var preceding = DAYS_BEFORE_MONTH[month] + ((month > 2 && leapyear) ? 1 : 0);
+ if (preceding > n) {
+ --month;
+ preceding -= DAYS_IN_MONTH[month] + ((month === 2 && leapyear) ? 1 : 0);
+ }
+ n -= preceding;
+ return {
+ year: year,
+ month: month,
+ day: n+1
+ };
+}
+
+/**
+ * Converts the stuff passed in into a valid date, applying overflows as needed
+ */
+function tmxxx(year, month, day, hour, minute, second, microsecond) {
+ hour = hour || 0; minute = minute || 0; second = second || 0;
+ microsecond = microsecond || 0;
+
+ if (microsecond < 0 || microsecond > 999999) {
+ utils.divmod(microsecond, 1000000, function (carry, ms) {
+ microsecond = ms;
+ second += carry;
+ });
+ }
+ if (second < 0 || second > 59) {
+ utils.divmod(second, 60, function (carry, s) {
+ second = s;
+ minute += carry;
+ });
+ }
+ if (minute < 0 || minute > 59) {
+ utils.divmod(minute, 60, function (carry, m) {
+ minute = m;
+ hour += carry;
+ });
+ }
+ if (hour < 0 || hour > 23) {
+ utils.divmod(hour, 24, function (carry, h) {
+ hour = h;
+ day += carry;
+ });
+ }
+ // That was easy. Now it gets muddy: the proper range for day
+ // can't be determined without knowing the correct month and year,
+ // but if day is, e.g., plus or minus a million, the current month
+ // and year values make no sense (and may also be out of bounds
+ // themselves).
+ // Saying 12 months == 1 year should be non-controversial.
+ if (month < 1 || month > 12) {
+ utils.divmod(month-1, 12, function (carry, m) {
+ month = m + 1;
+ year += carry;
+ });
+ }
+ // Now only day can be out of bounds (year may also be out of bounds
+ // for a datetime object, but we don't care about that here).
+ // If day is out of bounds, what to do is arguable, but at least the
+ // method here is principled and explainable.
+ var dim = days_in_month(year, month);
+ if (day < 1 || day > dim) {
+ // Move day-1 days from the first of the month. First try to
+ // get off cheap if we're only one day out of range (adjustments
+ // for timezone alone can't be worse than that).
+ if (day === 0) {
+ --month;
+ if (month > 0) {
+ day = days_in_month(year, month);
+ } else {
+ --year; month=12; day=31;
+ }
+ } else if (day == dim + 1) {
+ ++month;
+ day = 1;
+ if (month > 12) {
+ month = 1;
+ ++year;
+ }
+ } else {
+ var r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));
+ year = r.year;
+ month = r.month;
+ day = r.day;
+ }
+ }
+ return {
+ year: year,
+ month: month,
+ day: day,
+ hour: hour,
+ minute: minute,
+ second: second,
+ microsecond: microsecond
+ };
+}
+
+datetime.timedelta = py.type('timedelta', null, {
+ __init__: function () {
+ var args = py.PY_parseArgs(arguments, [
+ ['days', zero], ['seconds', zero], ['microseconds', zero],
+ ['milliseconds', zero], ['minutes', zero], ['hours', zero],
+ ['weeks', zero]
+ ]);
+
+ var d = 0, s = 0, m = 0;
+ var days = args.days.toJSON() + args.weeks.toJSON() * 7;
+ var seconds = args.seconds.toJSON()
+ + args.minutes.toJSON() * 60
+ + args.hours.toJSON() * 3600;
+ var microseconds = args.microseconds.toJSON()
+ + args.milliseconds.toJSON() * 1000;
+
+ // Get rid of all fractions, and normalize s and us.
+ // Take a deep breath <wink>.
+ var daysecondsfrac = utils.modf(days, function (dayfrac, days) {
+ d = days;
+ if (dayfrac) {
+ return utils.modf(dayfrac * 24 * 3600, function (dsf, dsw) {
+ s = dsw;
+ return dsf;
+ });
+ }
+ return 0;
+ });
+
+ var secondsfrac = utils.modf(seconds, function (sf, s) {
+ seconds = s;
+ return sf + daysecondsfrac;
+ });
+ utils.divmod(seconds, 24*3600, function (days, seconds) {
+ d += days;
+ s += seconds;
+ });
+ // seconds isn't referenced again before redefinition
+
+ microseconds += secondsfrac * 1e6;
+ utils.divmod(microseconds, 1000000, function (seconds, microseconds) {
+ utils.divmod(seconds, 24*3600, function (days, seconds) {
+ d += days;
+ s += seconds;
+ m += Math.round(microseconds);
+ });
+ });
+
+ // Carrying still possible here?
+
+ this.days = d;
+ this.seconds = s;
+ this.microseconds = m;
+ },
+ __str__: function () {
+ var hh, mm, ss;
+ utils.divmod(this.seconds, 60, function (m, s) {
+ utils.divmod(m, 60, function (h, m) {
+ hh = h;
+ mm = m;
+ ss = s;
+ });
+ });
+ var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss);
+ if (this.days) {
+ s = _.str.sprintf("%d day%s, %s",
+ this.days,
+ (this.days != 1 && this.days != -1) ? 's' : '',
+ s);
+ }
+ if (this.microseconds) {
+ s = _.str.sprintf("%s.%06d", s, this.microseconds);
+ }
+ return py.str.fromJSON(s);
+ },
+ __eq__: function (other) {
+ if (!py.PY_isInstance(other, datetime.timedelta)) {
+ return py.False;
+ }
+
+ return (this.days === other.days
+ && this.seconds === other.seconds
+ && this.microseconds === other.microseconds)
+ ? py.True : py.False;
+ },
+ __add__: function (other) {
+ if (!py.PY_isInstance(other, datetime.timedelta)) {
+ return py.NotImplemented;
+ }
+ return py.PY_call(datetime.timedelta, [
+ py.float.fromJSON(this.days + other.days),
+ py.float.fromJSON(this.seconds + other.seconds),
+ py.float.fromJSON(this.microseconds + other.microseconds)
+ ]);
+ },
+ __radd__: function (other) { return this.__add__(other); },
+ __sub__: function (other) {
+ if (!py.PY_isInstance(other, datetime.timedelta)) {
+ return py.NotImplemented;
+ }
+ return py.PY_call(datetime.timedelta, [
+ py.float.fromJSON(this.days - other.days),
+ py.float.fromJSON(this.seconds - other.seconds),
+ py.float.fromJSON(this.microseconds - other.microseconds)
+ ]);
+ },
+ __rsub__: function (other) {
+ if (!py.PY_isInstance(other, datetime.timedelta)) {
+ return py.NotImplemented;
+ }
+ return this.__neg__().__add__(other);
+ },
+ __neg__: function () {
+ return py.PY_call(datetime.timedelta, [
+ py.float.fromJSON(-this.days),
+ py.float.fromJSON(-this.seconds),
+ py.float.fromJSON(-this.microseconds)
+ ]);
+ },
+ __pos__: function () { return this; },
+ __mul__: function (other) {
+ if (!py.PY_isInstance(other, py.float)) {
+ return py.NotImplemented;
+ }
+ var n = other.toJSON();
+ return py.PY_call(datetime.timedelta, [
+ py.float.fromJSON(this.days * n),
+ py.float.fromJSON(this.seconds * n),
+ py.float.fromJSON(this.microseconds * n)
+ ]);
+ },
+ __rmul__: function (other) { return this.__mul__(other); },
+ __div__: function (other) {
+ if (!py.PY_isInstance(other, py.float)) {
+ return py.NotImplemented;
+ }
+ var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000
+ + this.microseconds;
+ return py.PY_call(
+ datetime.timedelta, [
+ zero, zero, py.float.fromJSON(usec / other.toJSON())]);
+ },
+ __floordiv__: function (other) { return this.__div__(other); },
+ total_seconds: function () {
+ return py.float.fromJSON(
+ this.days * 86400
+ + this.seconds
+ + this.microseconds / 1000000);
+ },
+ __nonzero__: function () {
+ return (!!this.days || !!this.seconds || !!this.microseconds)
+ ? py.True
+ : py.False;
+ }
+});
+
+datetime.datetime = py.type('datetime', null, {
+ __init__: function () {
+ var zero = py.float.fromJSON(0);
+ var args = py.PY_parseArgs(arguments, [
+ 'year', 'month', 'day',
+ ['hour', zero], ['minute', zero], ['second', zero],
+ ['microsecond', zero], ['tzinfo', py.None]
+ ]);
+ for(var key in args) {
+ if (!args.hasOwnProperty(key)) { continue; }
+ this[key] = asJS(args[key]);
+ }
+ },
+ replace: function () {
+ var args = py.PY_parseArgs(arguments, [
+ ['year', py.None], ['month', py.None], ['day', py.None],
+ ['hour', py.None], ['minute', py.None], ['second', py.None],
+ ['microsecond', py.None] // FIXME: tzinfo, can't use None as valid input
+ ]);
+ var params = {};
+ for(var key in args) {
+ if (!args.hasOwnProperty(key)) { continue; }
+
+ var arg = args[key];
+ params[key] = (arg === py.None ? this[key] : asJS(arg));
+ }
+ return py.PY_call(datetime.datetime, params);
+ },
+ strftime: function () {
+ var self = this;
+ var args = py.PY_parseArgs(arguments, 'format');
+ return py.str.fromJSON(args.format.toJSON()
+ .replace(/%([A-Za-z])/g, function (m, c) {
+ switch (c) {
+ case 'Y': return _.str.sprintf('%04d', self.year);
+ case 'm': return _.str.sprintf('%02d', self.month);
+ case 'd': return _.str.sprintf('%02d', self.day);
+ case 'H': return _.str.sprintf('%02d', self.hour);
+ case 'M': return _.str.sprintf('%02d', self.minute);
+ case 'S': return _.str.sprintf('%02d', self.second);
+ }
+ throw new Error('ValueError: No known conversion for ' + m);
+ }));
+ },
+ now: py.classmethod.fromJSON(function () {
+ var d = new Date();
+ return py.PY_call(datetime.datetime, [
+ d.getFullYear(), d.getMonth() + 1, d.getDate(),
+ d.getHours(), d.getMinutes(), d.getSeconds(),
+ d.getMilliseconds() * 1000]);
+ }),
+ today: py.classmethod.fromJSON(function () {
+ var dt_class = py.PY_getAttr(datetime, 'datetime');
+ return py.PY_call(py.PY_getAttr(dt_class, 'now'));
+ }),
+ utcnow: py.classmethod.fromJSON(function () {
+ var d = new Date();
+ return py.PY_call(datetime.datetime,
+ [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
+ d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
+ d.getUTCMilliseconds() * 1000]);
+ }),
+ combine: py.classmethod.fromJSON(function () {
+ var args = py.PY_parseArgs(arguments, 'date time');
+ return py.PY_call(datetime.datetime, [
+ py.PY_getAttr(args.date, 'year'),
+ py.PY_getAttr(args.date, 'month'),
+ py.PY_getAttr(args.date, 'day'),
+ py.PY_getAttr(args.time, 'hour'),
+ py.PY_getAttr(args.time, 'minute'),
+ py.PY_getAttr(args.time, 'second')
+ ]);
+ }),
+ toJSON: function () {
+ return new Date(
+ this.year,
+ this.month - 1,
+ this.day,
+ this.hour,
+ this.minute,
+ this.second,
+ this.microsecond / 1000);
+ },
+});
+
+datetime.date = py.type('date', null, {
+ __init__: function () {
+ var args = py.PY_parseArgs(arguments, 'year month day');
+ this.year = asJS(args.year);
+ this.month = asJS(args.month);
+ this.day = asJS(args.day);
+ },
+ strftime: function () {
+ var self = this;
+ var args = py.PY_parseArgs(arguments, 'format');
+ return py.str.fromJSON(args.format.toJSON()
+ .replace(/%([A-Za-z])/g, function (m, c) {
+ switch (c) {
+ case 'Y': return self.year;
+ case 'm': return _.str.sprintf('%02d', self.month);
+ case 'd': return _.str.sprintf('%02d', self.day);
+ }
+ throw new Error('ValueError: No known conversion for ' + m);
+ }));
+ },
+ __eq__: function (other) {
+ return (this.year === other.year
+ && this.month === other.month
+ && this.day === other.day)
+ ? py.True : py.False;
+ },
+ replace: function () {
+ var args = py.PY_parseArgs(arguments, [
+ ['year', py.None], ['month', py.None], ['day', py.None]
+ ]);
+ var params = {};
+ for(var key in args) {
+ if (!args.hasOwnProperty(key)) { continue; }
+
+ var arg = args[key];
+ params[key] = (arg === py.None ? this[key] : asJS(arg));
+ }
+ return py.PY_call(datetime.date, params);
+ },
+ __add__: function (other) {
+ if (!py.PY_isInstance(other, datetime.timedelta)) {
+ return py.NotImplemented;
+ }
+ var s = tmxxx(this.year, this.month, this.day + other.days);
+ return datetime.date.fromJSON(s.year, s.month, s.day);
+ },
+ __radd__: function (other) { return this.__add__(other); },
+ __sub__: function (other) {
+ if (py.PY_isInstance(other, datetime.timedelta)) {
+ return py.PY_add(this, py.PY_negative(other));
+ }
+ if (py.PY_isInstance(other, datetime.date)) {
+ // FIXME: getattr and sub API methods
+ return py.PY_call(datetime.timedelta, [
+ py.PY_subtract(
+ py.PY_call(py.PY_getAttr(this, 'toordinal')),
+ py.PY_call(py.PY_getAttr(other, 'toordinal')))
+ ]);
+ }
+ return py.NotImplemented;
+ },
+ toordinal: function () {
+ return py.float.fromJSON(ymd2ord(this.year, this.month, this.day));
+ },
+ weekday: function () {
+ return py.float.fromJSON((this.toordinal().toJSON()+6)%7);
+ },
+ fromJSON: function (year, month, day) {
+ return py.PY_call(datetime.date, [year, month, day]);
+ },
+ today: py.classmethod.fromJSON(function () {
+ var d = new Date ();
+ return py.PY_call(datetime.date, [
+ d.getFullYear(), d.getMonth() + 1, d.getDate()]);
+ }),
+});
+/**
+ Returns the current local date, which means the date on the client (which can be different
+ compared to the date of the server).
+
+ @return {datetime.date}
+*/
+function context_today () {
+ var d = new Date();
+ return py.PY_call(
+ datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
+}
+
+datetime.time = py.type('time', null, {
+ __init__: function () {
+ var zero = py.float.fromJSON(0);
+ var args = py.PY_parseArgs(arguments, [
+ ['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero],
+ ['tzinfo', py.None]
+ ]);
+
+ for(var k in args) {
+ if (!args.hasOwnProperty(k)) { continue; }
+ this[k] = asJS(args[k]);
+ }
+ }
+});
+
+var time = py.PY_call(py.object);
+time.strftime = py.PY_def.fromJSON(function () {
+ var args = py.PY_parseArgs(arguments, 'format');
+ var dt_class = py.PY_getAttr(datetime, 'datetime');
+ var d = py.PY_call(py.PY_getAttr(dt_class, 'utcnow'));
+ return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
+});
+
+var args = _.map(('year month day '
+ + 'years months weeks days '
+ + 'weekday leapdays yearday nlyearday').split(' '), function (arg) {
+ switch (arg) {
+ case 'years':case 'months':case 'days':case 'leapdays':case 'weeks':
+ return [arg, zero];
+ case 'year':case 'month':case 'day':case 'weekday':
+ case 'yearday':case 'nlyearday':
+ return [arg, null];
+ default:
+ throw new Error("Unknown relativedelta argument " + arg);
+ }
+});
+args.unshift('*');
+
+var _utils = {
+ monthrange: function (year, month) {
+ if (month < 1 || month > 12) {
+ throw new Error("Illegal month " + month);
+ }
+
+ var day1 = this.weekday(year, month, 1);
+ var ndays = this.mdays[month] + (month == this.February && this.isleap(year));
+ return [day1, ndays];
+ },
+ weekday: function (year, month, day) {
+ var date = py.PY_call(datetime.date, [year, month, day]);
+ return py.PY_call(py.PY_getAttr(date, 'weekday'));
+ },
+ isleap: function (year) {
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
+ },
+ mdays: [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+ January: 1,
+ February: 2
+};
+
+var relativedelta = py.type('relativedelta', null, {
+ __init__: function () {
+ this.ops = py.PY_parseArgs(arguments, args);
+ this.ops.days = py.float.fromJSON(
+ asJS(this.ops.days) + asJS(this.ops.weeks) * 7
+ );
+
+ var yday = zero;
+ if (this.ops.nlyearday) {
+ yday = this.ops.nlyearday;
+ } else if (this.ops.yearday) {
+ yday = this.ops.yearday;
+ if (asJS(this.ops.yearday) > 59) {
+ this.ops.leapdays = py.float.fromJS(-1);
+ }
+ }
+ if (py.PY_isTrue(yday)) {
+ var ydayidx = [31, 59, 90, 120, 151, 181, 212,
+ 243, 273, 304, 334, 366];
+ for(var idx=0; idx<ydayidx.length; ++idx) {
+ var ydays = ydayidx[idx];
+ if (asJS(yday) <= ydays) {
+ this.ops.month = py.float.fromJSON(idx+1);
+ if (!idx) {
+ this.ops.day = yday;
+ } else {
+ this.ops.day = py.PY_subtract(
+ yday,
+ py.float.fromJSON(ydayidx[idx-1])
+ );
+ }
+ break;
+ }
+ }
+ if (idx === ydayidx.length) {
+ throw new Error("Invalid year day (" + asJS(yday) + ")");
+ }
+ }
+ this._fix();
+ },
+ _fix: function () {
+ var months = asJS(this.ops.months);
+ if (Math.abs(months) > 11) {
+ var s = months > 0 ? 1 : -1;
+ var r = utils.divmod(months * s, 12);
+ this.ops.months = py.float.fromJSON(r.mod*s);
+ this.ops.years = py.float.fromJSON(
+ asJS(this.ops.years) + r.div*s);
+ }
+ this._has_time = 0;
+ },
+ __add__: function (other) {
+ if (!py.PY_isInstance(other, datetime.date)) {
+ return py.NotImplemented;
+ }
+ // TODO: test this whole mess
+ var year = (asJS(this.ops.year) || asJS(other.year)) + asJS(this.ops.years);
+ var month = asJS(this.ops.month) || asJS(other.month);
+ var months;
+ if (months = asJS(this.ops.months)) {
+ if (Math.abs(months) < 1 || Math.abs(months) > 12) {
+ throw new Error("Can only use relative months between -12 and +12");
+ }
+ month += months;
+ if (month > 12) {
+ year += 1;
+ month -= 12;
+ }
+ if (month < 1) {
+ year -= 1;
+ month += 12;
+ }
+ }
+
+ var day = Math.min(_utils.monthrange(year, month)[1],
+ asJS(this.ops.day) || asJS(other.day));
+
+ var repl = {
+ year: py.float.fromJSON(year),
+ month: py.float.fromJSON(month),
+ day: py.float.fromJSON(day)
+ };
+
+ var days = asJS(this.ops.days);
+ if (py.PY_isTrue(this.ops.leapdays) && month > 2 && _utils.isleap(year)) {
+ days += asJS(this.ops.leapdays);
+ }
+
+ var ret = py.PY_add(
+ py.PY_call(py.PY_getAttr(other, 'replace'), repl),
+ py.PY_call(datetime.timedelta, {
+ days: py.float.fromJSON(days)
+ })
+ );
+
+ if (this.ops.weekday) {
+ // FIXME: only handles numeric weekdays, not decorated
+ var weekday = asJS(this.ops.weekday), nth = 1;
+ var jumpdays = (Math.abs(nth) - 1) * 7;
+
+ var ret_weekday = asJS(py.PY_call(py.PY_getAttr(ret, 'weekday')));
+ if (nth > 0) {
+ jumpdays += (7-ret_weekday+weekday) % 7;
+ } else {
+ jumpdays += (ret_weekday - weekday) % 7;
+ jumpdays *= -1;
+ }
+ ret = py.PY_add(
+ ret,
+ py.PY_call(datetime.timedelta, {
+ days: py.float.fromJSON(jumpdays)
+ })
+ );
+ }
+
+ return ret;
+ },
+ __radd__: function (other) {
+ return this.__add__(other);
+ },
+ __rsub__: function (other) {
+ return this.__neg__().__radd__(other);
+ },
+ __neg__: function () {
+ return py.PY_call(relativedelta, {
+ years: py.PY_negative(this.ops.years),
+ months: py.PY_negative(this.ops.months),
+ days: py.PY_negative(this.ops.days),
+ leapdays: this.ops.leapdays,
+ year: this.ops.year,
+ month: this.ops.month,
+ day: this.ops.day,
+ weekday: this.ops.weekday
+ });
+ }
+});
+
+// recursively wraps JS objects passed into the context to attributedicts
+// which jsonify back to JS objects
+function wrap(value) {
+ if (value === null) { return py.None; }
+
+ switch (typeof value) {
+ case 'undefined': throw new Error("No conversion for undefined");
+ case 'boolean': return py.bool.fromJSON(value);
+ case 'number': return py.float.fromJSON(value);
+ case 'string': return py.str.fromJSON(value);
+ }
+
+ switch(value.constructor) {
+ case Object: return wrapping_dict.fromJSON(value);
+ case Array: return wrapping_list.fromJSON(value);
+ }
+
+ throw new Error("ValueError: unable to wrap " + value);
+}
+
+var wrapping_dict = py.type('wrapping_dict', null, {
+ __init__: function () {
+ this._store = {};
+ },
+ __getitem__: function (key) {
+ var k = key.toJSON();
+ if (!(k in this._store)) {
+ throw new Error("KeyError: '" + k + "'");
+ }
+ return wrap(this._store[k]);
+ },
+ __getattr__: function (key) {
+ return this.__getitem__(py.str.fromJSON(key));
+ },
+ get: function () {
+ var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]);
+
+ if (!(args.k.toJSON() in this._store)) { return args.d; }
+ return this.__getitem__(args.k);
+ },
+ fromJSON: function (d) {
+ var instance = py.PY_call(wrapping_dict);
+ instance._store = d;
+ return instance;
+ },
+ toJSON: function () {
+ return this._store;
+ },
+});
+
+var wrapping_list = py.type('wrapping_list', null, {
+ __init__: function () {
+ this._store = [];
+ },
+ __getitem__: function (index) {
+ return wrap(this._store[index.toJSON()]);
+ },
+ fromJSON: function (ar) {
+ var instance = py.PY_call(wrapping_list);
+ instance._store = ar;
+ return instance;
+ },
+ toJSON: function () {
+ return this._store;
+ },
+});
+
+function wrap_context (context) {
+ for (var k in context) {
+ if (!context.hasOwnProperty(k)) { continue; }
+ var val = context[k];
+
+ if (val === null) { continue; }
+ if (val.constructor === Array) {
+ context[k] = wrapping_list.fromJSON(val);
+ } else if (val.constructor === Object
+ && !py.PY_isInstance(val, py.object)) {
+ context[k] = wrapping_dict.fromJSON(val);
+ }
+ }
+ return context;
+}
+
+function eval_contexts (contexts, evaluation_context) {
+ evaluation_context = _.extend(pycontext(), evaluation_context || {});
+ return _(contexts).reduce(function (result_context, ctx) {
+ // __eval_context evaluations can lead to some of `contexts`'s
+ // values being null, skip them as well as empty contexts
+ if (_.isEmpty(ctx)) { return result_context; }
+ if (_.isString(ctx)) {
+ // wrap raw strings in context
+ ctx = { __ref: 'context', __debug: ctx };
+ }
+ var evaluated = ctx;
+ switch(ctx.__ref) {
+ case 'context':
+ evaluation_context.context = evaluation_context;
+ evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context));
+ break;
+ case 'compound_context':
+ var eval_context = eval_contexts([ctx.__eval_context]);
+ evaluated = eval_contexts(
+ ctx.__contexts, _.extend({}, evaluation_context, eval_context));
+ break;
+ }
+ // add newly evaluated context to evaluation context for following
+ // siblings
+ _.extend(evaluation_context, evaluated);
+ return _.extend(result_context, evaluated);
+ }, {});
+}
+
+function eval_domains (domains, evaluation_context) {
+ evaluation_context = _.extend(pycontext(), evaluation_context || {});
+ var result_domain = [];
+ _(domains).each(function (domain) {
+ if (_.isString(domain)) {
+ // wrap raw strings in domain
+ domain = { __ref: 'domain', __debug: domain };
+ }
+ switch(domain.__ref) {
+ case 'domain':
+ evaluation_context.context = evaluation_context;
+ result_domain.push.apply(
+ result_domain, py.eval(domain.__debug, wrap_context(evaluation_context)));
+ break;
+ case 'compound_domain':
+ var eval_context = eval_contexts([domain.__eval_context]);
+ result_domain.push.apply(
+ result_domain, eval_domains(
+ domain.__domains, _.extend(
+ {}, evaluation_context, eval_context)));
+ break;
+ default:
+ result_domain.push.apply(result_domain, domain);
+ }
+ });
+ return result_domain;
+}
+
+function eval_groupbys (contexts, evaluation_context) {
+ evaluation_context = _.extend(pycontext(), evaluation_context || {});
+ var result_group = [];
+ _(contexts).each(function (ctx) {
+ if (_.isString(ctx)) {
+ // wrap raw strings in context
+ ctx = { __ref: 'context', __debug: ctx };
+ }
+ var group;
+ var evaluated = ctx;
+ switch(ctx.__ref) {
+ case 'context':
+ evaluation_context.context = evaluation_context;
+ evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context));
+ break;
+ case 'compound_context':
+ var eval_context = eval_contexts([ctx.__eval_context]);
+ evaluated = eval_contexts(
+ ctx.__contexts, _.extend({}, evaluation_context, eval_context));
+ break;
+ }
+ group = evaluated.group_by;
+ if (!group) { return; }
+ if (typeof group === 'string') {
+ result_group.push(group);
+ } else if (group instanceof Array) {
+ result_group.push.apply(result_group, group);
+ } else {
+ throw new Error('Got invalid groupby {{'
+ + JSON.stringify(group) + '}}');
+ }
+ _.extend(evaluation_context, evaluated);
+ });
+ return result_group;
+}
+
+
+function pycontext () {
+ return _.extend({
+ datetime: datetime,
+ context_today: context_today,
+ time: time,
+ relativedelta: relativedelta,
+ current_date: py.PY_call(
+ time.strftime, [py.str.fromJSON('%Y-%m-%d')]),
+ }, session.user_context);
+}
+
+/**
+ * @param {String} type "domains", "contexts" or "groupbys"
+ * @param {Array} object domains or contexts to evaluate
+ * @param {Object} [context] evaluation context
+ */
+function pyeval (type, object, context, options) {
+ options = options || {};
+ context = _.extend(pycontext(), context || {});
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch(type) {
+ case 'context':
+ case 'contexts':
+ if (type === 'context')
+ object = [object];
+ return eval_contexts((options.no_user_context ? [] : [session.user_context]).concat(object), context);
+ case 'domain':
+ case 'domains':
+ if (type === 'domain')
+ object = [object];
+ return eval_domains(object, context);
+ case 'groupbys':
+ return eval_groupbys(object, context);
+ }
+ throw new Error("Unknow evaluation type " + type);
+}
+
+function eval_arg (arg) {
+ if (typeof arg !== 'object' || !arg.__ref) { return arg; }
+ switch(arg.__ref) {
+ case 'domain': case 'compound_domain':
+ return pyeval('domains', [arg]);
+ case 'context': case 'compound_context':
+ return pyeval('contexts', [arg]);
+ default:
+ throw new Error(_t("Unknown nonliteral type " + arg.__ref));
+ }
+}
+
+/**
+ * If args or kwargs are unevaluated contexts or domains (compound or not),
+ * evaluated them in-place.
+ *
+ * Potentially mutates both parameters.
+ *
+ * @param args
+ * @param kwargs
+ */
+function ensure_evaluated (args, kwargs) {
+ for (var i=0; i<args.length; ++i) {
+ args[i] = eval_arg(args[i]);
+ }
+ for (var k in kwargs) {
+ if (!kwargs.hasOwnProperty(k)) { continue; }
+ kwargs[k] = eval_arg(kwargs[k]);
+ }
+}
+
+function eval_domains_and_contexts (source) {
+ return new $.Deferred(function (d) {setTimeout(function () {
+ var result;
+ try {
+ result = sync_eval_domains_and_contexts(source);
+ }
+ catch (e) {
+ result = { error: {
+ code: 400,
+ message: _t("Evaluation Error"),
+ data: {
+ type: 'local_exception',
+ debug: _.str.sprintf(
+ _t("Local evaluation failure\n%s\n\n%s"),
+ e.message, JSON.stringify(source))
+ }
+ }};
+ }
+ d.resolve(result);
+ }, 0); });
+}
+
+function sync_eval_domains_and_contexts (source) {
+ var contexts = ([session.user_context] || []).concat(source.contexts);
+ // see Session.eval_context in Python
+ return {
+ context: pyeval('contexts', contexts),
+ domain: pyeval('domains', source.domains),
+ group_by: pyeval('groupbys', source.group_by_seq || [])
+ };
+}
+
+function py_eval (expr, context) {
+ return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null}));
+}
+
+
+return {
+ context: pycontext,
+ ensure_evaluated: ensure_evaluated,
+ eval: pyeval,
+ eval_domains_and_contexts: eval_domains_and_contexts,
+ sync_eval_domains_and_contexts: sync_eval_domains_and_contexts,
+ py_eval: py_eval,
+};
+
+});
diff --git a/addons/web/static/src/js/framework/registry.js b/addons/web/static/src/js/framework/registry.js
new file mode 100644
index 00000000000..c90614cf94a
--- /dev/null
+++ b/addons/web/static/src/js/framework/registry.js
@@ -0,0 +1,54 @@
+odoo.define('web.Registry', ['web.Class'], function (require) {
+"use strict";
+
+var Class = require('web.Class');
+
+var Registry = Class.extend({
+ init: function (mapping) {
+ this.map = Object.create(mapping || null);
+ },
+ get: function (key) {
+ return this.map[key];
+ },
+ contains: function (key) {
+ return (key in this.map);
+ },
+ /**
+ * Tries a number of keys, and returns the first object matching one of
+ * the keys.
+ *
+ * @param {Array} keys a sequence of keys to fetch the object for
+ * @returns {Class} the first class found matching an object
+ */
+ get_any: function (keys) {
+ for (var i=0; i<keys.length; i++) {
+ if (keys[i] in this.map) {
+ return this.map[keys[i]];
+ }
+ }
+ return null;
+ },
+ add: function (key, value) {
+ this.map[key] = value;
+ return this;
+ },
+ /**
+ * Creates and returns a copy of the current mapping, with the provided
+ * mapping argument added in (replacing existing keys if needed)
+ *
+ * Parent and child remain linked, a new key in the parent (which is not
+ * overwritten by the child) will appear in the child.
+ *
+ * @param {Object} [mapping={}] a mapping of keys to object-paths
+ */
+ extend: function (mapping) {
+ var child = new Registry(this.map);
+ _.extend(child.map, mapping);
+ return child;
+ },
+});
+
+return Registry;
+
+});
+
diff --git a/addons/web/static/src/js/framework/session.js b/addons/web/static/src/js/framework/session.js
new file mode 100644
index 00000000000..f81fec5ab54
--- /dev/null
+++ b/addons/web/static/src/js/framework/session.js
@@ -0,0 +1,393 @@
+odoo.define('web.Session', ['web.ajax', 'web.core', 'web.utils'], function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var utils = require('web.utils');
+
+var _t = core._t;
+var qweb = core.qweb;
+var mixins = core.mixins;
+
+var Session = core.Class.extend(mixins.EventDispatcherMixin, {
+ /**
+
+ @param parent The parent of the newly created object.
+ or `null` if the server to contact is the origin server.
+ @param {Dict} options A dictionary that can contain the following options:
+
+ * "override_session": Default to false. If true, the current session object will
+ not try to re-use a previously created session id stored in a cookie.
+ * "session_id": Default to null. If specified, the specified session_id will be used
+ by this session object. Specifying this option automatically implies that the option
+ "override_session" is set to true.
+ */
+ init: function(parent, origin, options) {
+ mixins.EventDispatcherMixin.init.call(this, parent);
+ options = options || {};
+ this.module_list = (options.modules && options.modules.slice()) || (window.odoo._modules && window.odoo._modules.slice()) || [];
+ this.server = null;
+ this.session_id = options.session_id || null;
+ this.override_session = options.override_session || !!options.session_id || false;
+ this.avoid_recursion = false;
+ this.use_cors = options.use_cors || false;
+ this.setup(origin);
+ this.debug = ($.deparam($.param.querystring()).debug !== undefined);
+ // TODO: session store in cookie should be optional
+ this.qweb_mutex = new utils.Mutex();
+ },
+ setup: function(origin, options) {
+ // must be able to customize server
+ var window_origin = location.protocol + "//" + location.host;
+ origin = origin ? origin.replace( /\/+$/, '') : window_origin;
+ if (!_.isUndefined(this.origin) && this.origin !== origin)
+ throw new Error('Session already bound to ' + this.origin);
+ else
+ this.origin = origin;
+ this.prefix = this.origin;
+ this.server = this.origin; // keep chs happy
+ this.origin_server = this.origin === window_origin;
+ options = options || {};
+ if ('use_cors' in options) {
+ this.use_cors = options.use_cors;
+ }
+ },
+ /**
+ * Setup a session
+ */
+ session_bind: function(origin) {
+ var self = this;
+ this.setup(origin);
+ qweb.default_dict._s = this.origin;
+ this.uid = null;
+ this.username = null;
+ this.user_context= {};
+ this.db = null;
+ this.module_loaded = {};
+ _(this.module_list).each(function (mod) {
+ self.module_loaded[mod] = true;
+ });
+ this.active_id = null;
+ return this.session_init();
+ },
+ /**
+ * Init a session, reloads from cookie, if it exists
+ */
+ session_init: function () {
+ var self = this;
+ return this.session_reload().then(function() {
+ var modules = self.module_list.join(',');
+ var deferred = self.load_qweb(modules);
+ if(self.session_is_valid()) {
+ return deferred.then(function() { return self.load_modules(); });
+ }
+ return $.when(
+ deferred,
+ self.rpc('/web/webclient/bootstrap_translations', {mods: self.module_list}).then(function(trans) {
+ _t.database.set_bundle(trans);
+ })
+ );
+ });
+ },
+ session_is_valid: function() {
+ var db = $.deparam.querystring().db;
+ if (db && this.db !== db) {
+ return false;
+ }
+ return !!this.uid;
+ },
+ /**
+ * The session is validated by restoration of a previous session
+ */
+ session_authenticate: function() {
+ var self = this;
+ return $.when(this._session_authenticate.apply(this, arguments)).then(function() {
+ return self.load_modules();
+ });
+ },
+ /**
+ * The session is validated either by login or by restoration of a previous session
+ */
+ _session_authenticate: function(db, login, password) {
+ var self = this;
+ var params = {db: db, login: login, password: password};
+ return this.rpc("/web/session/authenticate", params).then(function(result) {
+ if (!result.uid) {
+ return $.Deferred().reject();
+ }
+ delete result.session_id;
+ _.extend(self, result);
+ });
+ },
+ session_logout: function() {
+ $.bbq.removeState();
+ return this.rpc("/web/session/destroy", {});
+ },
+ get_cookie: function (name) {
+ if (!this.name) { return null; }
+ var nameEQ = this.name + '|' + name + '=';
+ var cookies = document.cookie.split(';');
+ for(var i=0; i<cookies.length; ++i) {
+ var cookie = cookies[i].replace(/^\s*/, '');
+ if(cookie.indexOf(nameEQ) === 0) {
+ try {
+ return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
+ } catch(err) {
+ // wrong cookie, delete it
+ this.set_cookie(name, '', -1);
+ }
+ }
+ }
+ return null;
+ },
+ /**
+ * Create a new cookie with the provided name and value
+ *
+ * @private
+ * @param name the cookie's name
+ * @param value the cookie's value
+ * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
+ */
+ set_cookie: function (name, value, ttl) {
+ if (!this.name) { return; }
+ ttl = ttl || 24*60*60*365;
+ document.cookie = [
+ this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
+ 'path=/',
+ 'max-age=' + ttl,
+ 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
+ ].join(';');
+ },
+ /**
+ * Load additional web addons of that instance and init them
+ *
+ */
+ load_modules: function() {
+ var self = this;
+ return this.rpc('/web/session/modules', {}).then(function(result) {
+ var all_modules = _.uniq(self.module_list.concat(result));
+ var to_load = _.difference(result, self.module_list).join(',');
+ self.module_list = all_modules;
+
+ var loaded = self.load_translations();
+ var locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US';
+ var file_list = [ locale ];
+ if(to_load.length) {
+ loaded = $.when(
+ loaded,
+ self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)),
+ self.load_qweb(to_load),
+ self.rpc('/web/webclient/jslist', {mods: to_load}).done(function(files) {
+ file_list = file_list.concat(files);
+ })
+ );
+ }
+ return loaded.then(function () {
+ return self.load_js(file_list);
+ }).done(function() {
+ self.on_modules_loaded();
+ self.trigger('module_loaded');
+ });
+ });
+ },
+ load_translations: function() {
+ return _t.database.load_translations(this, this.module_list, this.user_context.lang);
+ },
+ load_css: function (files) {
+ var self = this;
+ _.each(files, function (file) {
+ ajax.loadCSS(self.url(file, null));
+ });
+ },
+ load_js: function(files) {
+ var self = this;
+ var d = $.Deferred();
+ if (files.length !== 0) {
+ var file = files.shift();
+ var url = self.url(file, null);
+ ajax.loadJS(url).done(d.resolve);
+ } else {
+ d.resolve();
+ }
+ return d;
+ },
+ load_qweb: function(mods) {
+ var self = this;
+ self.qweb_mutex.exec(function() {
+ return self.rpc('/web/proxy/load', {path: '/web/webclient/qweb?mods=' + mods}).then(function(xml) {
+ if (!xml) { return; }
+ qweb.add_template(_.str.trim(xml));
+ });
+ });
+ return self.qweb_mutex.def;
+ },
+ on_modules_loaded: function() {
+ var openerp = window.openerp;
+ for(var j=0; j<this.module_list.length; j++) {
+ var mod = this.module_list[j];
+ if(this.module_loaded[mod])
+ continue;
+ openerp[mod] = {};
+ // init module mod
+ var fct = openerp._openerp[mod];
+ if(typeof(fct) === "function") {
+ openerp._openerp[mod] = {};
+ for (var k in fct) {
+ openerp._openerp[mod][k] = fct[k];
+ }
+ fct(openerp, openerp._openerp[mod]);
+ }
+ this.module_loaded[mod] = true;
+ }
+ },
+ get_file: function (options) {
+ if (this.override_session){
+ options.data.session_id = this.session_id;
+ }
+ ajax.get_file(options);
+ },
+ synchronized_mode: function(to_execute) {
+ var synch = this.synch;
+ this.synch = true;
+ try {
+ return to_execute();
+ } finally {
+ this.synch = synch;
+ }
+ },
+ /**
+ * (re)loads the content of a session: db name, username, user id, session
+ * context and status of the support contract
+ *
+ * @returns {$.Deferred} deferred indicating the session is done reloading
+ */
+ session_reload: function () {
+ var self = this;
+ return self.rpc("/web/session/get_session_info", {}).then(function(result) {
+ delete result.session_id;
+ _.extend(self, result);
+ });
+ },
+ check_session_id: function() {
+ var self = this;
+ if (this.avoid_recursion)
+ return $.when();
+ if (this.session_id)
+ return $.when(); // we already have the session id
+ if (!this.use_cors && (this.override_session || ! this.origin_server)) {
+ // If we don't use the origin server we consider we should always create a new session.
+ // Even if some browsers could support cookies when using jsonp that behavior is
+ // not consistent and the browser creators are tending to removing that feature.
+ this.avoid_recursion = true;
+ return this.rpc("/gen_session_id", {}).then(function(result) {
+ self.session_id = result;
+ }).always(function() {
+ self.avoid_recursion = false;
+ });
+ } else {
+ // normal use case, just use the cookie
+ self.session_id = utils.get_cookie("session_id");
+ return $.when();
+ }
+ },
+ /**
+ * Executes an RPC call, registering the provided callbacks.
+ *
+ * Registers a default error callback if none is provided, and handles
+ * setting the correct session id and session context in the parameter
+ * objects
+ *
+ * @param {String} url RPC endpoint
+ * @param {Object} params call parameters
+ * @param {Object} options additional options for rpc call
+ * @param {Function} success_callback function to execute on RPC call success
+ * @param {Function} error_callback function to execute on RPC call failure
+ * @returns {jQuery.Deferred} jquery-provided ajax deferred
+ */
+ rpc: function(url, params, options) {
+ var self = this;
+ options = _.clone(options || {});
+ var shadow = options.shadow || false;
+ delete options.shadow;
+
+ return self.check_session_id().then(function() {
+ // TODO: remove
+ if (! _.isString(url)) {
+ _.extend(options, url);
+ url = url.url;
+ }
+ // TODO correct handling of timeouts
+ if (! shadow)
+ self.trigger('request');
+ var fct;
+ if (self.origin_server) {
+ fct = ajax.jsonRpc;
+ if (self.override_session) {
+ options.headers = _.extend({}, options.headers, {
+ "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
+ });
+ }
+ } else if (self.use_cors) {
+ fct = ajax.jsonRpc;
+ url = self.url(url, null);
+ options.session_id = self.session_id || '';
+ if (self.override_session) {
+ options.headers = _.extend({}, options.headers, {
+ "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
+ });
+ }
+ } else {
+ fct = ajax.jsonpRpc;
+ url = self.url(url, null);
+ options.session_id = self.session_id || '';
+ }
+ var p = fct(url, "call", params, options);
+ p = p.then(function (result) {
+ if (! shadow)
+ self.trigger('response');
+ return result;
+ }, function(type, error, textStatus, errorThrown) {
+ if (type === "server") {
+ if (! shadow)
+ self.trigger('response');
+ if (error.code === 100) {
+ self.uid = false;
+ }
+ return $.Deferred().reject(error, $.Event());
+ } else {
+ if (! shadow)
+ self.trigger('response_failed');
+ var nerror = {
+ code: -32098,
+ message: "XmlHttpRequestError " + errorThrown,
+ data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
+ };
+ return $.Deferred().reject(nerror, $.Event());
+ }
+ });
+ return p.fail(function() { // Allow deferred user to disable rpc_error call in fail
+ p.fail(function(error, event) {
+ if (!event.isDefaultPrevented()) {
+ self.trigger('error', error, event);
+ }
+ });
+ });
+ });
+ },
+ url: function(path, params) {
+ params = _.extend(params || {});
+ if (this.override_session || (! this.origin_server))
+ params.session_id = this.session_id;
+ var qs = $.param(params);
+ if (qs.length > 0)
+ qs = "?" + qs;
+ var prefix = _.any(['http://', 'https://', '//'], function(el) {
+ return path.length >= el.length && path.slice(0, el.length) === el;
+ }) ? '' : this.prefix;
+ return prefix + path + qs;
+ },
+});
+
+return Session;
+
+});
diff --git a/addons/web/static/src/js/framework/time.js b/addons/web/static/src/js/framework/time.js
new file mode 100644
index 00000000000..7a13655c39b
--- /dev/null
+++ b/addons/web/static/src/js/framework/time.js
@@ -0,0 +1,285 @@
+odoo.define('web.time', ['web.translation', 'web.utils'], function (require) {
+"use strict";
+
+var translation = require('web.translation');
+var utils = require('web.utils');
+
+var lpad = utils.lpad;
+var rpad = utils.rpad;
+var _t = translation._t;
+
+/**
+ * Replacer function for JSON.stringify, serializes Date objects to UTC
+ * datetime in the OpenERP Server format.
+ *
+ * However, if a serialized value has a toJSON method that method is called
+ * *before* the replacer is invoked. Date#toJSON exists, and thus the value
+ * passed to the replacer is a string, the original Date has to be fetched
+ * on the parent object (which is provided as the replacer's context).
+ *
+ * @param {String} k
+ * @param {Object} v
+ * @returns {Object}
+ */
+function date_to_utc (k, v) {
+ var value = this[k];
+ if (!(value instanceof Date)) { return v; }
+
+ return datetime_to_str(value);
+}
+
+/**
+ * Converts a string to a Date javascript object using OpenERP's
+ * datetime string format (exemple: '2011-12-01 15:12:35.832').
+ *
+ * The time zone is assumed to be UTC (standard for OpenERP 6.1)
+ * and will be converted to the browser's time zone.
+ *
+ * @param {String} str A string representing a datetime.
+ * @returns {Date}
+ */
+function str_to_datetime (str) {
+ if(!str) {
+ return str;
+ }
+ var regex = /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
+ var res = regex.exec(str);
+ if ( !res ) {
+ throw new Error("'" + str + "' is not a valid datetime");
+ }
+ var tmp = new Date(2000,0,1);
+ tmp.setUTCMonth(1970);
+ tmp.setUTCMonth(0);
+ tmp.setUTCDate(1);
+ tmp.setUTCFullYear(parseFloat(res[1]));
+ tmp.setUTCMonth(parseFloat(res[2]) - 1);
+ tmp.setUTCDate(parseFloat(res[3]));
+ tmp.setUTCHours(parseFloat(res[4]));
+ tmp.setUTCMinutes(parseFloat(res[5]));
+ tmp.setUTCSeconds(parseFloat(res[6]));
+ tmp.setUTCSeconds(parseFloat(res[6]));
+ tmp.setUTCMilliseconds(parseFloat(utils.rpad((res[7] || "").slice(0, 3), 3)));
+ return tmp;
+}
+
+/**
+ * Converts a string to a Date javascript object using OpenERP's
+ * date string format (exemple: '2011-12-01').
+ *
+ * As a date is not subject to time zones, we assume it should be
+ * represented as a Date javascript object at 00:00:00 in the
+ * time zone of the browser.
+ *
+ * @param {String} str A string representing a date.
+ * @returns {Date}
+ */
+function str_to_date (str) {
+ if(!str) {
+ return str;
+ }
+ var regex = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
+ var res = regex.exec(str);
+ if ( !res ) {
+ throw new Error("'" + str + "' is not a valid date");
+ }
+ var tmp = new Date(2000,0,1);
+ tmp.setFullYear(parseFloat(res[1]));
+ tmp.setMonth(parseFloat(res[2]) - 1);
+ tmp.setDate(parseFloat(res[3]));
+ tmp.setHours(0);
+ tmp.setMinutes(0);
+ tmp.setSeconds(0);
+ return tmp;
+}
+
+/**
+ * Converts a string to a Date javascript object using OpenERP's
+ * time string format (exemple: '15:12:35').
+ *
+ * The OpenERP times are supposed to always be naive times. We assume it is
+ * represented using a javascript Date with a date 1 of January 1970 and a
+ * time corresponding to the meant time in the browser's time zone.
+ *
+ * @param {String} str A string representing a time.
+ * @returns {Date}
+ */
+function str_to_time (str) {
+ if(!str) {
+ return str;
+ }
+ var regex = /^(\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
+ var res = regex.exec(str);
+ if ( !res ) {
+ throw new Error("'" + str + "' is not a valid time");
+ }
+ var tmp = new Date();
+ tmp.setFullYear(1970);
+ tmp.setMonth(0);
+ tmp.setDate(1);
+ tmp.setHours(parseFloat(res[1]));
+ tmp.setMinutes(parseFloat(res[2]));
+ tmp.setSeconds(parseFloat(res[3]));
+ tmp.setMilliseconds(parseFloat(rpad((res[4] || "").slice(0, 3), 3)));
+ return tmp;
+}
+
+/**
+ * Converts a Date javascript object to a string using OpenERP's
+ * datetime string format (exemple: '2011-12-01 15:12:35').
+ *
+ * The time zone of the Date object is assumed to be the one of the
+ * browser and it will be converted to UTC (standard for OpenERP 6.1).
+ *
+ * @param {Date} obj
+ * @returns {String} A string representing a datetime.
+ */
+function datetime_to_str (obj) {
+ if (!obj) {
+ return false;
+ }
+ return lpad(obj.getUTCFullYear(),4) + "-" + lpad(obj.getUTCMonth() + 1,2) + "-"
+ + lpad(obj.getUTCDate(),2) + " " + lpad(obj.getUTCHours(),2) + ":"
+ + lpad(obj.getUTCMinutes(),2) + ":" + lpad(obj.getUTCSeconds(),2);
+}
+
+/**
+ * Converts a Date javascript object to a string using OpenERP's
+ * date string format (exemple: '2011-12-01').
+ *
+ * As a date is not subject to time zones, we assume it should be
+ * represented as a Date javascript object at 00:00:00 in the
+ * time zone of the browser.
+ *
+ * @param {Date} obj
+ * @returns {String} A string representing a date.
+ */
+function date_to_str (obj) {
+ if (!obj) {
+ return false;
+ }
+ return lpad(obj.getFullYear(),4) + "-" + lpad(obj.getMonth() + 1,2) + "-"
+ + lpad(obj.getDate(),2);
+}
+
+/**
+ * Converts a Date javascript object to a string using OpenERP's
+ * time string format (exemple: '15:12:35').
+ *
+ * The OpenERP times are supposed to always be naive times. We assume it is
+ * represented using a javascript Date with a date 1 of January 1970 and a
+ * time corresponding to the meant time in the browser's time zone.
+ *
+ * @param {Date} obj
+ * @returns {String} A string representing a time.
+ */
+function time_to_str (obj) {
+ if (!obj) {
+ return false;
+ }
+ return lpad(obj.getHours(),2) + ":" + lpad(obj.getMinutes(),2) + ":"
+ + lpad(obj.getSeconds(),2);
+}
+
+function auto_str_to_date (value) {
+ try {
+ return str_to_datetime(value);
+ } catch(e) {}
+ try {
+ return str_to_date(value);
+ } catch(e) {}
+ try {
+ return str_to_time(value);
+ } catch(e) {}
+ throw new Error(_.str.sprintf(_t("'%s' is not a correct date, datetime nor time"), value));
+}
+
+function auto_date_to_str (value, type) {
+ switch(type) {
+ case 'datetime':
+ return datetime_to_str(value);
+ case 'date':
+ return date_to_str(value);
+ case 'time':
+ return time_to_str(value);
+ default:
+ throw new Error(_.str.sprintf(_t("'%s' is not convertible to date, datetime nor time"), type));
+ }
+}
+
+/**
+ * Convert Python strftime to escaped moment.js format.
+ *
+ * @param {String} value original format
+ */
+function strftime_to_moment_format (value) {
+ if (_normalize_format_cache[value] === undefined) {
+ var isletter = /[a-zA-Z]/,
+ output = [],
+ inToken = false;
+
+ for (var index=0; index < value.length; ++index) {
+ var character = value[index];
+ if (character === '%' && !inToken) {
+ inToken = true;
+ continue;
+ }
+ if (isletter.test(character)) {
+ if (inToken && normalize_format_table[character] !== undefined) {
+ character = normalize_format_table[character];
+ } else {
+ character = '[' + character + ']'; // moment.js escape
+ }
+ }
+ output.push(character);
+ inToken = false;
+ }
+ _normalize_format_cache[value] = output.join('');
+ }
+ return _normalize_format_cache[value];
+}
+
+var _normalize_format_cache = {};
+var normalize_format_table = {
+ // Python strftime to moment.js conversion table
+ // See openerp/addons/base/res/res_lang_view.xml
+ // for details about supported directives
+ 'a': 'ddd',
+ 'A': 'dddd',
+ 'b': 'MMM',
+ 'B': 'MMMM',
+ 'd': 'DD',
+ 'H': 'HH',
+ 'I': 'hh',
+ 'j': 'DDDD',
+ 'm': 'MM',
+ 'M': 'mm',
+ 'p': 'A',
+ 'S': 'ss',
+ 'U': 'ww',
+ 'W': 'WW',
+ 'w': 'd',
+ 'y': 'YY',
+ 'Y': 'YYYY',
+ // unsupported directives
+ 'c': 'ddd MMM D HH:mm:ss YYYY',
+ 'x': 'MM/DD/YY',
+ 'X': 'HH:mm:ss'
+};
+
+
+
+return {
+ date_to_utc: date_to_utc,
+ str_to_datetime: str_to_datetime,
+ str_to_date: str_to_date,
+ str_to_time: str_to_time,
+ datetime_to_str: datetime_to_str,
+ date_to_str: date_to_str,
+ time_to_str: time_to_str,
+ auto_str_to_date: auto_str_to_date,
+ auto_date_to_str: auto_date_to_str,
+ strftime_to_moment_format: strftime_to_moment_format,
+};
+
+});
+
diff --git a/addons/web/static/src/js/framework/translation.js b/addons/web/static/src/js/framework/translation.js
new file mode 100644
index 00000000000..6f44a57b275
--- /dev/null
+++ b/addons/web/static/src/js/framework/translation.js
@@ -0,0 +1,98 @@
+
+odoo.define('web.translation', ['web.Class', 'qweb'], function (require) {
+"use strict";
+
+var Class = require('web.Class');
+
+var qweb = require('qweb');
+
+
+var TranslationDataBase = Class.extend(/** @lends instance.TranslationDataBase# */{
+ init: function() {
+ this.db = {};
+ this.parameters = {"direction": 'ltr',
+ "date_format": '%m/%d/%Y',
+ "time_format": '%H:%M:%S',
+ "grouping": [],
+ "decimal_point": ".",
+ "thousands_sep": ","};
+ },
+ set_bundle: function(translation_bundle) {
+ var self = this;
+ this.db = {};
+ var modules = _.keys(translation_bundle.modules);
+ modules.sort();
+ if (_.include(modules, "web")) {
+ modules = ["web"].concat(_.without(modules, "web"));
+ }
+ _.each(modules, function(name) {
+ self.add_module_translation(translation_bundle.modules[name]);
+ });
+ if (translation_bundle.lang_parameters) {
+ this.parameters = translation_bundle.lang_parameters;
+ if (typeof(py) !== "undefined") {
+ this.parameters.grouping = py.eval(this.parameters.grouping);
+ }
+ }
+ },
+ add_module_translation: function(mod) {
+ var self = this;
+ _.each(mod.messages, function(message) {
+ self.db[message.id] = message.string;
+ });
+ },
+ build_translation_function: function() {
+ var self = this;
+ var fcnt = function(str) {
+ var tmp = self.get(str);
+ return tmp === undefined ? str : tmp;
+ };
+ fcnt.database = this;
+ return fcnt;
+ },
+ get: function(key) {
+ return this.db[key];
+ },
+ /**
+ Loads the translations from an OpenERP server.
+
+ @param {openerp.Session} session The session object to contact the server.
+ @param {Array} [modules] The list of modules to load the translation. If not specified,
+ it will default to all the modules installed in the current database.
+ @param {Object} [lang] lang The language. If not specified it will default to the language
+ of the current user.
+ @returns {jQuery.Deferred}
+ */
+ load_translations: function(session, modules, lang) {
+ var self = this;
+ return session.rpc('/web/webclient/translations', {
+ "mods": modules || null,
+ "lang": lang || null
+ }).done(function(trans) {
+ self.set_bundle(trans);
+ });
+ }
+});
+
+var _t = new TranslationDataBase().build_translation_function();
+var _lt = function (s) {
+ return {toString: function () { return _t(s); }};
+};
+
+// provide timeago.js with our own translator method
+$.timeago.settings.translator = _t;
+
+qweb.default_dict = {
+ '_' : _,
+ 'JSON': JSON,
+ '_t' : _t,
+};
+
+return {
+ _t: _t,
+ _lt: _lt,
+ TranslationDataBase: TranslationDataBase,
+};
+
+});
+
diff --git a/addons/web/static/src/js/framework/utils.js b/addons/web/static/src/js/framework/utils.js
new file mode 100644
index 00000000000..d412a17bc9a
--- /dev/null
+++ b/addons/web/static/src/js/framework/utils.js
@@ -0,0 +1,383 @@
+odoo.define('web.utils', ['web.Class', 'web.translation'], function (require) {
+"use strict";
+
+var Class = require('web.Class');
+var translation = require('web.translation');
+
+var _t = translation._t;
+
+/**
+ * computes (Math.floor(a/b), a%b and passes that to the callback.
+ *
+ * returns the callback's result
+ */
+function divmod (a, b, fn) {
+ var mod = a%b;
+ // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a
+ // round of b
+ if (mod > 0 && b < 0 || mod < 0 && b > 0) {
+ mod += b;
+ }
+ return fn(Math.floor(a/b), mod);
+}
+
+/**
+ * Passes the fractional and integer parts of x to the callback, returns
+ * the callback's result
+ */
+function modf (x, fn) {
+ var mod = x%1;
+ if (mod < 0) {
+ mod += 1;
+ }
+ return fn(mod, Math.floor(x));
+}
+
+/*
+ * Left-pad provided arg 1 with zeroes until reaching size provided by second
+ * argument.
+ *
+ * @param {Number|String} str value to pad
+ * @param {Number} size size to reach on the final padded value
+ * @returns {String} padded string
+ */
+function lpad (str, size) {
+ str = "" + str;
+ return new Array(size - str.length + 1).join('0') + str;
+}
+
+function rpad (str, size) {
+ str = "" + str;
+ return str + new Array(size - str.length + 1).join('0');
+}
+
+var id = -1;
+
+function generate_id () {
+ return ++id;
+}
+
+/**
+ * Intersperses ``separator`` in ``str`` at the positions indicated by
+ * ``indices``.
+ *
+ * ``indices`` is an array of relative offsets (from the previous insertion
+ * position, starting from the end of the string) at which to insert
+ * ``separator``.
+ *
+ * There are two special values:
+ *
+ * ``-1``
+ * indicates the insertion should end now
+ * ``0``
+ * indicates that the previous section pattern should be repeated (until all
+ * of ``str`` is consumed)
+ *
+ * @param {String} str
+ * @param {Array<Number>} indices
+ * @param {String} separator
+ * @returns {String}
+ */
+function intersperse (str, indices, separator) {
+ separator = separator || '';
+ var result = [], last = str.length;
+
+ for(var i=0; i<indices.length; ++i) {
+ var section = indices[i];
+ if (section === -1 || last <= 0) {
+ // Done with string, or -1 (stops formatting string)
+ break;
+ } else if(section === 0 && i === 0) {
+ // repeats previous section, which there is none => stop
+ break;
+ } else if (section === 0) {
+ // repeat previous section forever
+ //noinspection AssignmentToForLoopParameterJS
+ section = indices[--i];
+ }
+ result.push(str.substring(last-section, last));
+ last -= section;
+ }
+
+ var s = str.substring(0, last);
+ if (s) { result.push(s); }
+ return result.reverse().join(separator);
+}
+
+function xml_to_json (node, strip_whitespace) {
+ switch (node.nodeType) {
+ case 9:
+ return xml_to_json(node.documentElement, strip_whitespace);
+ case 3:
+ case 4:
+ return (strip_whitespace && node.data.trim() === '') ? undefined : node.data;
+ case 1:
+ var attrs = $(node).getAttributes();
+ _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
+ if (attrs[key]) {
+ try {
+ attrs[key] = JSON.parse(attrs[key]);
+ } catch(e) { }
+ }
+ });
+ return {
+ tag: node.tagName.toLowerCase(),
+ attrs: attrs,
+ children: _.compact(_.map(node.childNodes, function(node) {
+ return xml_to_json(node, strip_whitespace);
+ })),
+ };
+ }
+}
+
+function json_node_to_xml (node, human_readable, indent) {
+ // For debugging purpose, this function will convert a json node back to xml
+ indent = indent || 0;
+ var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
+ r = sindent + '<' + node.tag,
+ cr = human_readable ? '\n' : '';
+
+ if (typeof(node) === 'string') {
+ return sindent + node;
+ } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
+ throw new Error(
+ _.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
+ JSON.stringify(node)));
+ }
+ for (var attr in node.attrs) {
+ var vattr = node.attrs[attr];
+ if (typeof(vattr) !== 'string') {
+ // domains, ...
+ vattr = JSON.stringify(vattr);
+ }
+ vattr = vattr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+ if (human_readable) {
+ vattr = vattr.replace(/&quot;/g, "'");
+ }
+ r += ' ' + attr + '="' + vattr + '"';
+ }
+ if (node.children && node.children.length) {
+ r += '>' + cr;
+ var childs = [];
+ for (var i = 0, ii = node.children.length; i < ii; i++) {
+ childs.push(json_node_to_xml(node.children[i], human_readable, indent + 1));
+ }
+ r += childs.join(cr);
+ r += cr + sindent + '</' + node.tag + '>';
+ return r;
+ } else {
+ return r + '/>';
+ }
+}
+
+function xml_to_str (node) {
+ var str = "";
+ if (window.XMLSerializer) {
+ str = (new XMLSerializer()).serializeToString(node);
+ } else if (window.ActiveXObject) {
+ str = node.xml;
+ } else {
+ throw new Error(_t("Could not serialize XML"));
+ }
+ // Browsers won't deal with self closing tags except void elements:
+ // http://www.w3.org/TR/html-markup/syntax.html
+ var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
+
+ // The following regex is a bit naive but it's ok for the xmlserializer output
+ str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
+ if (void_elements.indexOf(tag) < 0) {
+ return "<" + tag + attrs + "></" + tag + ">";
+ } else {
+ return match;
+ }
+ });
+ return str;
+}
+
+function Mutex () {
+ this.def = $.Deferred().resolve();
+}
+
+Mutex.prototype.exec = function (action) {
+ var current = this.def;
+ var next = this.def = $.Deferred();
+ return current.then(function() {
+ return $.when(action()).always(function() {
+ next.resolve();
+ });
+ });
+};
+
+function get_cookie (c_name) {
+ var cookies = document.cookie ? document.cookie.split('; ') : [];
+ for (var i = 0, l = cookies.length; i < l; i++) {
+ var parts = cookies[i].split('=');
+ var name = parts.shift();
+ var cookie = parts.join('=');
+
+ if (c_name && c_name === name) {
+ return cookie;
+ }
+ }
+ return "";
+}
+
+/**
+ * Insert "thousands" separators in the provided number (which is actually
+ * a string)
+ *
+ * @param {String} num
+ * @returns {String}
+ */
+function insert_thousand_seps (num) {
+ var negative = num[0] === '-';
+ num = (negative ? num.slice(1) : num);
+ return (negative ? '-' : '') + intersperse(
+ num, _t.database.parameters.grouping, _t.database.parameters.thousands_sep);
+}
+
+function is_bin_size (v) {
+ return (/^\d+(\.\d*)? \w+$/).test(v);
+}
+
+/**
+ * Check with a scary heuristic if the value is a bin_size or not.
+ * If not, compute an approximate size out of the base64 encoded string.
+ *
+ * @param {String} value original format
+ */
+function binary_to_binsize (value) {
+ if (!value) {
+ return human_size(0);
+ }
+ if (value.substr(0, 10).indexOf(' ') == -1) {
+ // Computing approximate size out of base64 encoded string
+ // http://en.wikipedia.org/wiki/Base64#MIME
+ return human_size(value.length / 1.37);
+ } else {
+ // already bin_size
+ return value;
+ }
+}
+
+/**
+ * Returns a human readable size
+ *
+ * @param {Number} numner of bytes
+ */
+function human_size (size) {
+ var units = _t("Bytes,Kb,Mb,Gb,Tb,Pb,Eb,Zb,Yb").split(',');
+ var i = 0;
+ while (size >= 1024) {
+ size /= 1024;
+ ++i;
+ }
+ return size.toFixed(2) + ' ' + units[i];
+}
+
+/**
+ * performs a half up rounding with arbitrary precision, correcting for float loss of precision
+ * See the corresponding float_round() in server/tools/float_utils.py for more info
+ * @param {Number} the value to be rounded
+ * @param {Number} a precision parameter. eg: 0.01 rounds to two digits.
+ */
+function round_precision (value, precision) {
+ if (!value) {
+ return 0;
+ } else if (!precision || precision < 0) {
+ precision = 1;
+ }
+ var normalized_value = value / precision;
+ var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
+ var epsilon = Math.pow(2, epsilon_magnitude - 53);
+ normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
+ var rounded_value = Math.round(normalized_value);
+ return rounded_value * precision;
+}
+
+/**
+ * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision
+ * See the corresponding float_round() in server/tools/float_utils.py for more info
+ * @param {Number} the value to be rounded
+ * @param {Number} the number of decimals. eg: round_decimals(3.141592,2) -> 3.14
+ */
+function round_decimals (value, decimals) {
+ return round_precision(value, Math.pow(10,-decimals));
+}
+
+function float_is_zero (value, decimals) {
+ var epsilon = Math.pow(10, -decimals);
+ return Math.abs(round_precision(value, epsilon)) < epsilon;
+};
+
+function assert (bool) {
+ if (!bool) {
+ throw new Error("AssertionError");
+ }
+}
+
+var DropMisordered = Class.extend({
+ /**
+ * @constructs instance.web.DropMisordered
+ * @extends instance.web.Class
+ *
+ * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored
+ */
+ init: function (failMisordered) {
+ // local sequence number, for requests sent
+ this.lsn = 0;
+ // remote sequence number, seqnum of last received request
+ this.rsn = -1;
+ this.failMisordered = failMisordered || false;
+ },
+ /**
+ * Adds a deferred (usually an async request) to the sequencer
+ *
+ * @param {$.Deferred} deferred to ensure add
+ * @returns {$.Deferred}
+ */
+ add: function (deferred) {
+ var res = $.Deferred();
+
+ var self = this, seq = this.lsn++;
+ deferred.done(function () {
+ if (seq > self.rsn) {
+ self.rsn = seq;
+ res.resolve.apply(res, arguments);
+ } else if (self.failMisordered) {
+ res.reject();
+ }
+ }).fail(function () {
+ res.reject.apply(res, arguments);
+ });
+
+ return res.promise();
+ },
+});
+
+
+return {
+ divmod: divmod,
+ modf: modf,
+ lpad: lpad,
+ rpad: rpad,
+ generate_id: generate_id,
+ intersperse: intersperse,
+ xml_to_json: xml_to_json,
+ json_node_to_xml: json_node_to_xml,
+ xml_to_str: xml_to_str,
+ Mutex: Mutex,
+ get_cookie: get_cookie,
+ insert_thousand_seps: insert_thousand_seps,
+ is_bin_size: is_bin_size,
+ binary_to_binsize: binary_to_binsize,
+ human_size: human_size,
+ round_precision: round_precision,
+ round_decimals: round_decimals,
+ float_is_zero: float_is_zero,
+ assert: assert,
+ DropMisordered: DropMisordered,
+};
+
+});
+
diff --git a/addons/web/static/src/js/framework/view.js b/addons/web/static/src/js/framework/view.js
new file mode 100644
index 00000000000..c315e53d710
--- /dev/null
+++ b/addons/web/static/src/js/framework/view.js
@@ -0,0 +1,239 @@
+odoo.define('web.View', ['web.core', 'web.data', 'web.pyeval', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var pyeval = require('web.pyeval');
+var Widget = require('web.Widget');
+
+var View = Widget.extend({
+ // name displayed in view switchers
+ display_name: '',
+ /**
+ * Define a view type for each view to allow automatic call to fields_view_get.
+ */
+ view_type: undefined,
+ init: function(parent, dataset, view_id, options) {
+ this._super(parent);
+ this.ViewManager = parent;
+ this.dataset = dataset;
+ this.view_id = view_id;
+ this.$buttons = options && options.$buttons;
+ this.set_default_options(options);
+ },
+ start: function () {
+ return this.load_view();
+ },
+ load_view: function() {
+ var self = this;
+ var view_loaded_def;
+ if (this.embedded_view) {
+ view_loaded_def = $.Deferred();
+ $.async_when().done(function() {
+ view_loaded_def.resolve(self.embedded_view);
+ });
+ } else {
+ if (! this.view_type)
+ console.warn("view_type is not defined", this);
+ view_loaded_def = this.dataset._model.fields_view_get({
+ "view_id": this.view_id,
+ "view_type": this.view_type,
+ "toolbar": !!this.options.$sidebar,
+ "context": this.dataset.get_context(),
+ });
+ }
+ return this.alive(view_loaded_def).then(function(r) {
+ self.fields_view = r;
+ // add css classes that reflect the (absence of) access rights
+ self.$el.addClass('oe_view')
+ .toggleClass('oe_cannot_create', !self.is_action_enabled('create'))
+ .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit'))
+ .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete'));
+ return $.when(self.view_loading(r)).then(function() {
+ self.trigger('view_loaded', r);
+ });
+ });
+ },
+ view_loading: function(r) {
+ },
+ set_default_options: function(options) {
+ this.options = options || {};
+ _.defaults(this.options, {
+ // All possible views options should be defaulted here
+ $sidebar: null,
+ sidebar_id: null,
+ action: null,
+ action_views_ids: {}
+ });
+ },
+ /**
+ * Fetches and executes the action identified by ``action_data``.
+ *
+ * @param {Object} action_data the action descriptor data
+ * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
+ * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
+ * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
+ * @param {Object} [action_data.context=null] additional action context, to add to the current context
+ * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
+ * @param {Object} [record_id] the identifier of the object on which the action is to be applied
+ * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
+ */
+ do_execute_action: function (action_data, dataset, record_id, on_closed) {
+ var self = this;
+ var result_handler = function () {
+ if (on_closed) { on_closed.apply(null, arguments); }
+ if (self.getParent() && self.getParent().on_action_executed) {
+ return self.getParent().on_action_executed.apply(null, arguments);
+ }
+ };
+ var context = new data.CompoundContext(dataset.get_context(), action_data.context || {});
+
+ // response handler
+ var handler = function (action) {
+ if (action && action.constructor == Object) {
+ // filter out context keys that are specific to the current action.
+ // Wrong default_* and search_default_* values will no give the expected result
+ // Wrong group_by values will simply fail and forbid rendering of the destination view
+ var ncontext = new data.CompoundContext(
+ _.object(_.reject(_.pairs(dataset.get_context().eval()), function(pair) {
+ return pair[0].match('^(?:(?:default_|search_default_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
+ }))
+ );
+ ncontext.add(action_data.context || {});
+ ncontext.add({active_model: dataset.model});
+ if (record_id) {
+ ncontext.add({
+ active_id: record_id,
+ active_ids: [record_id],
+ });
+ }
+ ncontext.add(action.context || {});
+ action.context = ncontext;
+ return self.do_action(action, {
+ on_close: result_handler,
+ });
+ } else {
+ self.do_action({"type":"ir.actions.act_window_close"});
+ return result_handler();
+ }
+ };
+
+ if (action_data.special === 'cancel') {
+ return handler({"type":"ir.actions.act_window_close"});
+ } else if (action_data.type=="object") {
+ var args = [[record_id]];
+ if (action_data.args) {
+ try {
+ // Warning: quotes and double quotes problem due to json and xml clash
+ // Maybe we should force escaping in xml or do a better parse of the args array
+ var additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
+ args = args.concat(additional_args);
+ } catch(e) {
+ console.error("Could not JSON.parse arguments", action_data.args);
+ }
+ }
+ args.push(context);
+ return dataset.call_button(action_data.name, args).then(handler).then(function () {
+ core.bus.trigger('do_reload_needaction');
+ });
+ } else if (action_data.type=="action") {
+ return this.rpc('/web/action/load', {
+ action_id: action_data.name,
+ context: _.extend(pyeval.eval('context', context), {'active_model': dataset.model, 'active_ids': dataset.ids, 'active_id': record_id}),
+ do_not_eval: true
+ }).then(handler);
+ } else {
+ return dataset.exec_workflow(record_id, action_data.name).then(handler);
+ }
+ },
+ /**
+ * Directly set a view to use instead of calling fields_view_get. This method must
+ * be called before start(). When an embedded view is set, underlying implementations
+ * of instance.web.View must use the provided view instead of any other one.
+ *
+ * @param embedded_view A view.
+ */
+ set_embedded_view: function(embedded_view) {
+ this.embedded_view = embedded_view;
+ },
+ do_show: function () {
+ var self = this;
+ if (this.$buttons) {
+ this.$buttons.show();
+ }
+ this.$el.show();
+ setTimeout(function () {
+ self.$el.parent().addClass('in');
+ }, 0);
+
+ core.bus.trigger('view_shown', this);
+ },
+ do_hide: function () {
+ if (this.$buttons) {
+ this.$buttons.hide();
+ }
+ this.$el.parent().removeClass('in');
+ this.$el.hide();
+ },
+ is_active: function () {
+ return this.ViewManager.active_view.controller === this;
+ }, /**
+ * Wraps fn to only call it if the current view is the active one. If the
+ * current view is not active, doesn't call fn.
+ *
+ * fn can not return anything, as a non-call to fn can't return anything
+ * either
+ *
+ * @param {Function} fn function to wrap in the active guard
+ */
+ guard_active: function (fn) {
+ var self = this;
+ return function () {
+ if (self.is_active()) {
+ fn.apply(self, arguments);
+ }
+ };
+ },
+ do_push_state: function(state) {
+ if (this.getParent() && this.getParent().do_push_state) {
+ this.getParent().do_push_state(state);
+ }
+ },
+ do_load_state: function (state, warm) {
+
+ },
+ /**
+ * Switches to a specific view type
+ */
+ do_switch_view: function() {
+ this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments)));
+ },
+ do_search: function(domain, context, group_by) {
+ },
+ sidebar_eval_context: function () {
+ return $.when({});
+ },
+ /**
+ * Asks the view to reload itself, if the reloading is asynchronous should
+ * return a {$.Deferred} indicating when the reloading is done.
+ */
+ reload: function () {
+ return $.when();
+ },
+ /**
+ * Return whether the user can perform the action ('create', 'edit', 'delete') in this view.
+ * An action is disabled by setting the corresponding attribute in the view's main element,
+ * like: <form string="" create="false" edit="false" delete="false">
+ */
+ is_action_enabled: function(action) {
+ var attrs = this.fields_view.arch.attrs;
+ return (action in attrs) ? JSON.parse(attrs[action]) : true;
+ },
+ get_context: function () {
+ return {};
+ },
+});
+
+return View;
+
+});
diff --git a/addons/web/static/src/js/framework/widget.js b/addons/web/static/src/js/framework/widget.js
new file mode 100644
index 00000000000..6d691f812e0
--- /dev/null
+++ b/addons/web/static/src/js/framework/widget.js
@@ -0,0 +1,367 @@
+odoo.define('web.Widget', ['web.core', 'web.session'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var session = require('web.session');
+
+var qweb = core.qweb;
+var mixins = core.mixins;
+
+/**
+ * Base class for all visual components. Provides a lot of functionalities helpful
+ * for the management of a part of the DOM.
+ *
+ * Widget handles:
+ * - Rendering with QWeb.
+ * - Life-cycle management and parenting (when a parent is destroyed, all its children are
+ * destroyed too).
+ * - Insertion in DOM.
+ *
+ * Guide to create implementations of the Widget class:
+ * ==============================================
+ *
+ * Here is a sample child class:
+ *
+ * MyWidget = openerp.base.Widget.extend({
+ * // the name of the QWeb template to use for rendering
+ * template: "MyQWebTemplate",
+ *
+ * init: function(parent) {
+ * this._super(parent);
+ * // stuff that you want to init before the rendering
+ * },
+ * start: function() {
+ * // stuff you want to make after the rendering, `this.$el` holds a correct value
+ * this.$el.find(".my_button").click(/* an example of event binding * /);
+ *
+ * // if you have some asynchronous operations, it's a good idea to return
+ * // a promise in start()
+ * var promise = this.rpc(...);
+ * return promise;
+ * }
+ * });
+ *
+ * Now this class can simply be used with the following syntax:
+ *
+ * var my_widget = new MyWidget(this);
+ * my_widget.appendTo($(".some-div"));
+ *
+ * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
+ * DOM inside the ".some-div" div and its events were binded.
+ *
+ * And of course, when you don't need that widget anymore, just do:
+ *
+ * my_widget.destroy();
+ *
+ * That will kill the widget in a clean way and erase its content from the dom.
+ */
+
+
+var Widget = core.Class.extend(mixins.PropertiesMixin, {
+ // Backbone-ish API
+ tagName: 'div',
+ id: null,
+ className: null,
+ attributes: {},
+ events: {},
+ /**
+ * The name of the QWeb template that will be used for rendering. Must be
+ * redefined in subclasses or the default render() method can not be used.
+ *
+ * @type string
+ */
+ template: null,
+ /**
+ * Constructs the widget and sets its parent if a parent is given.
+ *
+ * @constructs openerp.Widget
+ *
+ * @param {openerp.Widget} parent Binds the current instance to the given Widget instance.
+ * When that widget is destroyed by calling destroy(), the current instance will be
+ * destroyed too. Can be null.
+ */
+ init: function(parent) {
+ mixins.PropertiesMixin.init.call(this);
+ this.setParent(parent);
+ // Bind on_/do_* methods to this
+ // We might remove this automatic binding in the future
+ for (var name in this) {
+ if(typeof(this[name]) == "function") {
+ if((/^on_|^do_/).test(name)) {
+ this[name] = _.bind(this[name], this);
+ }
+ }
+ }
+ // FIXME: this should not be
+ this.setElement(this._make_descriptive());
+ },
+ /**
+ * Destroys the current widget, also destroys all its children before destroying itself.
+ */
+ destroy: function() {
+ _.each(this.getChildren(), function(el) {
+ el.destroy();
+ });
+ if(this.$el) {
+ this.$el.remove();
+ }
+ mixins.PropertiesMixin.destroy.call(this);
+ },
+ /**
+ * Renders the current widget and appends it to the given jQuery object or Widget.
+ *
+ * @param target A jQuery object or a Widget instance.
+ */
+ appendTo: function(target) {
+ var self = this;
+ return this.__widgetRenderAndInsert(function(t) {
+ self.$el.appendTo(t);
+ }, target);
+ },
+ /**
+ * Renders the current widget and prepends it to the given jQuery object or Widget.
+ *
+ * @param target A jQuery object or a Widget instance.
+ */
+ prependTo: function(target) {
+ var self = this;
+ return this.__widgetRenderAndInsert(function(t) {
+ self.$el.prependTo(t);
+ }, target);
+ },
+ /**
+ * Renders the current widget and inserts it after to the given jQuery object or Widget.
+ *
+ * @param target A jQuery object or a Widget instance.
+ */
+ insertAfter: function(target) {
+ var self = this;
+ return this.__widgetRenderAndInsert(function(t) {
+ self.$el.insertAfter(t);
+ }, target);
+ },
+ /**
+ * Renders the current widget and inserts it before to the given jQuery object or Widget.
+ *
+ * @param target A jQuery object or a Widget instance.
+ */
+ insertBefore: function(target) {
+ var self = this;
+ return this.__widgetRenderAndInsert(function(t) {
+ self.$el.insertBefore(t);
+ }, target);
+ },
+ /**
+ * Renders the current widget and replaces the given jQuery object.
+ *
+ * @param target A jQuery object or a Widget instance.
+ */
+ replace: function(target) {
+ return this.__widgetRenderAndInsert(_.bind(function(t) {
+ this.$el.replaceAll(t);
+ }, this), target);
+ },
+ __widgetRenderAndInsert: function(insertion, target) {
+ this.renderElement();
+ insertion(target);
+ return this.start();
+ },
+ /**
+ * Method called after rendering. Mostly used to bind actions, perform asynchronous
+ * calls, etc...
+ *
+ * By convention, this method should return an object that can be passed to $.when()
+ * to inform the caller when this widget has been initialized.
+ *
+ * @returns {jQuery.Deferred or any}
+ */
+ start: function() {
+ return $.when();
+ },
+ /**
+ * Renders the element. The default implementation renders the widget using QWeb,
+ * `this.template` must be defined. The context given to QWeb contains the "widget"
+ * key that references `this`.
+ */
+ renderElement: function() {
+ var $el;
+ if (this.template) {
+ $el = $(qweb.render(this.template, {widget: this}).trim());
+ } else {
+ $el = this._make_descriptive();
+ }
+ this.replaceElement($el);
+ },
+ /**
+ * Re-sets the widget's root element and replaces the old root element
+ * (if any) by the new one in the DOM.
+ *
+ * @param {HTMLElement | jQuery} $el
+ * @returns {*} this
+ */
+ replaceElement: function ($el) {
+ var $oldel = this.$el;
+ this.setElement($el);
+ if ($oldel && !$oldel.is(this.$el)) {
+ if ($oldel.length > 1) {
+ $oldel.wrapAll('<div/>');
+ $oldel.parent().replaceWith(this.$el);
+ } else {
+ $oldel.replaceWith(this.$el);
+ }
+ }
+ return this;
+ },
+ /**
+ * Re-sets the widget's root element (el/$el/$el).
+ *
+ * Includes:
+ * * re-delegating events
+ * * re-binding sub-elements
+ * * if the widget already had a root element, replacing the pre-existing
+ * element in the DOM
+ *
+ * @param {HTMLElement | jQuery} element new root element for the widget
+ * @return {*} this
+ */
+ setElement: function (element) {
+ // NB: completely useless, as WidgetMixin#init creates a $el
+ // always
+ if (this.$el) {
+ this.undelegateEvents();
+ }
+
+ this.$el = (element instanceof $) ? element : $(element);
+ this.el = this.$el[0];
+
+ this.delegateEvents();
+
+ return this;
+ },
+ /**
+ * Utility function to build small DOM elements.
+ *
+ * @param {String} tagName name of the DOM element to create
+ * @param {Object} [attributes] map of DOM attributes to set on the element
+ * @param {String} [content] HTML content to set on the element
+ * @return {Element}
+ */
+ make: function (tagName, attributes, content) {
+ var el = document.createElement(tagName);
+ if (!_.isEmpty(attributes)) {
+ $(el).attr(attributes);
+ }
+ if (content) {
+ $(el).html(content);
+ }
+ return el;
+ },
+ /**
+ * Makes a potential root element from the declarative builder of the
+ * widget
+ *
+ * @return {jQuery}
+ * @private
+ */
+ _make_descriptive: function () {
+ var attrs = _.extend({}, this.attributes || {});
+ if (this.id) { attrs.id = this.id; }
+ if (this.className) { attrs['class'] = this.className; }
+ return $(this.make(this.tagName, attrs));
+ },
+ delegateEvents: function () {
+ var events = this.events;
+ if (_.isEmpty(events)) { return; }
+
+ for(var key in events) {
+ if (!events.hasOwnProperty(key)) { continue; }
+
+ var method = this.proxy(events[key]);
+
+ var match = /^(\S+)(\s+(.*))?$/.exec(key);
+ var event = match[1];
+ var selector = match[3];
+
+ event += '.widget_events';
+ if (!selector) {
+ this.$el.on(event, method);
+ } else {
+ this.$el.on(event, selector, method);
+ }
+ }
+ },
+ undelegateEvents: function () {
+ this.$el.off('.widget_events');
+ },
+ /**
+ * Shortcut for ``this.$el.find(selector)``
+ *
+ * @param {String} selector CSS selector, rooted in $el
+ * @returns {jQuery} selector match
+ */
+ $: function(selector) {
+ if (selector === undefined)
+ return this.$el;
+ return this.$el.find(selector);
+ },
+ do_show: function () {
+ this.$el.show();
+ },
+ do_hide: function () {
+ this.$el.hide();
+ },
+ /**
+ * Proxies a method of the object, in order to keep the right ``this`` on
+ * method invocations.
+ *
+ * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
+ * even more so to ``jQuery.proxy`` with a fundamental difference: its
+ * resolution of the method being called is lazy, meaning it will use the
+ * method as it is when the proxy is called, not when the proxy is created.
+ *
+ * Other methods will fix the bound method to what it is when creating the
+ * binding/proxy, which is fine in most javascript code but problematic in
+ * OpenERP Web where developers may want to replace existing callbacks with
+ * theirs.
+ *
+ * The semantics of this precisely replace closing over the method call.
+ *
+ * @param {String|Function} method function or name of the method to invoke
+ * @returns {Function} proxied method
+ */
+ proxy: function (method) {
+ var self = this;
+ return function () {
+ var fn = (typeof method === 'string') ? self[method] : method;
+ return fn.apply(self, arguments);
+ };
+ },
+ /**
+ * Informs the action manager to do an action. This supposes that
+ * the action manager can be found amongst the ancestors of the current widget.
+ * If that's not the case this method will simply return `false`.
+ */
+ do_action: function() {
+ var parent = this.getParent();
+ if (parent) {
+ return parent.do_action.apply(parent, arguments);
+ }
+ return false;
+ },
+ do_notify: function() {
+ if (this.getParent()) {
+ return this.getParent().do_notify.apply(this,arguments);
+ }
+ return false;
+ },
+ do_warn: function(title, message) {
+ core.bus.trigger('display_notification_warning', title, message);
+ },
+ rpc: function(url, data, options) {
+ return this.alive(session.rpc(url, data, options));
+ },
+});
+
+return Widget;
+
+}); \ No newline at end of file
diff --git a/addons/web/static/src/js/menu.js b/addons/web/static/src/js/menu.js
new file mode 100644
index 00000000000..c7bf4b07828
--- /dev/null
+++ b/addons/web/static/src/js/menu.js
@@ -0,0 +1,261 @@
+odoo.define('web.Menu', ['web.core', 'web.session', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+
+var Menu = Widget.extend({
+ init: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.is_bound = $.Deferred();
+ this.data = {data:{children:[]}};
+ this.on("menu_bound", this, function() {
+ // launch the fetch of needaction counters, asynchronous
+ var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
+ var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
+ if (!_.isEmpty(all_menu_ids)) {
+ this.do_load_needaction(all_menu_ids);
+ }
+ });
+ core.bus.on('do_reload_needaction', this, this.do_reload_needaction);
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ return this.bind_menu();
+ },
+ do_reload: function() {
+ var self = this;
+ self.bind_menu();
+ },
+ bind_menu: function() {
+ var self = this;
+ this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container');
+ this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
+ this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
+ // Hide second level submenus
+ this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
+ if (self.current_menu) {
+ self.open_menu(self.current_menu);
+ }
+ this.trigger('menu_bound');
+
+ var lazyreflow = _.debounce(this.reflow.bind(this), 200);
+ core.bus.on('resize', this, function() {
+ if ($(window).width() < 768 ) {
+ lazyreflow('all_outside');
+ } else {
+ lazyreflow();
+ }
+ });
+ core.bus.trigger('resize');
+
+ this.is_bound.resolve();
+ },
+ do_load_needaction: function (menu_ids) {
+ var self = this;
+ menu_ids = _.compact(menu_ids);
+ if (_.isEmpty(menu_ids)) {
+ return $.when();
+ }
+ return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
+ self.on_needaction_loaded(r);
+ });
+ },
+ on_needaction_loaded: function(data) {
+ var self = this;
+ this.needaction_data = data;
+ _.each(this.needaction_data, function (item, menu_id) {
+ var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
+ $item.find('.badge').remove();
+ if (item.needaction_counter && item.needaction_counter > 0) {
+ $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
+ }
+ });
+ },
+ /**
+ * Reflow the menu items and dock overflowing items into a "More" menu item.
+ * Automatically called when 'menu_bound' event is triggered and on window resizing.
+ *
+ * @param {string} behavior If set to 'all_outside', all the items are displayed.
+ * If not set, only the overflowing items are hidden.
+ */
+ reflow: function(behavior) {
+ var self = this;
+ var $more_container = this.$('#menu_more_container').hide();
+ var $more = this.$('#menu_more');
+ var $systray = this.$el.parents().find('.oe_systray');
+
+ $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
+
+ // 'all_outside' beahavior should display all the items, so hide the more menu and exit
+ if (behavior === 'all_outside') {
+ // Show list of menu items
+ self.$el.show();
+ this.$el.find('li').show();
+ $more_container.hide();
+ return;
+ }
+
+ // Hide all menu items
+ var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
+ // Show list of menu items (which is empty for now since all menu items are hidden)
+ self.$el.show();
+ $toplevel_items.each(function() {
+ var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
+ self.$el.parent().children(':visible').each(function() {
+ remaining_space -= $(this).outerWidth();
+ });
+
+ if ($(this).width() >= remaining_space) {
+ return false; // the current item will be appended in more_container
+ }
+ $(this).show(); // show the current item in menu bar
+ });
+ $more.append($toplevel_items.filter(':hidden').show());
+ $more_container.toggle(!!$more.children().length);
+ // Hide toplevel item if there is only one
+ var $toplevel = self.$el.children("li:visible");
+ if ($toplevel.length === 1) {
+ $toplevel.hide();
+ }
+ },
+ /**
+ * Opens a given menu by id, as if a user had browsed to that menu by hand
+ * except does not trigger any event on the way
+ *
+ * @param {Number} id database id of the terminal menu to select
+ */
+ open_menu: function (id) {
+ this.current_menu = id;
+ session.active_id = id;
+ var $clicked_menu, $sub_menu, $main_menu;
+ $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
+ this.trigger('open_menu', id, $clicked_menu);
+
+ if (this.$secondary_menus.has($clicked_menu).length) {
+ $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
+ $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
+ } else {
+ $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
+ $main_menu = $clicked_menu;
+ }
+
+ // Activate current main menu
+ this.$el.find('.active').removeClass('active');
+ $main_menu.parent().addClass('active');
+
+ // Show current sub menu
+ this.$secondary_menus.find('.oe_secondary_menu').hide();
+ $sub_menu.show();
+
+ // Hide/Show the leftbar menu depending of the presence of sub-items
+ this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
+
+ // Activate current menu item and show parents
+ this.$secondary_menus.find('.active').removeClass('active');
+ if ($main_menu !== $clicked_menu) {
+ $clicked_menu.parents().show();
+ if ($clicked_menu.is('.oe_menu_toggler')) {
+ $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
+ } else {
+ $clicked_menu.parent().addClass('active');
+ }
+ }
+ // add a tooltip to cropped menu items
+ this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
+ $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
+ });
+ },
+ /**
+ * Call open_menu with the first menu_item matching an action_id
+ *
+ * @param {Number} id the action_id to match
+ */
+ open_action: function (id) {
+ var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
+ var menu_id = $menu.data('menu');
+ if (menu_id) {
+ this.open_menu(menu_id);
+ }
+ },
+ /**
+ * Process a click on a menu item
+ *
+ * @param {Number} id the menu_id
+ * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
+ */
+ menu_click: function(id, needaction) {
+ if (!id) { return; }
+
+ // find back the menuitem in dom to get the action
+ var $item = this.$el.find('a[data-menu=' + id + ']');
+ if (!$item.length) {
+ $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
+ }
+ var action_id = $item.data('action-id');
+ // If first level menu doesnt have action trigger first leaf
+ if (!action_id) {
+ if(this.$el.has($item).length) {
+ var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
+ var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
+ if($items.length) {
+ action_id = $items.data('action-id');
+ id = $items.data('menu');
+ }
+ }
+ }
+ if (action_id) {
+ this.trigger('menu_click', {
+ action_id: action_id,
+ needaction: needaction,
+ id: id,
+ previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
+ }, $item);
+ } else {
+ console.log('Menu no action found web test 04 will fail');
+ }
+ this.open_menu(id);
+ },
+ do_reload_needaction: function () {
+ var self = this;
+ if (self.current_menu) {
+ self.do_load_needaction([self.current_menu]).then(function () {
+ self.trigger("need_action_reloaded");
+ });
+ }
+ },
+ /**
+ * Jquery event handler for menu click
+ *
+ * @param {Event} ev the jquery event
+ */
+ on_top_menu_click: function(ev) {
+ ev.preventDefault();
+ var self = this;
+ var id = $(ev.currentTarget).data('menu');
+
+ // Fetch the menu leaves ids in order to check if they need a 'needaction'
+ var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
+ var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
+ var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
+
+ self.do_load_needaction(menu_ids).then(function () {
+ self.trigger("need_action_reloaded");
+ });
+ this.$el.parents().find(".oe_secondary_menus_container").scrollTop(0,0);
+
+ this.on_menu_click(ev);
+ },
+ on_menu_click: function(ev) {
+ ev.preventDefault();
+ var needaction = $(ev.target).is('div#menu_counter');
+ this.menu_click($(ev.currentTarget).data('menu'), needaction);
+ },
+});
+
+return Menu;
+});
diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js
deleted file mode 100644
index ce5decb6911..00000000000
--- a/addons/web/static/src/js/openerpframework.js
+++ /dev/null
@@ -1,1564 +0,0 @@
-/*
- * Copyright (c) 2012, OpenERP S.A.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
- /*
- The only dependencies of this file are underscore >= 1.3.1, jQuery >= 1.8.3 and
- QWeb >= 1.0.0 . No dependencies shall be added.
-
- This file must compile in EcmaScript 3 and work in IE7.
- */
-
-(function() {
-/* jshint es3: true */
-"use strict";
-
-function declare($, _, QWeb2) {
-var openerp = {};
-
-/**
- * Improved John Resig's inheritance, based on:
- *
- * Simple JavaScript Inheritance
- * By John Resig http://ejohn.org/
- * MIT Licensed.
- *
- * Adds "include()"
- *
- * Defines The Class object. That object can be used to define and inherit classes using
- * the extend() method.
- *
- * Example:
- *
- * var Person = openerp.Class.extend({
- * init: function(isDancing){
- * this.dancing = isDancing;
- * },
- * dance: function(){
- * return this.dancing;
- * }
- * });
- *
- * The init() method act as a constructor. This class can be instancied this way:
- *
- * var person = new Person(true);
- * person.dance();
- *
- * The Person class can also be extended again:
- *
- * var Ninja = Person.extend({
- * init: function(){
- * this._super( false );
- * },
- * dance: function(){
- * // Call the inherited version of dance()
- * return this._super();
- * },
- * swingSword: function(){
- * return true;
- * }
- * });
- *
- * When extending a class, each re-defined method can use this._super() to call the previous
- * implementation of that method.
- */
-(function() {
- var initializing = false,
- fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
- // The web Class implementation (does nothing)
- openerp.Class = function(){};
-
- /**
- * Subclass an existing class
- *
- * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
- */
- openerp.Class.extend = function() {
- var _super = this.prototype;
- // Support mixins arguments
- var args = _.toArray(arguments);
- args.unshift({});
- var prop = _.extend.apply(_,args);
-
- // Instantiate a web class (but only create the instance,
- // don't run the init constructor)
- initializing = true;
- var This = this;
- var prototype = new This();
- initializing = false;
-
- // Copy the properties over onto the new prototype
- _.each(prop, function(val, name) {
- // Check if we're overwriting an existing function
- prototype[name] = typeof prop[name] == "function" &&
- fnTest.test(prop[name]) ?
- (function(name, fn) {
- return function() {
- var tmp = this._super;
-
- // Add a new ._super() method that is the same
- // method but on the super-class
- this._super = _super[name];
-
- // The method only need to be bound temporarily, so
- // we remove it when we're done executing
- var ret = fn.apply(this, arguments);
- this._super = tmp;
-
- return ret;
- };
- })(name, prop[name]) :
- prop[name];
- });
-
- // The dummy class constructor
- function Class() {
- if(this.constructor !== openerp.Class){
- throw new Error("You can only instanciate objects with the 'new' operator");
- }
- // All construction is actually done in the init method
- this._super = null;
- if (!initializing && this.init) {
- var ret = this.init.apply(this, arguments);
- if (ret) { return ret; }
- }
- return this;
- }
- Class.include = function (properties) {
- _.each(properties, function(val, name) {
- if (typeof properties[name] !== 'function'
- || !fnTest.test(properties[name])) {
- prototype[name] = properties[name];
- } else if (typeof prototype[name] === 'function'
- && prototype.hasOwnProperty(name)) {
- prototype[name] = (function (name, fn, previous) {
- return function () {
- var tmp = this._super;
- this._super = previous;
- var ret = fn.apply(this, arguments);
- this._super = tmp;
- return ret;
- };
- })(name, properties[name], prototype[name]);
- } else if (typeof _super[name] === 'function') {
- prototype[name] = (function (name, fn) {
- return function () {
- var tmp = this._super;
- this._super = _super[name];
- var ret = fn.apply(this, arguments);
- this._super = tmp;
- return ret;
- };
- })(name, properties[name]);
- }
- });
- };
-
- // Populate our constructed prototype object
- Class.prototype = prototype;
-
- // Enforce the constructor to be what we expect
- Class.constructor = Class;
-
- // And make this class extendable
- Class.extend = this.extend;
-
- return Class;
- };
-})();
-
-// Mixins
-
-/**
- * Mixin to structure objects' life-cycles folowing a parent-children
- * relationship. Each object can a have a parent and multiple children.
- * When an object is destroyed, all its children are destroyed too releasing
- * any resource they could have reserved before.
- */
-openerp.ParentedMixin = {
- __parentedMixin : true,
- init: function() {
- this.__parentedDestroyed = false;
- this.__parentedChildren = [];
- this.__parentedParent = null;
- },
- /**
- * Set the parent of the current object. When calling this method, the
- * parent will also be informed and will return the current object
- * when its getChildren() method is called. If the current object did
- * already have a parent, it is unregistered before, which means the
- * previous parent will not return the current object anymore when its
- * getChildren() method is called.
- */
- setParent : function(parent) {
- if (this.getParent()) {
- if (this.getParent().__parentedMixin) {
- this.getParent().__parentedChildren = _.without(this
- .getParent().getChildren(), this);
- }
- }
- this.__parentedParent = parent;
- if (parent && parent.__parentedMixin) {
- parent.__parentedChildren.push(this);
- }
- },
- /**
- * Return the current parent of the object (or null).
- */
- getParent : function() {
- return this.__parentedParent;
- },
- /**
- * Return a list of the children of the current object.
- */
- getChildren : function() {
- return _.clone(this.__parentedChildren);
- },
- /**
- * Returns true if destroy() was called on the current object.
- */
- isDestroyed : function() {
- return this.__parentedDestroyed;
- },
- /**
- Utility method to only execute asynchronous actions if the current
- object has not been destroyed.
-
- @param {$.Deferred} promise The promise representing the asynchronous
- action.
- @param {bool} [reject=false] If true, the returned promise will be
- rejected with no arguments if the current
- object is destroyed. If false, the
- returned promise will never be resolved
- or rejected.
- @returns {$.Deferred} A promise that will mirror the given promise if
- everything goes fine but will either be rejected
- with no arguments or never resolved if the
- current object is destroyed.
- */
- alive: function(promise, reject) {
- var self = this;
- return $.Deferred(function (def) {
- promise.then(function () {
- if (!self.isDestroyed()) {
- def.resolve.apply(def, arguments);
- }
- }, function () {
- if (!self.isDestroyed()) {
- def.reject.apply(def, arguments);
- }
- }).always(function () {
- if (reject) {
- // noop if def already resolved or rejected
- def.reject();
- }
- // otherwise leave promise in limbo
- });
- }).promise();
- },
- /**
- * Inform the object it should destroy itself, releasing any
- * resource it could have reserved.
- */
- destroy : function() {
- _.each(this.getChildren(), function(el) {
- el.destroy();
- });
- this.setParent(undefined);
- this.__parentedDestroyed = true;
- },
- /**
- * Find the closest ancestor matching predicate
- */
- findAncestor: function (predicate) {
- var ancestor = this;
- while (!(predicate(ancestor)) && ancestor && ancestor.getParent) {
- ancestor = ancestor.getParent();
- }
- return ancestor;
- },
-};
-
-/**
- * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
- *
- * This class just handle the dispatching of events, it is not meant to be extended,
- * nor used directly. All integration with parenting and automatic unregistration of
- * events is done in EventDispatcherMixin.
- *
- * Copyright notice for the following Class:
- *
- * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
- * Backbone may be freely distributed under the MIT license.
- * For all details and documentation:
- * http://backbonejs.org
- *
- */
-var Events = openerp.Class.extend({
- on : function(events, callback, context) {
- var ev;
- events = events.split(/\s+/);
- var calls = this._callbacks || (this._callbacks = {});
- while ((ev = events.shift())) {
- var list = calls[ev] || (calls[ev] = {});
- var tail = list.tail || (list.tail = list.next = {});
- tail.callback = callback;
- tail.context = context;
- list.tail = tail.next = {};
- }
- return this;
- },
-
- off : function(events, callback, context) {
- var ev, calls, node;
- if (!events) {
- delete this._callbacks;
- } else if ((calls = this._callbacks)) {
- events = events.split(/\s+/);
- while ((ev = events.shift())) {
- node = calls[ev];
- delete calls[ev];
- if (!callback || !node)
- continue;
- while ((node = node.next) && node.next) {
- if (node.callback === callback
- && (!context || node.context === context))
- continue;
- this.on(ev, node.callback, node.context);
- }
- }
- }
- return this;
- },
-
- callbackList: function() {
- var lst = [];
- _.each(this._callbacks || {}, function(el, eventName) {
- var node = el;
- while ((node = node.next) && node.next) {
- lst.push([eventName, node.callback, node.context]);
- }
- });
- return lst;
- },
-
- trigger : function(events) {
- var event, node, calls, tail, args, all, rest;
- if (!(calls = this._callbacks))
- return this;
- all = calls['all'];
- (events = events.split(/\s+/)).push(null);
- // Save references to the current heads & tails.
- while ((event = events.shift())) {
- if (all)
- events.push({
- next : all.next,
- tail : all.tail,
- event : event
- });
- if (!(node = calls[event]))
- continue;
- events.push({
- next : node.next,
- tail : node.tail
- });
- }
- rest = Array.prototype.slice.call(arguments, 1);
- while ((node = events.pop())) {
- tail = node.tail;
- args = node.event ? [ node.event ].concat(rest) : rest;
- while ((node = node.next) !== tail) {
- node.callback.apply(node.context || this, args);
- }
- }
- return this;
- }
-});
-
-/**
- Mixin containing an event system. Events are also registered by specifying the target object
- (the object which will receive the event when it is raised). Both the event-emitting object
- and the target object store or reference to each other. This is used to correctly remove all
- reference to the event handler when any of the object is destroyed (when the destroy() method
- from ParentedMixin is called). Removing those references is necessary to avoid memory leak
- and phantom events (events which are raised and sent to a previously destroyed object).
-*/
-openerp.EventDispatcherMixin = _.extend({}, openerp.ParentedMixin, {
- __eventDispatcherMixin: true,
- init: function() {
- openerp.ParentedMixin.init.call(this);
- this.__edispatcherEvents = new Events();
- this.__edispatcherRegisteredEvents = [];
- },
- on: function(events, dest, func) {
- var self = this;
- if (typeof func !== "function") {
- throw new Error("Event handler must be a function.");
- }
- events = events.split(/\s+/);
- _.each(events, function(eventName) {
- self.__edispatcherEvents.on(eventName, func, dest);
- if (dest && dest.__eventDispatcherMixin) {
- dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
- }
- });
- return this;
- },
- off: function(events, dest, func) {
- var self = this;
- events = events.split(/\s+/);
- _.each(events, function(eventName) {
- self.__edispatcherEvents.off(eventName, func, dest);
- if (dest && dest.__eventDispatcherMixin) {
- dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
- return !(el.name === eventName && el.func === func && el.source === self);
- });
- }
- });
- return this;
- },
- trigger: function(events) {
- this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
- return this;
- },
- destroy: function() {
- var self = this;
- _.each(this.__edispatcherRegisteredEvents, function(event) {
- event.source.__edispatcherEvents.off(event.name, event.func, self);
- });
- this.__edispatcherRegisteredEvents = [];
- _.each(this.__edispatcherEvents.callbackList(), function(cal) {
- this.off(cal[0], cal[2], cal[1]);
- }, this);
- this.__edispatcherEvents.off();
- openerp.ParentedMixin.destroy.call(this);
- }
-});
-
-openerp.PropertiesMixin = _.extend({}, openerp.EventDispatcherMixin, {
- init: function() {
- openerp.EventDispatcherMixin.init.call(this);
- this.__getterSetterInternalMap = {};
- },
- set: function(arg1, arg2, arg3) {
- var map;
- var options;
- if (typeof arg1 === "string") {
- map = {};
- map[arg1] = arg2;
- options = arg3 || {};
- } else {
- map = arg1;
- options = arg2 || {};
- }
- var self = this;
- var changed = false;
- _.each(map, function(val, key) {
- var tmp = self.__getterSetterInternalMap[key];
- if (tmp === val)
- return;
- if (key === 'value' && self.field && self.field.type === 'float' && tmp && val){
- var precision = self.field.digits ? self.field.digits[1] : 2;
- if (openerp.web.float_is_zero(tmp - val, precision))
- return;
- }
- changed = true;
- self.__getterSetterInternalMap[key] = val;
- if (! options.silent)
- self.trigger("change:" + key, self, {
- oldValue: tmp,
- newValue: val
- });
- });
- if (changed)
- self.trigger("change", self);
- },
- get: function(key) {
- return this.__getterSetterInternalMap[key];
- }
-});
-
-/**
- * Base class for all visual components. Provides a lot of functionalities helpful
- * for the management of a part of the DOM.
- *
- * Widget handles:
- * - Rendering with QWeb.
- * - Life-cycle management and parenting (when a parent is destroyed, all its children are
- * destroyed too).
- * - Insertion in DOM.
- *
- * Guide to create implementations of the Widget class:
- * ==============================================
- *
- * Here is a sample child class:
- *
- * MyWidget = openerp.base.Widget.extend({
- * // the name of the QWeb template to use for rendering
- * template: "MyQWebTemplate",
- *
- * init: function(parent) {
- * this._super(parent);
- * // stuff that you want to init before the rendering
- * },
- * start: function() {
- * // stuff you want to make after the rendering, `this.$el` holds a correct value
- * this.$el.find(".my_button").click(/* an example of event binding * /);
- *
- * // if you have some asynchronous operations, it's a good idea to return
- * // a promise in start()
- * var promise = this.rpc(...);
- * return promise;
- * }
- * });
- *
- * Now this class can simply be used with the following syntax:
- *
- * var my_widget = new MyWidget(this);
- * my_widget.appendTo($(".some-div"));
- *
- * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
- * DOM inside the ".some-div" div and its events were binded.
- *
- * And of course, when you don't need that widget anymore, just do:
- *
- * my_widget.destroy();
- *
- * That will kill the widget in a clean way and erase its content from the dom.
- */
-openerp.Widget = openerp.Class.extend(openerp.PropertiesMixin, {
- // Backbone-ish API
- tagName: 'div',
- id: null,
- className: null,
- attributes: {},
- events: {},
- /**
- * The name of the QWeb template that will be used for rendering. Must be
- * redefined in subclasses or the default render() method can not be used.
- *
- * @type string
- */
- template: null,
- /**
- * Constructs the widget and sets its parent if a parent is given.
- *
- * @constructs openerp.Widget
- *
- * @param {openerp.Widget} parent Binds the current instance to the given Widget instance.
- * When that widget is destroyed by calling destroy(), the current instance will be
- * destroyed too. Can be null.
- */
- init: function(parent) {
- openerp.PropertiesMixin.init.call(this);
- this.setParent(parent);
- // Bind on_/do_* methods to this
- // We might remove this automatic binding in the future
- for (var name in this) {
- if(typeof(this[name]) == "function") {
- if((/^on_|^do_/).test(name)) {
- this[name] = _.bind(this[name], this);
- }
- }
- }
- // FIXME: this should not be
- this.setElement(this._make_descriptive());
- },
- /**
- * Destroys the current widget, also destroys all its children before destroying itself.
- */
- destroy: function() {
- _.each(this.getChildren(), function(el) {
- el.destroy();
- });
- if(this.$el) {
- this.$el.remove();
- }
- openerp.PropertiesMixin.destroy.call(this);
- },
- /**
- * Renders the current widget and appends it to the given jQuery object or Widget.
- *
- * @param target A jQuery object or a Widget instance.
- */
- appendTo: function(target) {
- var self = this;
- return this.__widgetRenderAndInsert(function(t) {
- self.$el.appendTo(t);
- }, target);
- },
- /**
- * Renders the current widget and prepends it to the given jQuery object or Widget.
- *
- * @param target A jQuery object or a Widget instance.
- */
- prependTo: function(target) {
- var self = this;
- return this.__widgetRenderAndInsert(function(t) {
- self.$el.prependTo(t);
- }, target);
- },
- /**
- * Renders the current widget and inserts it after to the given jQuery object or Widget.
- *
- * @param target A jQuery object or a Widget instance.
- */
- insertAfter: function(target) {
- var self = this;
- return this.__widgetRenderAndInsert(function(t) {
- self.$el.insertAfter(t);
- }, target);
- },
- /**
- * Renders the current widget and inserts it before to the given jQuery object or Widget.
- *
- * @param target A jQuery object or a Widget instance.
- */
- insertBefore: function(target) {
- var self = this;
- return this.__widgetRenderAndInsert(function(t) {
- self.$el.insertBefore(t);
- }, target);
- },
- /**
- * Renders the current widget and replaces the given jQuery object.
- *
- * @param target A jQuery object or a Widget instance.
- */
- replace: function(target) {
- return this.__widgetRenderAndInsert(_.bind(function(t) {
- this.$el.replaceAll(t);
- }, this), target);
- },
- __widgetRenderAndInsert: function(insertion, target) {
- this.renderElement();
- insertion(target);
- return this.start();
- },
- /**
- * Method called after rendering. Mostly used to bind actions, perform asynchronous
- * calls, etc...
- *
- * By convention, this method should return an object that can be passed to $.when()
- * to inform the caller when this widget has been initialized.
- *
- * @returns {jQuery.Deferred or any}
- */
- start: function() {
- return $.when();
- },
- /**
- * Renders the element. The default implementation renders the widget using QWeb,
- * `this.template` must be defined. The context given to QWeb contains the "widget"
- * key that references `this`.
- */
- renderElement: function() {
- var $el;
- if (this.template) {
- $el = $(openerp.qweb.render(this.template, {widget: this}).trim());
- } else {
- $el = this._make_descriptive();
- }
- this.replaceElement($el);
- },
- /**
- * Re-sets the widget's root element and replaces the old root element
- * (if any) by the new one in the DOM.
- *
- * @param {HTMLElement | jQuery} $el
- * @returns {*} this
- */
- replaceElement: function ($el) {
- var $oldel = this.$el;
- this.setElement($el);
- if ($oldel && !$oldel.is(this.$el)) {
- if ($oldel.length > 1) {
- $oldel.wrapAll('<div/>');
- $oldel.parent().replaceWith(this.$el);
- } else {
- $oldel.replaceWith(this.$el);
- }
- }
- return this;
- },
- /**
- * Re-sets the widget's root element (el/$el/$el).
- *
- * Includes:
- * * re-delegating events
- * * re-binding sub-elements
- * * if the widget already had a root element, replacing the pre-existing
- * element in the DOM
- *
- * @param {HTMLElement | jQuery} element new root element for the widget
- * @return {*} this
- */
- setElement: function (element) {
- // NB: completely useless, as WidgetMixin#init creates a $el
- // always
- if (this.$el) {
- this.undelegateEvents();
- }
-
- this.$el = (element instanceof $) ? element : $(element);
- this.el = this.$el[0];
-
- this.delegateEvents();
-
- return this;
- },
- /**
- * Utility function to build small DOM elements.
- *
- * @param {String} tagName name of the DOM element to create
- * @param {Object} [attributes] map of DOM attributes to set on the element
- * @param {String} [content] HTML content to set on the element
- * @return {Element}
- */
- make: function (tagName, attributes, content) {
- var el = document.createElement(tagName);
- if (!_.isEmpty(attributes)) {
- $(el).attr(attributes);
- }
- if (content) {
- $(el).html(content);
- }
- return el;
- },
- /**
- * Makes a potential root element from the declarative builder of the
- * widget
- *
- * @return {jQuery}
- * @private
- */
- _make_descriptive: function () {
- var attrs = _.extend({}, this.attributes || {});
- if (this.id) { attrs.id = this.id; }
- if (this.className) { attrs['class'] = this.className; }
- return $(this.make(this.tagName, attrs));
- },
- delegateEvents: function () {
- var events = this.events;
- if (_.isEmpty(events)) { return; }
-
- for(var key in events) {
- if (!events.hasOwnProperty(key)) { continue; }
-
- var method = this.proxy(events[key]);
-
- var match = /^(\S+)(\s+(.*))?$/.exec(key);
- var event = match[1];
- var selector = match[3];
-
- event += '.widget_events';
- if (!selector) {
- this.$el.on(event, method);
- } else {
- this.$el.on(event, selector, method);
- }
- }
- },
- undelegateEvents: function () {
- this.$el.off('.widget_events');
- },
- /**
- * Shortcut for ``this.$el.find(selector)``
- *
- * @param {String} selector CSS selector, rooted in $el
- * @returns {jQuery} selector match
- */
- $: function(selector) {
- if (selector === undefined)
- return this.$el;
- return this.$el.find(selector);
- },
- do_show: function () {
- this.$el.show();
- },
- do_hide: function () {
- this.$el.hide();
- },
- /**
- * Proxies a method of the object, in order to keep the right ``this`` on
- * method invocations.
- *
- * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
- * even more so to ``jQuery.proxy`` with a fundamental difference: its
- * resolution of the method being called is lazy, meaning it will use the
- * method as it is when the proxy is called, not when the proxy is created.
- *
- * Other methods will fix the bound method to what it is when creating the
- * binding/proxy, which is fine in most javascript code but problematic in
- * OpenERP Web where developers may want to replace existing callbacks with
- * theirs.
- *
- * The semantics of this precisely replace closing over the method call.
- *
- * @param {String|Function} method function or name of the method to invoke
- * @returns {Function} proxied method
- */
- proxy: function (method) {
- var self = this;
- return function () {
- var fn = (typeof method === 'string') ? self[method] : method;
- return fn.apply(self, arguments);
- };
- }
-});
-
-var genericJsonRpc = function(fct_name, params, fct) {
- var data = {
- jsonrpc: "2.0",
- method: fct_name,
- params: params,
- id: Math.floor(Math.random() * 1000 * 1000 * 1000)
- };
- var xhr = fct(data);
- var result = xhr.pipe(function(result) {
- if (result.error !== undefined) {
- console.error("Server application error", result.error);
- return $.Deferred().reject("server", result.error);
- } else {
- return result.result;
- }
- }, function() {
- //console.error("JsonRPC communication error", _.toArray(arguments));
- var def = $.Deferred();
- return def.reject.apply(def, ["communication"].concat(_.toArray(arguments)));
- });
- // FIXME: jsonp?
- result.abort = function () { xhr.abort && xhr.abort(); };
- return result;
-};
-
-/**
- * Replacer function for JSON.stringify, serializes Date objects to UTC
- * datetime in the OpenERP Server format.
- *
- * However, if a serialized value has a toJSON method that method is called
- * *before* the replacer is invoked. Date#toJSON exists, and thus the value
- * passed to the replacer is a string, the original Date has to be fetched
- * on the parent object (which is provided as the replacer's context).
- *
- * @param {String} k
- * @param {Object} v
- * @returns {Object}
- */
-function date_to_utc(k, v) {
- var value = this[k];
- if (!(value instanceof Date)) { return v; }
-
- return openerp.datetime_to_str(value);
-}
-
-openerp.jsonRpc = function(url, fct_name, params, settings) {
- return genericJsonRpc(fct_name, params, function(data) {
- return $.ajax(url, _.extend({}, settings, {
- url: url,
- dataType: 'json',
- type: 'POST',
- data: JSON.stringify(data, date_to_utc),
- contentType: 'application/json'
- }));
- });
-};
-
-openerp.jsonpRpc = function(url, fct_name, params, settings) {
- settings = settings || {};
- return genericJsonRpc(fct_name, params, function(data) {
- var payload_str = JSON.stringify(data, date_to_utc);
- var payload_url = $.param({r:payload_str});
- var force2step = settings.force2step || false;
- delete settings.force2step;
- var session_id = settings.session_id || null;
- delete settings.session_id;
- if (payload_url.length < 2000 && ! force2step) {
- return $.ajax(url, _.extend({}, settings, {
- url: url,
- dataType: 'jsonp',
- jsonp: 'jsonp',
- type: 'GET',
- cache: false,
- data: {r: payload_str, session_id: session_id}
- }));
- } else {
- var args = {session_id: session_id, id: data.id};
- var ifid = _.uniqueId('oe_rpc_iframe');
- var html = "<iframe src='javascript:false;' name='" + ifid + "' id='" + ifid + "' style='display:none'></iframe>";
- var $iframe = $(html);
- var nurl = 'jsonp=1&' + $.param(args);
- nurl = url.indexOf("?") !== -1 ? url + "&" + nurl : url + "?" + nurl;
- var $form = $('<form>')
- .attr('method', 'POST')
- .attr('target', ifid)
- .attr('enctype', "multipart/form-data")
- .attr('action', nurl)
- .append($('<input type="hidden" name="r" />').attr('value', payload_str))
- .hide()
- .appendTo($('body'));
- var cleanUp = function() {
- if ($iframe) {
- $iframe.unbind("load").remove();
- }
- $form.remove();
- };
- var deferred = $.Deferred();
- // the first bind is fired up when the iframe is added to the DOM
- $iframe.bind('load', function() {
- // the second bind is fired up when the result of the form submission is received
- $iframe.unbind('load').bind('load', function() {
- $.ajax({
- url: url,
- dataType: 'jsonp',
- jsonp: 'jsonp',
- type: 'GET',
- cache: false,
- data: {session_id: session_id, id: data.id}
- }).always(function() {
- cleanUp();
- }).done(function() {
- deferred.resolve.apply(deferred, arguments);
- }).fail(function() {
- deferred.reject.apply(deferred, arguments);
- });
- });
- // now that the iframe can receive data, we fill and submit the form
- $form.submit();
- });
- // append the iframe to the DOM (will trigger the first load)
- $form.after($iframe);
- if (settings.timeout) {
- realSetTimeout(function() {
- deferred.reject({});
- }, settings.timeout);
- }
- return deferred;
- }
- });
-};
-
-openerp.loadCSS = function (url) {
- if (!$('link[href="' + url + '"]').length) {
- $('head').append($('<link>', {
- 'href': url,
- 'rel': 'stylesheet',
- 'type': 'text/css'
- }));
- }
-};
-openerp.loadJS = function (url) {
- var def = $.Deferred();
- if ($('script[src="' + url + '"]').length) {
- def.resolve();
- } else {
- var script = document.createElement('script');
- script.type = 'text/javascript';
- script.src = url;
- script.onload = script.onreadystatechange = function() {
- if ((script.readyState && script.readyState != "loaded" && script.readyState != "complete") || script.onload_done) {
- return;
- }
- script.onload_done = true;
- def.resolve(url);
- };
- script.onerror = function () {
- console.error("Error loading file", script.src);
- def.reject(url);
- };
- var head = document.head || document.getElementsByTagName('head')[0];
- head.appendChild(script);
- }
- return def;
-};
-openerp.loadBundle = function (name) {
- return $.when(
- openerp.loadCSS('/web/css/' + name),
- openerp.loadJS('/web/js/' + name)
- );
-};
-
-var realSetTimeout = function(fct, millis) {
- var finished = new Date().getTime() + millis;
- var wait = function() {
- var current = new Date().getTime();
- if (current < finished) {
- setTimeout(wait, finished - current);
- } else {
- fct();
- }
- };
- setTimeout(wait, millis);
-};
-
-openerp.Session = openerp.Class.extend(openerp.PropertiesMixin, {
- triggers: {
- 'request': 'Request sent',
- 'response': 'Response received',
- 'response_failed': 'HTTP Error response or timeout received',
- 'error': 'The received response is an JSON-RPC error'
- },
- /**
- @constructs openerp.Session
-
- @param parent The parent of the newly created object.
- @param {String} origin Url of the OpenERP server to contact with this session object
- or `null` if the server to contact is the origin server.
- @param {Dict} options A dictionary that can contain the following options:
-
- * "override_session": Default to false. If true, the current session object will
- not try to re-use a previously created session id stored in a cookie.
- * "session_id": Default to null. If specified, the specified session_id will be used
- by this session object. Specifying this option automatically implies that the option
- "override_session" is set to true.
- */
- init: function(parent, origin, options) {
- openerp.PropertiesMixin.init.call(this, parent);
- options = options || {};
- this.server = null;
- this.session_id = options.session_id || null;
- this.override_session = options.override_session || !!options.session_id || false;
- this.avoid_recursion = false;
- this.use_cors = options.use_cors || false;
- this.setup(origin);
- },
- setup: function(origin) {
- // must be able to customize server
- var window_origin = location.protocol + "//" + location.host;
- origin = origin ? origin.replace( /\/+$/, '') : window_origin;
- if (!_.isUndefined(this.origin) && this.origin !== origin)
- throw new Error('Session already bound to ' + this.origin);
- else
- this.origin = origin;
- this.prefix = this.origin;
- this.server = this.origin; // keep chs happy
- this.origin_server = this.origin === window_origin;
- },
- /**
- * (re)loads the content of a session: db name, username, user id, session
- * context and status of the support contract
- *
- * @returns {$.Deferred} deferred indicating the session is done reloading
- */
- session_reload: function () {
- var self = this;
- return self.rpc("/web/session/get_session_info", {}).then(function(result) {
- delete result.session_id;
- _.extend(self, result);
- });
- },
- /**
- * The session is validated either by login or by restoration of a previous session
- */
- session_authenticate: function(db, login, password) {
- var self = this;
- var params = {db: db, login: login, password: password};
- return this.rpc("/web/session/authenticate", params).then(function(result) {
- if (!result.uid) {
- return $.Deferred().reject();
- }
- delete result.session_id;
- _.extend(self, result);
- });
- },
- check_session_id: function() {
- var self = this;
- if (this.avoid_recursion)
- return $.when();
- if (this.session_id)
- return $.when(); // we already have the session id
- if (!this.use_cors && (this.override_session || ! this.origin_server)) {
- // If we don't use the origin server we consider we should always create a new session.
- // Even if some browsers could support cookies when using jsonp that behavior is
- // not consistent and the browser creators are tending to removing that feature.
- this.avoid_recursion = true;
- return this.rpc("/gen_session_id", {}).then(function(result) {
- self.session_id = result;
- }).always(function() {
- self.avoid_recursion = false;
- });
- } else {
- // normal use case, just use the cookie
- self.session_id = openerp.get_cookie("session_id");
- return $.when();
- }
- },
- /**
- * Executes an RPC call, registering the provided callbacks.
- *
- * Registers a default error callback if none is provided, and handles
- * setting the correct session id and session context in the parameter
- * objects
- *
- * @param {String} url RPC endpoint
- * @param {Object} params call parameters
- * @param {Object} options additional options for rpc call
- * @param {Function} success_callback function to execute on RPC call success
- * @param {Function} error_callback function to execute on RPC call failure
- * @returns {jQuery.Deferred} jquery-provided ajax deferred
- */
- rpc: function(url, params, options) {
- var self = this;
- options = _.clone(options || {});
- var shadow = options.shadow || false;
- delete options.shadow;
-
- return self.check_session_id().then(function() {
- // TODO: remove
- if (! _.isString(url)) {
- _.extend(options, url);
- url = url.url;
- }
- // TODO correct handling of timeouts
- if (! shadow)
- self.trigger('request');
- var fct;
- if (self.origin_server) {
- fct = openerp.jsonRpc;
- if (self.override_session) {
- options.headers = _.extend({}, options.headers, {
- "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
- });
- }
- } else if (self.use_cors) {
- fct = openerp.jsonRpc;
- url = self.url(url, null);
- options.session_id = self.session_id || '';
- if (self.override_session) {
- options.headers = _.extend({}, options.headers, {
- "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
- });
- }
- } else {
- fct = openerp.jsonpRpc;
- url = self.url(url, null);
- options.session_id = self.session_id || '';
- }
- var p = fct(url, "call", params, options);
- p = p.then(function (result) {
- if (! shadow)
- self.trigger('response');
- return result;
- }, function(type, error, textStatus, errorThrown) {
- if (type === "server") {
- if (! shadow)
- self.trigger('response');
- if (error.code === 100) {
- self.uid = false;
- }
- return $.Deferred().reject(error, $.Event());
- } else {
- if (! shadow)
- self.trigger('response_failed');
- var nerror = {
- code: -32098,
- message: "XmlHttpRequestError " + errorThrown,
- data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
- };
- return $.Deferred().reject(nerror, $.Event());
- }
- });
- return p.fail(function() { // Allow deferred user to disable rpc_error call in fail
- p.fail(function(error, event) {
- if (!event.isDefaultPrevented()) {
- self.trigger('error', error, event);
- }
- });
- });
- });
- },
- url: function(path, params) {
- params = _.extend(params || {});
- if (this.override_session || (! this.origin_server))
- params.session_id = this.session_id;
- var qs = $.param(params);
- if (qs.length > 0)
- qs = "?" + qs;
- var prefix = _.any(['http://', 'https://', '//'], function(el) {
- return path.length >= el.length && path.slice(0, el.length) === el;
- }) ? '' : this.prefix;
- return prefix + path + qs;
- },
- model: function(model_name) {
- return new openerp.Model(this, model_name);
- }
-});
-
-openerp.Model = openerp.Class.extend({
- /**
- new openerp.Model([session,] model_name)
-
- @constructs instance.Model
- @extends instance.Class
-
- @param {openerp.Session} [session] The session object used to communicate with
- the server.
- @param {String} model_name name of the OpenERP model this object is bound to
- @param {Object} [context]
- @param {Array} [domain]
- */
- init: function () {
- var session, model_name;
- var args = _.toArray(arguments);
- args.reverse();
- session = args.pop();
- if (session && ! (session instanceof openerp.Session)) {
- model_name = session;
- session = null;
- } else {
- model_name = args.pop();
- }
-
- this.name = model_name;
- this._session = session;
- },
- session: function() {
- if (! this._session)
- throw new Error("Not session specified");
- return this._session;
- },
- /**
- * Call a method (over RPC) on the bound OpenERP model.
- *
- * @param {String} method name of the method to call
- * @param {Array} [args] positional arguments
- * @param {Object} [kwargs] keyword arguments
- * @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<>} call result
- */
- call: function (method, args, kwargs, options) {
- args = args || [];
- kwargs = kwargs || {};
- if (!_.isArray(args)) {
- // call(method, kwargs)
- kwargs = args;
- args = [];
- }
- var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
- return this.session().rpc(call_kw, {
- model: this.name,
- method: method,
- args: args,
- kwargs: kwargs
- }, options);
- }
-});
-
-/** OpenERP Translations */
-openerp.TranslationDataBase = openerp.Class.extend(/** @lends instance.TranslationDataBase# */{
- /**
- * @constructs instance.TranslationDataBase
- * @extends instance.Class
- */
- init: function() {
- this.db = {};
- this.parameters = {"direction": 'ltr',
- "date_format": '%m/%d/%Y',
- "time_format": '%H:%M:%S',
- "grouping": [],
- "decimal_point": ".",
- "thousands_sep": ","};
- },
- set_bundle: function(translation_bundle) {
- var self = this;
- this.db = {};
- var modules = _.keys(translation_bundle.modules);
- modules.sort();
- if (_.include(modules, "web")) {
- modules = ["web"].concat(_.without(modules, "web"));
- }
- _.each(modules, function(name) {
- self.add_module_translation(translation_bundle.modules[name]);
- });
- if (translation_bundle.lang_parameters) {
- this.parameters = translation_bundle.lang_parameters;
- }
- },
- add_module_translation: function(mod) {
- var self = this;
- _.each(mod.messages, function(message) {
- self.db[message.id] = message.string;
- });
- },
- build_translation_function: function() {
- var self = this;
- var fcnt = function(str) {
- var tmp = self.get(str);
- return tmp === undefined ? str : tmp;
- };
- fcnt.database = this;
- return fcnt;
- },
- get: function(key) {
- return this.db[key];
- },
- /**
- Loads the translations from an OpenERP server.
-
- @param {openerp.Session} session The session object to contact the server.
- @param {Array} [modules] The list of modules to load the translation. If not specified,
- it will default to all the modules installed in the current database.
- @param {Object} [lang] lang The language. If not specified it will default to the language
- of the current user.
- @returns {jQuery.Deferred}
- */
- load_translations: function(session, modules, lang) {
- var self = this;
- return session.rpc('/web/webclient/translations', {
- "mods": modules || null,
- "lang": lang || null
- }).done(function(trans) {
- self.set_bundle(trans);
- });
- }
-});
-
-openerp._t = new openerp.TranslationDataBase().build_translation_function();
-
-openerp.get_cookie = function(c_name) {
- var cookies = document.cookie ? document.cookie.split('; ') : [];
- for (var i = 0, l = cookies.length; i < l; i++) {
- var parts = cookies[i].split('=');
- var name = parts.shift();
- var cookie = parts.join('=');
-
- if (c_name && c_name === name) {
- return cookie;
- }
- }
- return "";
-};
-
-openerp.qweb = new QWeb2.Engine();
-
-openerp.qweb.default_dict = {
- '_' : _,
- 'JSON': JSON,
- '_t' : openerp._t
-};
-
-openerp.Mutex = openerp.Class.extend({
- init: function() {
- this.def = $.Deferred().resolve();
- },
- exec: function(action) {
- var current = this.def;
- var next = this.def = $.Deferred();
- return current.then(function() {
- return $.when(action()).always(function() {
- next.resolve();
- });
- });
- }
-});
-
-/**
- * Converts a string to a Date javascript object using OpenERP's
- * datetime string format (exemple: '2011-12-01 15:12:35.832').
- *
- * The time zone is assumed to be UTC (standard for OpenERP 6.1)
- * and will be converted to the browser's time zone.
- *
- * @param {String} str A string representing a datetime.
- * @returns {Date}
- */
-openerp.str_to_datetime = function(str) {
- if(!str) {
- return str;
- }
- var regex = /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
- var res = regex.exec(str);
- if ( !res ) {
- throw new Error("'" + str + "' is not a valid datetime");
- }
- var tmp = new Date(2000,0,1);
- tmp.setUTCMonth(1970);
- tmp.setUTCMonth(0);
- tmp.setUTCDate(1);
- tmp.setUTCFullYear(parseFloat(res[1]));
- tmp.setUTCMonth(parseFloat(res[2]) - 1);
- tmp.setUTCDate(parseFloat(res[3]));
- tmp.setUTCHours(parseFloat(res[4]));
- tmp.setUTCMinutes(parseFloat(res[5]));
- tmp.setUTCSeconds(parseFloat(res[6]));
- tmp.setUTCSeconds(parseFloat(res[6]));
- tmp.setUTCMilliseconds(parseFloat(rpad((res[7] || "").slice(0, 3), 3)));
- return tmp;
-};
-
-/**
- * Converts a string to a Date javascript object using OpenERP's
- * date string format (exemple: '2011-12-01').
- *
- * As a date is not subject to time zones, we assume it should be
- * represented as a Date javascript object at 00:00:00 in the
- * time zone of the browser.
- *
- * @param {String} str A string representing a date.
- * @returns {Date}
- */
-openerp.str_to_date = function(str) {
- if(!str) {
- return str;
- }
- var regex = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
- var res = regex.exec(str);
- if ( !res ) {
- throw new Error("'" + str + "' is not a valid date");
- }
- var tmp = new Date(2000,0,1);
- tmp.setFullYear(parseFloat(res[1]));
- tmp.setMonth(parseFloat(res[2]) - 1);
- tmp.setDate(parseFloat(res[3]));
- tmp.setHours(0);
- tmp.setMinutes(0);
- tmp.setSeconds(0);
- return tmp;
-};
-
-/**
- * Converts a string to a Date javascript object using OpenERP's
- * time string format (exemple: '15:12:35').
- *
- * The OpenERP times are supposed to always be naive times. We assume it is
- * represented using a javascript Date with a date 1 of January 1970 and a
- * time corresponding to the meant time in the browser's time zone.
- *
- * @param {String} str A string representing a time.
- * @returns {Date}
- */
-openerp.str_to_time = function(str) {
- if(!str) {
- return str;
- }
- var regex = /^(\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
- var res = regex.exec(str);
- if ( !res ) {
- throw new Error("'" + str + "' is not a valid time");
- }
- var tmp = new Date();
- tmp.setFullYear(1970);
- tmp.setMonth(0);
- tmp.setDate(1);
- tmp.setHours(parseFloat(res[1]));
- tmp.setMinutes(parseFloat(res[2]));
- tmp.setSeconds(parseFloat(res[3]));
- tmp.setMilliseconds(parseFloat(rpad((res[4] || "").slice(0, 3), 3)));
- return tmp;
-};
-
-/*
- * Left-pad provided arg 1 with zeroes until reaching size provided by second
- * argument.
- *
- * @param {Number|String} str value to pad
- * @param {Number} size size to reach on the final padded value
- * @returns {String} padded string
- */
-var lpad = function(str, size) {
- str = "" + str;
- return new Array(size - str.length + 1).join('0') + str;
-};
-
-var rpad = function(str, size) {
- str = "" + str;
- return str + new Array(size - str.length + 1).join('0');
-};
-
-/**
- * Converts a Date javascript object to a string using OpenERP's
- * datetime string format (exemple: '2011-12-01 15:12:35').
- *
- * The time zone of the Date object is assumed to be the one of the
- * browser and it will be converted to UTC (standard for OpenERP 6.1).
- *
- * @param {Date} obj
- * @returns {String} A string representing a datetime.
- */
-openerp.datetime_to_str = function(obj) {
- if (!obj) {
- return false;
- }
- return lpad(obj.getUTCFullYear(),4) + "-" + lpad(obj.getUTCMonth() + 1,2) + "-"
- + lpad(obj.getUTCDate(),2) + " " + lpad(obj.getUTCHours(),2) + ":"
- + lpad(obj.getUTCMinutes(),2) + ":" + lpad(obj.getUTCSeconds(),2);
-};
-
-/**
- * Converts a Date javascript object to a string using OpenERP's
- * date string format (exemple: '2011-12-01').
- *
- * As a date is not subject to time zones, we assume it should be
- * represented as a Date javascript object at 00:00:00 in the
- * time zone of the browser.
- *
- * @param {Date} obj
- * @returns {String} A string representing a date.
- */
-openerp.date_to_str = function(obj) {
- if (!obj) {
- return false;
- }
- return lpad(obj.getFullYear(),4) + "-" + lpad(obj.getMonth() + 1,2) + "-"
- + lpad(obj.getDate(),2);
-};
-
-/**
- * Converts a Date javascript object to a string using OpenERP's
- * time string format (exemple: '15:12:35').
- *
- * The OpenERP times are supposed to always be naive times. We assume it is
- * represented using a javascript Date with a date 1 of January 1970 and a
- * time corresponding to the meant time in the browser's time zone.
- *
- * @param {Date} obj
- * @returns {String} A string representing a time.
- */
-openerp.time_to_str = function(obj) {
- if (!obj) {
- return false;
- }
- return lpad(obj.getHours(),2) + ":" + lpad(obj.getMinutes(),2) + ":"
- + lpad(obj.getSeconds(),2);
-};
-
-// jQuery custom plugins
-jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
- return function( elem ) {
- return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
- };
-});
-
-openerp.declare = declare;
-
-return openerp;
-}
-
-if (typeof(define) !== "undefined") { // amd
- define(["jquery", "underscore", "qweb2"], declare);
-} else {
- window.openerp = declare($, _, QWeb2);
-}
-
-})();
diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js
deleted file mode 100644
index c132b8365fb..00000000000
--- a/addons/web/static/src/js/pyeval.js
+++ /dev/null
@@ -1,1012 +0,0 @@
-/*
- * py.js helpers and setup
- */
-(function() {
-
- var instance = openerp;
-
- instance.web.pyeval = {};
-
- var obj = function () {};
- obj.prototype = py.object;
- var asJS = function (arg) {
- if (arg instanceof obj) {
- return arg.toJSON();
- }
- return arg;
- };
-
- var datetime = py.PY_call(py.object);
-
- /**
- * computes (Math.floor(a/b), a%b and passes that to the callback.
- *
- * returns the callback's result
- */
- var divmod = function (a, b, fn) {
- var mod = a%b;
- // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a
- // round of b
- if (mod > 0 && b < 0 || mod < 0 && b > 0) {
- mod += b;
- }
- return fn(Math.floor(a/b), mod);
- };
- /**
- * Passes the fractional and integer parts of x to the callback, returns
- * the callback's result
- */
- var modf = function (x, fn) {
- var mod = x%1;
- if (mod < 0) {
- mod += 1;
- }
- return fn(mod, Math.floor(x));
- };
- var zero = py.float.fromJSON(0);
-
- // Port from pypy/lib_pypy/datetime.py
- var DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
- var DAYS_BEFORE_MONTH = [null];
- var dbm = 0;
- for (var i=1; i<DAYS_IN_MONTH.length; ++i) {
- DAYS_BEFORE_MONTH.push(dbm);
- dbm += DAYS_IN_MONTH[i];
- }
- var is_leap = function (year) {
- return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
- };
- var days_before_year = function (year) {
- var y = year - 1;
- return y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400);
- };
- var days_in_month = function (year, month) {
- if (month === 2 && is_leap(year)) {
- return 29;
- }
- return DAYS_IN_MONTH[month];
- };
- var days_before_month = function (year, month) {
- var post_leap_feb = month > 2 && is_leap(year);
- return DAYS_BEFORE_MONTH[month]
- + (post_leap_feb ? 1 : 0);
- };
- var ymd2ord = function (year, month, day) {
- var dim = days_in_month(year, month);
- if (!(1 <= day && day <= dim)) {
- throw new Error("ValueError: day must be in 1.." + dim);
- }
- return days_before_year(year)
- + days_before_month(year, month)
- + day;
- };
- var DI400Y = days_before_year(401);
- var DI100Y = days_before_year(101);
- var DI4Y = days_before_year(5);
- var assert = function (bool) {
- if (!bool) {
- throw new Error("AssertionError");
- }
- };
- var ord2ymd = function (n) {
- --n;
- var n400, n100, n4, n1, n0;
- divmod(n, DI400Y, function (_n400, n) {
- n400 = _n400;
- divmod(n, DI100Y, function (_n100, n) {
- n100 = _n100;
- divmod(n, DI4Y, function (_n4, n) {
- n4 = _n4;
- divmod(n, 365, function (_n1, n) {
- n1 = _n1;
- n0 = n;
- });
- });
- });
- });
-
- n = n0;
- var year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;
- if (n1 == 4 || n100 == 100) {
- assert(n0 === 0);
- return {
- year: year - 1,
- month: 12,
- day: 31
- };
- }
-
- var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);
- assert(leapyear == is_leap(year));
- var month = (n + 50) >> 5;
- var preceding = DAYS_BEFORE_MONTH[month] + ((month > 2 && leapyear) ? 1 : 0);
- if (preceding > n) {
- --month;
- preceding -= DAYS_IN_MONTH[month] + ((month === 2 && leapyear) ? 1 : 0);
- }
- n -= preceding;
- return {
- year: year,
- month: month,
- day: n+1
- };
- };
-
- /**
- * Converts the stuff passed in into a valid date, applying overflows as needed
- */
- var tmxxx = function (year, month, day, hour, minute, second, microsecond) {
- hour = hour || 0; minute = minute || 0; second = second || 0;
- microsecond = microsecond || 0;
-
- if (microsecond < 0 || microsecond > 999999) {
- divmod(microsecond, 1000000, function (carry, ms) {
- microsecond = ms;
- second += carry;
- });
- }
- if (second < 0 || second > 59) {
- divmod(second, 60, function (carry, s) {
- second = s;
- minute += carry;
- });
- }
- if (minute < 0 || minute > 59) {
- divmod(minute, 60, function (carry, m) {
- minute = m;
- hour += carry;
- });
- }
- if (hour < 0 || hour > 23) {
- divmod(hour, 24, function (carry, h) {
- hour = h;
- day += carry;
- });
- }
- // That was easy. Now it gets muddy: the proper range for day
- // can't be determined without knowing the correct month and year,
- // but if day is, e.g., plus or minus a million, the current month
- // and year values make no sense (and may also be out of bounds
- // themselves).
- // Saying 12 months == 1 year should be non-controversial.
- if (month < 1 || month > 12) {
- divmod(month-1, 12, function (carry, m) {
- month = m + 1;
- year += carry;
- });
- }
- // Now only day can be out of bounds (year may also be out of bounds
- // for a datetime object, but we don't care about that here).
- // If day is out of bounds, what to do is arguable, but at least the
- // method here is principled and explainable.
- var dim = days_in_month(year, month);
- if (day < 1 || day > dim) {
- // Move day-1 days from the first of the month. First try to
- // get off cheap if we're only one day out of range (adjustments
- // for timezone alone can't be worse than that).
- if (day === 0) {
- --month;
- if (month > 0) {
- day = days_in_month(year, month);
- } else {
- --year; month=12; day=31;
- }
- } else if (day == dim + 1) {
- ++month;
- day = 1;
- if (month > 12) {
- month = 1;
- ++year;
- }
- } else {
- var r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));
- year = r.year;
- month = r.month;
- day = r.day;
- }
- }
- return {
- year: year,
- month: month,
- day: day,
- hour: hour,
- minute: minute,
- second: second,
- microsecond: microsecond
- };
- };
- datetime.timedelta = py.type('timedelta', null, {
- __init__: function () {
- var args = py.PY_parseArgs(arguments, [
- ['days', zero], ['seconds', zero], ['microseconds', zero],
- ['milliseconds', zero], ['minutes', zero], ['hours', zero],
- ['weeks', zero]
- ]);
- var d = 0, s = 0, m = 0;
- var days = args.days.toJSON() + args.weeks.toJSON() * 7;
- var seconds = args.seconds.toJSON()
- + args.minutes.toJSON() * 60
- + args.hours.toJSON() * 3600;
- var microseconds = args.microseconds.toJSON()
- + args.milliseconds.toJSON() * 1000;
-
- // Get rid of all fractions, and normalize s and us.
- // Take a deep breath <wink>.
- var daysecondsfrac = modf(days, function (dayfrac, days) {
- d = days;
- if (dayfrac) {
- return modf(dayfrac * 24 * 3600, function (dsf, dsw) {
- s = dsw;
- return dsf;
- });
- }
- return 0;
- });
-
- var secondsfrac = modf(seconds, function (sf, s) {
- seconds = s;
- return sf + daysecondsfrac;
- });
- divmod(seconds, 24*3600, function (days, seconds) {
- d += days;
- s += seconds;
- });
- // seconds isn't referenced again before redefinition
-
- microseconds += secondsfrac * 1e6;
- divmod(microseconds, 1000000, function (seconds, microseconds) {
- divmod(seconds, 24*3600, function (days, seconds) {
- d += days;
- s += seconds;
- m += Math.round(microseconds);
- });
- });
-
- // Carrying still possible here?
-
- this.days = d;
- this.seconds = s;
- this.microseconds = m;
- },
- __str__: function () {
- var hh, mm, ss;
- divmod(this.seconds, 60, function (m, s) {
- divmod(m, 60, function (h, m) {
- hh = h;
- mm = m;
- ss = s;
- });
- });
- var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss);
- if (this.days) {
- s = _.str.sprintf("%d day%s, %s",
- this.days,
- (this.days != 1 && this.days != -1) ? 's' : '',
- s);
- }
- if (this.microseconds) {
- s = _.str.sprintf("%s.%06d", s, this.microseconds);
- }
- return py.str.fromJSON(s);
- },
- __eq__: function (other) {
- if (!py.PY_isInstance(other, datetime.timedelta)) {
- return py.False;
- }
- return (this.days === other.days
- && this.seconds === other.seconds
- && this.microseconds === other.microseconds)
- ? py.True : py.False;
- },
- __add__: function (other) {
- if (!py.PY_isInstance(other, datetime.timedelta)) {
- return py.NotImplemented;
- }
- return py.PY_call(datetime.timedelta, [
- py.float.fromJSON(this.days + other.days),
- py.float.fromJSON(this.seconds + other.seconds),
- py.float.fromJSON(this.microseconds + other.microseconds)
- ]);
- },
- __radd__: function (other) { return this.__add__(other); },
- __sub__: function (other) {
- if (!py.PY_isInstance(other, datetime.timedelta)) {
- return py.NotImplemented;
- }
- return py.PY_call(datetime.timedelta, [
- py.float.fromJSON(this.days - other.days),
- py.float.fromJSON(this.seconds - other.seconds),
- py.float.fromJSON(this.microseconds - other.microseconds)
- ]);
- },
- __rsub__: function (other) {
- if (!py.PY_isInstance(other, datetime.timedelta)) {
- return py.NotImplemented;
- }
- return this.__neg__().__add__(other);
- },
- __neg__: function () {
- return py.PY_call(datetime.timedelta, [
- py.float.fromJSON(-this.days),
- py.float.fromJSON(-this.seconds),
- py.float.fromJSON(-this.microseconds)
- ]);
- },
- __pos__: function () { return this; },
- __mul__: function (other) {
- if (!py.PY_isInstance(other, py.float)) {
- return py.NotImplemented;
- }
- var n = other.toJSON();
- return py.PY_call(datetime.timedelta, [
- py.float.fromJSON(this.days * n),
- py.float.fromJSON(this.seconds * n),
- py.float.fromJSON(this.microseconds * n)
- ]);
- },
- __rmul__: function (other) { return this.__mul__(other); },
- __div__: function (other) {
- if (!py.PY_isInstance(other, py.float)) {
- return py.NotImplemented;
- }
- var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000
- + this.microseconds;
- return py.PY_call(
- datetime.timedelta, [
- zero, zero, py.float.fromJSON(usec / other.toJSON())]);
- },
- __floordiv__: function (other) { return this.__div__(other); },
- total_seconds: function () {
- return py.float.fromJSON(
- this.days * 86400
- + this.seconds
- + this.microseconds / 1000000);
- },
- __nonzero__: function () {
- return (!!this.days || !!this.seconds || !!this.microseconds)
- ? py.True
- : py.False;
- }
- });
- datetime.datetime = py.type('datetime', null, {
- __init__: function () {
- var zero = py.float.fromJSON(0);
- var args = py.PY_parseArgs(arguments, [
- 'year', 'month', 'day',
- ['hour', zero], ['minute', zero], ['second', zero],
- ['microsecond', zero], ['tzinfo', py.None]
- ]);
- for(var key in args) {
- if (!args.hasOwnProperty(key)) { continue; }
- this[key] = asJS(args[key]);
- }
- },
- replace: function () {
- var args = py.PY_parseArgs(arguments, [
- ['year', py.None], ['month', py.None], ['day', py.None],
- ['hour', py.None], ['minute', py.None], ['second', py.None],
- ['microsecond', py.None] // FIXME: tzinfo, can't use None as valid input
- ]);
- var params = {};
- for(var key in args) {
- if (!args.hasOwnProperty(key)) { continue; }
-
- var arg = args[key];
- params[key] = (arg === py.None ? this[key] : asJS(arg));
- }
- return py.PY_call(datetime.datetime, params);
- },
- strftime: function () {
- var self = this;
- var args = py.PY_parseArgs(arguments, 'format');
- return py.str.fromJSON(args.format.toJSON()
- .replace(/%([A-Za-z])/g, function (m, c) {
- switch (c) {
- case 'Y': return _.str.sprintf('%04d', self.year);
- case 'm': return _.str.sprintf('%02d', self.month);
- case 'd': return _.str.sprintf('%02d', self.day);
- case 'H': return _.str.sprintf('%02d', self.hour);
- case 'M': return _.str.sprintf('%02d', self.minute);
- case 'S': return _.str.sprintf('%02d', self.second);
- }
- throw new Error('ValueError: No known conversion for ' + m);
- }));
- },
- now: py.classmethod.fromJSON(function () {
- var d = new Date;
- return py.PY_call(datetime.datetime, [
- d.getFullYear(), d.getMonth() + 1, d.getDate(),
- d.getHours(), d.getMinutes(), d.getSeconds(),
- d.getMilliseconds() * 1000]);
- }),
- today: py.classmethod.fromJSON(function () {
- var dt_class = py.PY_getAttr(datetime, 'datetime');
- return py.PY_call(py.PY_getAttr(dt_class, 'now'));
- }),
- utcnow: py.classmethod.fromJSON(function () {
- var d = new Date();
- return py.PY_call(datetime.datetime,
- [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
- d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
- d.getUTCMilliseconds() * 1000]);
- }),
- combine: py.classmethod.fromJSON(function () {
- var args = py.PY_parseArgs(arguments, 'date time');
- return py.PY_call(datetime.datetime, [
- py.PY_getAttr(args.date, 'year'),
- py.PY_getAttr(args.date, 'month'),
- py.PY_getAttr(args.date, 'day'),
- py.PY_getAttr(args.time, 'hour'),
- py.PY_getAttr(args.time, 'minute'),
- py.PY_getAttr(args.time, 'second')
- ]);
- }),
- toJSON: function () {
- return new Date(
- this.year,
- this.month - 1,
- this.day,
- this.hour,
- this.minute,
- this.second,
- this.microsecond / 1000);
- },
- });
- datetime.date = py.type('date', null, {
- __init__: function () {
- var args = py.PY_parseArgs(arguments, 'year month day');
- this.year = asJS(args.year);
- this.month = asJS(args.month);
- this.day = asJS(args.day);
- },
- strftime: function () {
- var self = this;
- var args = py.PY_parseArgs(arguments, 'format');
- return py.str.fromJSON(args.format.toJSON()
- .replace(/%([A-Za-z])/g, function (m, c) {
- switch (c) {
- case 'Y': return self.year;
- case 'm': return _.str.sprintf('%02d', self.month);
- case 'd': return _.str.sprintf('%02d', self.day);
- }
- throw new Error('ValueError: No known conversion for ' + m);
- }));
- },
- __eq__: function (other) {
- return (this.year === other.year
- && this.month === other.month
- && this.day === other.day)
- ? py.True : py.False;
- },
- replace: function () {
- var args = py.PY_parseArgs(arguments, [
- ['year', py.None], ['month', py.None], ['day', py.None]
- ]);
- var params = {};
- for(var key in args) {
- if (!args.hasOwnProperty(key)) { continue; }
-
- var arg = args[key];
- params[key] = (arg === py.None ? this[key] : asJS(arg));
- }
- return py.PY_call(datetime.date, params);
- },
- __add__: function (other) {
- if (!py.PY_isInstance(other, datetime.timedelta)) {
- return py.NotImplemented;
- }
-
- var s = tmxxx(this.year, this.month, this.day + other.days);
- return datetime.date.fromJSON(s.year, s.month, s.day);
- },
- __radd__: function (other) { return this.__add__(other); },
- __sub__: function (other) {
- if (py.PY_isInstance(other, datetime.timedelta)) {
- return py.PY_add(this, py.PY_negative(other));
- }
- if (py.PY_isInstance(other, datetime.date)) {
- // FIXME: getattr and sub API methods
- return py.PY_call(datetime.timedelta, [
- py.PY_subtract(
- py.PY_call(py.PY_getAttr(this, 'toordinal')),
- py.PY_call(py.PY_getAttr(other, 'toordinal')))
- ]);
- }
- return py.NotImplemented;
- },
- toordinal: function () {
- return py.float.fromJSON(ymd2ord(this.year, this.month, this.day));
- },
- weekday: function () {
- return py.float.fromJSON((this.toordinal().toJSON()+6)%7);
- },
- fromJSON: function (year, month, day) {
- return py.PY_call(datetime.date, [year, month, day]);
- },
- today: py.classmethod.fromJSON(function () {
- var d = new Date;
- return py.PY_call(datetime.date, [
- d.getFullYear(), d.getMonth() + 1, d.getDate()]);
- }),
- });
- /**
- Returns the current local date, which means the date on the client (which can be different
- compared to the date of the server).
-
- @return {datetime.date}
- */
- var context_today = function() {
- var d = new Date();
- return py.PY_call(
- datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
- };
- datetime.time = py.type('time', null, {
- __init__: function () {
- var zero = py.float.fromJSON(0);
- var args = py.PY_parseArgs(arguments, [
- ['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero],
- ['tzinfo', py.None]
- ]);
-
- for(var k in args) {
- if (!args.hasOwnProperty(k)) { continue; }
- this[k] = asJS(args[k]);
- }
- }
- });
- var time = py.PY_call(py.object);
- time.strftime = py.PY_def.fromJSON(function () {
- var args = py.PY_parseArgs(arguments, 'format');
- var dt_class = py.PY_getAttr(datetime, 'datetime');
- var d = py.PY_call(py.PY_getAttr(dt_class, 'utcnow'));
- return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
- });
-
- var args = _.map(('year month day '
- + 'years months weeks days '
- + 'weekday leapdays yearday nlyearday').split(' '), function (arg) {
- switch (arg) {
- case 'years':case 'months':case 'days':case 'leapdays':case 'weeks':
- return [arg, zero];
- case 'year':case 'month':case 'day':case 'weekday':
- case 'yearday':case 'nlyearday':
- return [arg, null];
- default:
- throw new Error("Unknown relativedelta argument " + arg);
- }
- });
- args.unshift('*');
- var utils = {
- divmod: function (x, y) {
- var rem = x % y;
- return {
- div: (x - rem) / y,
- mod: rem
- };
- },
- monthrange: function (year, month) {
- if (month < 1 || month > 12) {
- throw new Error("Illegal month " + month);
- }
-
- var day1 = this.weekday(year, month, 1);
- var ndays = this.mdays[month] + (month == this.February && this.isleap(year));
- return [day1, ndays];
- },
- weekday: function (year, month, day) {
- var date = py.PY_call(datetime.date, [year, month, day]);
- return py.PY_call(py.PY_getAttr(date, 'weekday'));
- },
- isleap: function (year) {
- return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
- },
- mdays: [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
- January: 1,
- February: 2
- };
- var relativedelta = py.type('relativedelta', null, {
- __init__: function () {
- this.ops = py.PY_parseArgs(arguments, args);
- this.ops.days = py.float.fromJSON(
- asJS(this.ops.days) + asJS(this.ops.weeks) * 7
- );
-
- var yday = zero;
- if (this.ops.nlyearday) {
- yday = this.ops.nlyearday;
- } else if (this.ops.yearday) {
- yday = this.ops.yearday;
- if (asJS(this.ops.yearday) > 59) {
- this.ops.leapdays = py.float.fromJS(-1);
- }
- }
- if (py.PY_isTrue(yday)) {
- var ydayidx = [31, 59, 90, 120, 151, 181, 212,
- 243, 273, 304, 334, 366];
- for(var idx=0; idx<ydayidx.length; ++idx) {
- var ydays = ydayidx[idx];
- if (asJS(yday) <= ydays) {
- this.ops.month = py.float.fromJSON(idx+1);
- if (!idx) {
- this.ops.day = yday;
- } else {
- this.ops.day = py.PY_subtract(
- yday,
- py.float.fromJSON(ydayidx[idx-1])
- );
- }
- break;
- }
- }
- if (idx === ydayidx.length) {
- throw new Error("Invalid year day (" + asJS(yday) + ")");
- }
- }
- this._fix();
- },
- _fix: function () {
- var months = asJS(this.ops.months);
- if (Math.abs(months) > 11) {
- var s = months > 0 ? 1 : -1;
- var r = utils.divmod(months * s, 12);
- this.ops.months = py.float.fromJSON(r.mod*s);
- this.ops.years = py.float.fromJSON(
- asJS(this.ops.years) + r.div*s);
- }
- this._has_time = 0;
- },
- __add__: function (other) {
- if (!py.PY_isInstance(other, datetime.date)) {
- return py.NotImplemented;
- }
- // TODO: test this whole mess
- var year = (asJS(this.ops.year) || asJS(other.year)) + asJS(this.ops.years);
- var month = asJS(this.ops.month) || asJS(other.month);
- var months;
- if (months = asJS(this.ops.months)) {
- if (Math.abs(months) < 1 || Math.abs(months) > 12) {
- throw new Error("Can only use relative months between -12 and +12");
- }
- month += months;
- if (month > 12) {
- year += 1;
- month -= 12;
- }
- if (month < 1) {
- year -= 1;
- month += 12;
- }
- }
-
- var day = Math.min(utils.monthrange(year, month)[1],
- asJS(this.ops.day) || asJS(other.day));
-
- var repl = {
- year: py.float.fromJSON(year),
- month: py.float.fromJSON(month),
- day: py.float.fromJSON(day)
- };
-
- var days = asJS(this.ops.days);
- if (py.PY_isTrue(this.ops.leapdays) && month > 2 && utils.isleap(year)) {
- days += asJS(this.ops.leapdays);
- }
-
- var ret = py.PY_add(
- py.PY_call(py.PY_getAttr(other, 'replace'), repl),
- py.PY_call(datetime.timedelta, {
- days: py.float.fromJSON(days)
- })
- );
-
- if (this.ops.weekday) {
- // FIXME: only handles numeric weekdays, not decorated
- var weekday = asJS(this.ops.weekday), nth = 1;
- var jumpdays = (Math.abs(nth) - 1) * 7;
-
- var ret_weekday = asJS(py.PY_call(py.PY_getAttr(ret, 'weekday')));
- if (nth > 0) {
- jumpdays += (7-ret_weekday+weekday) % 7;
- } else {
- jumpdays += (ret_weekday - weekday) % 7;
- jumpdays *= -1;
- }
- ret = py.PY_add(
- ret,
- py.PY_call(datetime.timedelta, {
- days: py.float.fromJSON(jumpdays)
- })
- );
- }
-
- return ret;
- },
- __radd__: function (other) {
- return this.__add__(other);
- },
- __rsub__: function (other) {
- return this.__neg__().__radd__(other);
- },
- __neg__: function () {
- return py.PY_call(relativedelta, {
- years: py.PY_negative(this.ops.years),
- months: py.PY_negative(this.ops.months),
- days: py.PY_negative(this.ops.days),
- leapdays: this.ops.leapdays,
- year: this.ops.year,
- month: this.ops.month,
- day: this.ops.day,
- weekday: this.ops.weekday
- });
- }
- });
-
- // recursively wraps JS objects passed into the context to attributedicts
- // which jsonify back to JS objects
- var wrap = function (value) {
- if (value === null) { return py.None; }
-
- switch (typeof value) {
- case 'undefined': throw new Error("No conversion for undefined");
- case 'boolean': return py.bool.fromJSON(value);
- case 'number': return py.float.fromJSON(value);
- case 'string': return py.str.fromJSON(value);
- }
-
- switch(value.constructor) {
- case Object: return wrapping_dict.fromJSON(value);
- case Array: return wrapping_list.fromJSON(value);
- }
-
- throw new Error("ValueError: unable to wrap " + value);
- };
- var wrapping_dict = py.type('wrapping_dict', null, {
- __init__: function () {
- this._store = {};
- },
- __getitem__: function (key) {
- var k = key.toJSON();
- if (!(k in this._store)) {
- throw new Error("KeyError: '" + k + "'");
- }
- return wrap(this._store[k]);
- },
- __getattr__: function (key) {
- return this.__getitem__(py.str.fromJSON(key));
- },
- get: function () {
- var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]);
-
- if (!(args.k.toJSON() in this._store)) { return args.d; }
- return this.__getitem__(args.k);
- },
- fromJSON: function (d) {
- var instance = py.PY_call(wrapping_dict);
- instance._store = d;
- return instance;
- },
- toJSON: function () {
- return this._store;
- },
- });
- var wrapping_list = py.type('wrapping_list', null, {
- __init__: function () {
- this._store = [];
- },
- __getitem__: function (index) {
- return wrap(this._store[index.toJSON()]);
- },
- fromJSON: function (ar) {
- var instance = py.PY_call(wrapping_list);
- instance._store = ar;
- return instance;
- },
- toJSON: function () {
- return this._store;
- },
- });
- var wrap_context = function (context) {
- for (var k in context) {
- if (!context.hasOwnProperty(k)) { continue; }
- var val = context[k];
-
- if (val === null) { continue; }
- if (val.constructor === Array) {
- context[k] = wrapping_list.fromJSON(val);
- } else if (val.constructor === Object
- && !py.PY_isInstance(val, py.object)) {
- context[k] = wrapping_dict.fromJSON(val);
- }
- }
- return context;
- };
-
- var eval_contexts = function (contexts, evaluation_context) {
- evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
- return _(contexts).reduce(function (result_context, ctx) {
- // __eval_context evaluations can lead to some of `contexts`'s
- // values being null, skip them as well as empty contexts
- if (_.isEmpty(ctx)) { return result_context; }
- if (_.isString(ctx)) {
- // wrap raw strings in context
- ctx = { __ref: 'context', __debug: ctx };
- }
- var evaluated = ctx;
- switch(ctx.__ref) {
- case 'context':
- evaluation_context.context = evaluation_context;
- evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context));
- break;
- case 'compound_context':
- var eval_context = eval_contexts([ctx.__eval_context]);
- evaluated = eval_contexts(
- ctx.__contexts, _.extend({}, evaluation_context, eval_context));
- break;
- }
- // add newly evaluated context to evaluation context for following
- // siblings
- _.extend(evaluation_context, evaluated);
- return _.extend(result_context, evaluated);
- }, {});
- };
- var eval_domains = function (domains, evaluation_context) {
- evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
- var result_domain = [];
- _(domains).each(function (domain) {
- if (_.isString(domain)) {
- // wrap raw strings in domain
- domain = { __ref: 'domain', __debug: domain };
- }
- switch(domain.__ref) {
- case 'domain':
- evaluation_context.context = evaluation_context;
- result_domain.push.apply(
- result_domain, py.eval(domain.__debug, wrap_context(evaluation_context)));
- break;
- case 'compound_domain':
- var eval_context = eval_contexts([domain.__eval_context]);
- result_domain.push.apply(
- result_domain, eval_domains(
- domain.__domains, _.extend(
- {}, evaluation_context, eval_context)));
- break;
- default:
- result_domain.push.apply(result_domain, domain);
- }
- });
- return result_domain;
- };
- var eval_groupbys = function (contexts, evaluation_context) {
- evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
- var result_group = [];
- _(contexts).each(function (ctx) {
- if (_.isString(ctx)) {
- // wrap raw strings in context
- ctx = { __ref: 'context', __debug: ctx };
- }
- var group;
- var evaluated = ctx;
- switch(ctx.__ref) {
- case 'context':
- evaluation_context.context = evaluation_context;
- evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context));
- break;
- case 'compound_context':
- var eval_context = eval_contexts([ctx.__eval_context]);
- evaluated = eval_contexts(
- ctx.__contexts, _.extend({}, evaluation_context, eval_context));
- break;
- }
- group = evaluated.group_by;
- if (!group) { return; }
- if (typeof group === 'string') {
- result_group.push(group);
- } else if (group instanceof Array) {
- result_group.push.apply(result_group, group);
- } else {
- throw new Error('Got invalid groupby {{'
- + JSON.stringify(group) + '}}');
- }
- _.extend(evaluation_context, evaluated);
- });
- return result_group;
- };
-
- instance.web.pyeval.context = function () {
- return _.extend({
- datetime: datetime,
- context_today: context_today,
- time: time,
- relativedelta: relativedelta,
- current_date: py.PY_call(
- time.strftime, [py.str.fromJSON('%Y-%m-%d')]),
- }, instance.session.user_context);
- };
-
- /**
- * @param {String} type "domains", "contexts" or "groupbys"
- * @param {Array} object domains or contexts to evaluate
- * @param {Object} [context] evaluation context
- */
- instance.web.pyeval.eval = function (type, object, context, options) {
- options = options || {};
- context = _.extend(instance.web.pyeval.context(), context || {});
-
- //noinspection FallthroughInSwitchStatementJS
- switch(type) {
- case 'context':
- case 'contexts':
- if (type === 'context')
- object = [object];
- return eval_contexts((options.no_user_context ? [] : [instance.session.user_context]).concat(object), context);
- case 'domain':
- case 'domains':
- if (type === 'domain')
- object = [object];
- return eval_domains(object, context);
- case 'groupbys':
- return eval_groupbys(object, context);
- }
- throw new Error("Unknow evaluation type " + type);
- };
-
- var eval_arg = function (arg) {
- if (typeof arg !== 'object' || !arg.__ref) { return arg; }
- switch(arg.__ref) {
- case 'domain': case 'compound_domain':
- return instance.web.pyeval.eval('domains', [arg]);
- case 'context': case 'compound_context':
- return instance.web.pyeval.eval('contexts', [arg]);
- default:
- throw new Error(instance.web._t("Unknown nonliteral type " + arg.__ref));
- }
- };
- /**
- * If args or kwargs are unevaluated contexts or domains (compound or not),
- * evaluated them in-place.
- *
- * Potentially mutates both parameters.
- *
- * @param args
- * @param kwargs
- */
- instance.web.pyeval.ensure_evaluated = function (args, kwargs) {
- for (var i=0; i<args.length; ++i) {
- args[i] = eval_arg(args[i]);
- }
- for (var k in kwargs) {
- if (!kwargs.hasOwnProperty(k)) { continue; }
- kwargs[k] = eval_arg(kwargs[k]);
- }
- };
- instance.web.pyeval.eval_domains_and_contexts = function (source) {
- return new $.Deferred(function (d) {setTimeout(function () {
- var result;
- try {
- result = instance.web.pyeval.sync_eval_domains_and_contexts(source);
- }
- catch (e) {
- result = { error: {
- code: 400,
- message: instance.web._t("Evaluation Error"),
- data: {
- type: 'local_exception',
- debug: _.str.sprintf(
- instance.web._t("Local evaluation failure\n%s\n\n%s"),
- e.message, JSON.stringify(source))
- }
- }};
- }
- d.resolve(result);
- }, 0); });
- };
- instance.web.pyeval.sync_eval_domains_and_contexts = function (source) {
- var contexts = ([instance.session.user_context] || []).concat(source.contexts);
- // see Session.eval_context in Python
- return {
- context: instance.web.pyeval.eval('contexts', contexts),
- domain: instance.web.pyeval.eval('domains', source.domains),
- group_by: instance.web.pyeval.eval('groupbys', source.group_by_seq || [])
- };
- };
-})();
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
deleted file mode 100644
index f65cb3cb140..00000000000
--- a/addons/web/static/src/js/search.js
+++ /dev/null
@@ -1,2333 +0,0 @@
-
-(function() {
-
-var instance = openerp;
-openerp.web.search = {};
-
-var QWeb = instance.web.qweb,
- _t = instance.web._t,
- _lt = instance.web._lt;
-
-/** @namespace */
-var my = instance.web.search = {};
-
-var B = Backbone;
-my.FacetValue = B.Model.extend({
-
-});
-my.FacetValues = B.Collection.extend({
- model: my.FacetValue
-});
-my.Facet = B.Model.extend({
- initialize: function (attrs) {
- var values = attrs.values;
- delete attrs.values;
-
- B.Model.prototype.initialize.apply(this, arguments);
-
- this.values = new my.FacetValues(values || []);
- this.values.on('add remove change reset', function (_, options) {
- this.trigger('change', this, options);
- }, this);
- },
- get: function (key) {
- if (key !== 'values') {
- return B.Model.prototype.get.call(this, key);
- }
- return this.values.toJSON();
- },
- set: function (key, value) {
- if (key !== 'values') {
- return B.Model.prototype.set.call(this, key, value);
- }
- this.values.reset(value);
- },
- toJSON: function () {
- var out = {};
- var attrs = this.attributes;
- for(var att in attrs) {
- if (!attrs.hasOwnProperty(att) || att === 'field') {
- continue;
- }
- out[att] = attrs[att];
- }
- out.values = this.values.toJSON();
- return out;
- }
-});
-my.SearchQuery = B.Collection.extend({
- model: my.Facet,
- initialize: function () {
- B.Collection.prototype.initialize.apply(
- this, arguments);
- this.on('change', function (facet) {
- if(!facet.values.isEmpty()) { return; }
-
- this.remove(facet, {silent: true});
- }, this);
- },
- add: function (values, options) {
- options = options || {};
-
- if (!values) {
- values = [];
- } else if (!(values instanceof Array)) {
- values = [values];
- }
-
- _(values).each(function (value) {
- var model = this._prepareModel(value, options);
- var previous = this.detect(function (facet) {
- return facet.get('category') === model.get('category')
- && facet.get('field') === model.get('field');
- });
- if (previous) {
- previous.values.add(model.get('values'), _.omit(options, 'at', 'merge'));
- return;
- }
- B.Collection.prototype.add.call(this, model, options);
- }, this);
- // warning: in backbone 1.0+ add is supposed to return the added models,
- // but here toggle may delegate to add and return its value directly.
- // return value of neither seems actually used but should be tested
- // before change, probably
- return this;
- },
- toggle: function (value, options) {
- options = options || {};
-
- var facet = this.detect(function (facet) {
- return facet.get('category') === value.category
- && facet.get('field') === value.field;
- });
- if (!facet) {
- return this.add(value, options);
- }
-
- var changed = false;
- _(value.values).each(function (val) {
- var already_value = facet.values.detect(function (v) {
- return v.get('value') === val.value
- && v.get('label') === val.label;
- });
- // toggle value
- if (already_value) {
- facet.values.remove(already_value, {silent: true});
- } else {
- facet.values.add(val, {silent: true});
- }
- changed = true;
- });
- // "Commit" changes to values array as a single call, so observers of
- // change event don't get misled by intermediate incomplete toggling
- // states
- facet.trigger('change', facet);
- return this;
- }
-});
-
-function assert(condition, message) {
- if(!condition) {
- throw new Error(message);
- }
-}
-my.InputView = instance.web.Widget.extend({
- template: 'SearchView.InputView',
- events: {
- focus: function () { this.trigger('focused', this); },
- blur: function () { this.$el.text(''); this.trigger('blurred', this); },
- keydown: 'onKeydown',
- paste: 'onPaste',
- },
- getSelection: function () {
- this.el.normalize();
- // get Text node
- var root = this.el.childNodes[0];
- if (!root || !root.textContent) {
- // if input does not have a child node, or the child node is an
- // empty string, then the selection can only be (0, 0)
- return {start: 0, end: 0};
- }
- var range = window.getSelection().getRangeAt(0);
- // In Firefox, depending on the way text is selected (drag, double- or
- // triple-click) the range may start or end on the parent of the
- // selected text node‽ Check for this condition and fixup the range
- // note: apparently with C-a this can go even higher?
- if (range.startContainer === this.el && range.startOffset === 0) {
- range.setStart(root, 0);
- }
- if (range.endContainer === this.el && range.endOffset === 1) {
- range.setEnd(root, root.length);
- }
- assert(range.startContainer === root,
- "selection should be in the input view");
- assert(range.endContainer === root,
- "selection should be in the input view");
- return {
- start: range.startOffset,
- end: range.endOffset
- };
- },
- onKeydown: function (e) {
- this.el.normalize();
- var sel;
- switch (e.which) {
- // Do not insert newline, but let it bubble so searchview can use it
- case $.ui.keyCode.ENTER:
- e.preventDefault();
- break;
-
- // FIXME: may forget content if non-empty but caret at index 0, ok?
- case $.ui.keyCode.BACKSPACE:
- sel = this.getSelection();
- if (sel.start === 0 && sel.start === sel.end) {
- e.preventDefault();
- var preceding = this.getParent().siblingSubview(this, -1);
- if (preceding && (preceding instanceof my.FacetView)) {
- preceding.model.destroy();
- }
- }
- break;
-
- // let left/right events propagate to view if caret is at input border
- // and not a selection
- case $.ui.keyCode.LEFT:
- sel = this.getSelection();
- if (sel.start !== 0 || sel.start !== sel.end) {
- e.stopPropagation();
- }
- break;
- case $.ui.keyCode.RIGHT:
- sel = this.getSelection();
- var len = this.$el.text().length;
- if (sel.start !== len || sel.start !== sel.end) {
- e.stopPropagation();
- }
- break;
- }
- },
- setCursorAtEnd: function () {
- this.el.normalize();
- var sel = window.getSelection();
- sel.removeAllRanges();
- var range = document.createRange();
- // in theory, range.selectNodeContents should work here. In practice,
- // MSIE9 has issues from time to time, instead of selecting the inner
- // text node it would select the reference node instead (e.g. in demo
- // data, company news, copy across the "Company News" link + the title,
- // from about half the link to half the text, paste in search box then
- // hit the left arrow key, getSelection would blow up).
- //
- // Explicitly selecting only the inner text node (only child node
- // since we've normalized the parent) avoids the issue
- range.selectNode(this.el.childNodes[0]);
- range.collapse(false);
- sel.addRange(range);
- },
- onPaste: function () {
- this.el.normalize();
- // In MSIE and Webkit, it is possible to get various representations of
- // the clipboard data at this point e.g.
- // window.clipboardData.getData('Text') and
- // event.clipboardData.getData('text/plain') to ensure we have a plain
- // text representation of the object (and probably ensure the object is
- // pastable as well, so nobody puts an image in the search view)
- // (nb: since it's not possible to alter the content of the clipboard
- // — at least in Webkit — to ensure only textual content is available,
- // using this would require 1. getting the text data; 2. manually
- // inserting the text data into the content; and 3. cancelling the
- // paste event)
- //
- // But Firefox doesn't support the clipboard API (as of FF18)
- // although it correctly triggers the paste event (Opera does not even
- // do that) => implement lowest-denominator system where onPaste
- // triggers a followup "cleanup" pass after the data has been pasted
- setTimeout(function () {
- // Read text content (ignore pasted HTML)
- var data = this.$el.text();
- if (!data)
- return;
- // paste raw text back in
- this.$el.empty().text(data);
- this.el.normalize();
- // Set the cursor at the end of the text, so the cursor is not lost
- // in some kind of error-spawning limbo.
- this.setCursorAtEnd();
- }.bind(this), 0);
- }
-});
-my.FacetView = instance.web.Widget.extend({
- template: 'SearchView.FacetView',
- events: {
- 'focus': function () { this.trigger('focused', this); },
- 'blur': function () { this.trigger('blurred', this); },
- 'click': function (e) {
- if ($(e.target).is('.oe_facet_remove')) {
- this.model.destroy();
- return false;
- }
- this.$el.focus();
- e.stopPropagation();
- },
- 'keydown': function (e) {
- var keys = $.ui.keyCode;
- switch (e.which) {
- case keys.BACKSPACE:
- case keys.DELETE:
- this.model.destroy();
- return false;
- }
- }
- },
- init: function (parent, model) {
- this._super(parent);
- this.model = model;
- this.model.on('change', this.model_changed, this);
- },
- destroy: function () {
- this.model.off('change', this.model_changed, this);
- this._super();
- },
- start: function () {
- var self = this;
- var $e = this.$('> span:last-child');
- return $.when(this._super()).then(function () {
- return $.when.apply(null, self.model.values.map(function (value) {
- return new my.FacetValueView(self, value).appendTo($e);
- }));
- });
- },
- model_changed: function () {
- this.$el.text(this.$el.text() + '*');
- }
-});
-my.FacetValueView = instance.web.Widget.extend({
- template: 'SearchView.FacetView.Value',
- init: function (parent, model) {
- this._super(parent);
- this.model = model;
- this.model.on('change', this.model_changed, this);
- },
- destroy: function () {
- this.model.off('change', this.model_changed, this);
- this._super();
- },
- model_changed: function () {
- this.$el.text(this.$el.text() + '*');
- }
-});
-
-instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.SearchView# */{
- template: "SearchView",
- events: {
- // focus last input if view itself is clicked
- 'click': function (e) {
- if (e.target === this.$('.oe_searchview_facets')[0]) {
- this.$('.oe_searchview_input:last').focus();
- }
- },
- // search button
- 'click div.oe_searchview_search': function (e) {
- e.stopImmediatePropagation();
- this.do_search();
- },
- 'click .oe_searchview_unfold_drawer': function (e) {
- e.stopImmediatePropagation();
- $(e.target).toggleClass('fa-caret-down fa-caret-up');
- localStorage.visible_search_menu = (localStorage.visible_search_menu !== 'true');
- this.toggle_buttons();
- },
- 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) {
- switch(e.which) {
- case $.ui.keyCode.LEFT:
- this.focusPreceding(e.target);
- e.preventDefault();
- break;
- case $.ui.keyCode.RIGHT:
- if (!this.autocomplete.is_expandable()) {
- this.focusFollowing(e.target);
- }
- e.preventDefault();
- break;
- }
- },
- 'autocompleteopen': function () {
- this.$el.autocomplete('widget').css('z-index', 9999);
- },
- },
- /**
- * @constructs instance.web.SearchView
- * @extends instance.web.Widget
- *
- * @param parent
- * @param dataset
- * @param view_id
- * @param defaults
- * @param {Object} [options]
- * @param {Boolean} [options.hidden=false] hide the search view
- * @param {Boolean} [options.disable_custom_filters=false] do not load custom filters from ir.filters
- */
- init: function(parent, dataset, view_id, defaults, options) {
- this.options = _.defaults(options || {}, {
- hidden: false,
- disable_custom_filters: false,
- });
- this._super(parent);
- this.query = undefined;
- this.dataset = dataset;
- this.view_id = view_id;
- this.search_fields = [];
- this.filters = [];
- this.groupbys = [];
- this.visible_filters = (localStorage.visible_search_menu === 'true');
- this.input_subviews = []; // for user input in searchbar
- this.defaults = defaults || {};
- this.headless = this.options.hidden && _.isEmpty(this.defaults);
- this.$buttons = this.options.$buttons;
-
- this.filter_menu = undefined;
- this.groupby_menu = undefined;
- this.favorite_menu = undefined;
- this.action_id = this.options && this.options.action && this.options.action.id;
- },
- start: function() {
- if (this.headless) {
- this.$el.hide();
- }
- this.toggle_visibility(false);
- this.$facets_container = this.$('div.oe_searchview_facets');
- this.setup_global_completion();
- this.query = new my.SearchQuery()
- .on('add change reset remove', this.proxy('do_search'))
- .on('change', this.proxy('renderChangedFacets'))
- .on('add reset remove', this.proxy('renderFacets'));
- var load_view = instance.web.fields_view_get({
- model: this.dataset._model,
- view_id: this.view_id,
- view_type: 'search',
- context: this.dataset.get_context(),
- });
- this.$('.oe_searchview_unfold_drawer')
- .toggleClass('fa-caret-down', !this.visible_filters)
- .toggleClass('fa-caret-up', this.visible_filters);
- return this.alive($.when(this._super(), this.alive(load_view).then(this.view_loaded.bind(this))));
- },
- view_loaded: function (r) {
- var self = this;
- this.fields_view_get = r;
- this.view_id = this.view_id || r.view_id;
- this.prepare_search_inputs();
- if (this.$buttons) {
-
- var fields_def = new instance.web.Model(this.dataset.model).call('fields_get', {
- context: this.dataset.context
- });
-
- this.groupby_menu = new my.GroupByMenu(this, this.groupbys, fields_def);
- this.filter_menu = new my.FilterMenu(this, this.filters, fields_def);
- this.favorite_menu = new my.FavoriteMenu(this, this.query, this.dataset.model, this.action_id);
-
- this.filter_menu.appendTo(this.$buttons);
- this.groupby_menu.appendTo(this.$buttons);
- var custom_filters_ready = this.favorite_menu.appendTo(this.$buttons);
- }
- return $.when(custom_filters_ready).then(this.proxy('set_default_filters'));
- },
- set_default_filters: function () {
- var self = this,
- default_custom_filter = this.$buttons && this.favorite_menu.get_default_filter();
- if (!self.options.disable_custom_filters && default_custom_filter) {
- return this.favorite_menu.toggle_filter(default_custom_filter, true);
- }
- if (!_.isEmpty(this.defaults)) {
- var inputs = this.search_fields.concat(this.filters, this.groupbys),
- defaults = _.invoke(inputs, 'facet_for_defaults', this.defaults);
- return $.when.apply(null, defaults).then(function () {
- self.query.reset(_(arguments).compact(), {preventSearch: true});
- });
- }
- this.query.reset([], {preventSearch: true});
- return $.when();
- },
- /**
- * Performs the search view collection of widget data.
- *
- * If the collection went well (all fields are valid), then triggers
- * :js:func:`instance.web.SearchView.on_search`.
- *
- * If at least one field failed its validation, triggers
- * :js:func:`instance.web.SearchView.on_invalid` instead.
- *
- * @param [_query]
- * @param {Object} [options]
- */
- do_search: function (_query, options) {
- if (options && options.preventSearch) {
- return;
- }
- var search = this.build_search_data();
- this.trigger('search_data', search.domains, search.contexts, search.groupbys);
- },
- /**
- * Extract search data from the view's facets.
- *
- * Result is an object with 3 (own) properties:
- *
- * domains
- * Array of domains
- * contexts
- * Array of contexts
- * groupbys
- * Array of domains, in groupby order rather than view order
- *
- * @return {Object}
- */
- build_search_data: function () {
- var domains = [], contexts = [], groupbys = [];
-
- this.query.each(function (facet) {
- var field = facet.get('field');
- var domain = field.get_domain(facet);
- if (domain) {
- domains.push(domain);
- }
- var context = field.get_context(facet);
- if (context) {
- contexts.push(context);
- }
- var group_by = field.get_groupby(facet);
- if (group_by) {
- groupbys.push.apply(groupbys, group_by);
- }
- });
- return {
- domains: domains,
- contexts: contexts,
- groupbys: groupbys,
- };
- },
- toggle_visibility: function (is_visible) {
- this.$el.toggle(!this.headless && is_visible);
- this.$buttons && this.$buttons.toggle(!this.headless && is_visible && this.visible_filters);
- if (!this.headless && is_visible) {
- this.$('div.oe_searchview_input').last().focus();
- }
- },
- toggle_buttons: function (is_visible) {
- this.visible_filters = is_visible || !this.visible_filters;
- this.$buttons && this.$buttons.toggle(this.visible_filters);
- },
- /**
- * Sets up search view's view-wide auto-completion widget
- */
- setup_global_completion: function () {
- var self = this;
- this.autocomplete = new my.AutoComplete(this, {
- source: this.proxy('complete_global_search'),
- select: this.proxy('select_completion'),
- delay: 0,
- get_search_string: function () {
- return self.$('div.oe_searchview_input').text();
- },
- });
- this.autocomplete.appendTo(this.$el);
- },
- /**
- * Provide auto-completion result for req.term (an array to `resp`)
- *
- * @param {Object} req request to complete
- * @param {String} req.term searched term to complete
- * @param {Function} resp response callback
- */
- complete_global_search: function (req, resp) {
- var inputs = this.search_fields.concat(this.filters, this.groupbys);
- $.when.apply(null, _(inputs).chain()
- .filter(function (input) { return input.visible(); })
- .invoke('complete', req.term)
- .value()).then(function () {
- resp(_(arguments).chain()
- .compact()
- .flatten(true)
- .value());
- });
- },
- /**
- * Action to perform in case of selection: create a facet (model)
- * and add it to the search collection
- *
- * @param {Object} e selection event, preventDefault to avoid setting value on object
- * @param {Object} ui selection information
- * @param {Object} ui.item selected completion item
- */
- select_completion: function (e, ui) {
- e.preventDefault();
- var input_index = _(this.input_subviews).indexOf(
- this.subviewForRoot(
- this.$('div.oe_searchview_input:focus')[0]));
- this.query.add(ui.item.facet, {at: input_index / 2});
- },
- subviewForRoot: function (subview_root) {
- return _(this.input_subviews).detect(function (subview) {
- return subview.$el[0] === subview_root;
- });
- },
- siblingSubview: function (subview, direction, wrap_around) {
- var index = _(this.input_subviews).indexOf(subview) + direction;
- if (wrap_around && index < 0) {
- index = this.input_subviews.length - 1;
- } else if (wrap_around && index >= this.input_subviews.length) {
- index = 0;
- }
- return this.input_subviews[index];
- },
- focusPreceding: function (subview_root) {
- return this.siblingSubview(
- this.subviewForRoot(subview_root), -1, true)
- .$el.focus();
- },
- focusFollowing: function (subview_root) {
- return this.siblingSubview(
- this.subviewForRoot(subview_root), +1, true)
- .$el.focus();
- },
- /**
- * @param {openerp.web.search.SearchQuery | undefined} Undefined if event is change
- * @param {openerp.web.search.Facet}
- * @param {Object} [options]
- */
- renderFacets: function (collection, model, options) {
- var self = this;
- var started = [];
- _.invoke(this.input_subviews, 'destroy');
- this.input_subviews = [];
-
- var i = new my.InputView(this);
- started.push(i.appendTo(this.$facets_container));
- this.input_subviews.push(i);
- this.query.each(function (facet) {
- var f = new my.FacetView(this, facet);
- started.push(f.appendTo(self.$facets_container));
- self.input_subviews.push(f);
-
- var i = new my.InputView(this);
- started.push(i.appendTo(self.$facets_container));
- self.input_subviews.push(i);
- }, this);
- _.each(this.input_subviews, function (childView) {
- childView.on('focused', self, self.proxy('childFocused'));
- childView.on('blurred', self, self.proxy('childBlurred'));
- });
-
- $.when.apply(null, started).then(function () {
- if (options && options.focus_input === false) return;
- var input_to_focus;
- // options.at: facet inserted at given index, focus next input
- // otherwise just focus last input
- if (!options || typeof options.at !== 'number') {
- input_to_focus = _.last(self.input_subviews);
- } else {
- input_to_focus = self.input_subviews[(options.at + 1) * 2];
- }
- input_to_focus.$el.focus();
- });
- },
- childFocused: function () {
- this.$el.addClass('active');
- },
- childBlurred: function () {
- this.$el.val('').removeClass('active').trigger('blur');
- this.autocomplete.close();
- },
- /**
- * Call the renderFacets method with the correct arguments.
- * This is due to the fact that change events are called with two arguments
- * (model, options) while add, reset and remove events are called with
- * (collection, model, options) as arguments
- */
- renderChangedFacets: function (model, options) {
- this.renderFacets(undefined, model, options);
- },
- // it should parse the arch field of the view, instantiate the corresponding
- // filters/fields, and put them in the correct variables:
- // * this.search_fields is a list of all the fields,
- // * this.filters: groups of filters
- // * this.group_by: group_bys
- prepare_search_inputs: function () {
- var self = this,
- arch = this.fields_view_get.arch;
-
- var filters = [].concat.apply([], _.map(arch.children, function (item) {
- return item.tag !== 'group' ? eval_item(item) : item.children.map(eval_item);
- }));
- function eval_item (item) {
- var category = 'filters';
- if (item.attrs.context) {
- try {
- var context = instance.web.pyeval.eval('context', item.attrs.context);
- if (context.group_by) {
- category = 'group_by';
- }
- } catch (e) {}
- }
- return {
- item: item,
- category: category,
- };
- }
- var current_group = [],
- current_category = 'filters',
- categories = {filters: this.filters, group_by: this.groupbys};
-
- _.each(filters.concat({category:'filters', item: 'separator'}), function (filter) {
- if (filter.item.tag === 'filter' && filter.category === current_category) {
- return current_group.push(new my.Filter(filter.item, self));
- }
- if (current_group.length) {
- var group = new my.FilterGroup(current_group, self);
- categories[current_category].push(group);
- current_group = [];
- }
- if (filter.item.tag === 'field') {
- var attrs = filter.item.attrs,
- field = self.fields_view_get.fields[attrs.name],
- Obj = my.fields.get_any([attrs.widget, field.type]);
- if (Obj) {
- self.search_fields.push(new (Obj) (filter.item, field, self));
- }
- }
- if (filter.item.tag === 'filter') {
- current_group.push(new my.Filter(filter.item, self));
- }
- current_category = filter.category;
- });
- },
-});
-
-/**
- * Registry of search fields, called by :js:class:`instance.web.SearchView` to
- * find and instantiate its field widgets.
- */
-instance.web.search.fields = new instance.web.Registry({
- 'char': 'instance.web.search.CharField',
- 'text': 'instance.web.search.CharField',
- 'html': 'instance.web.search.CharField',
- 'boolean': 'instance.web.search.BooleanField',
- 'integer': 'instance.web.search.IntegerField',
- 'id': 'instance.web.search.IntegerField',
- 'float': 'instance.web.search.FloatField',
- 'selection': 'instance.web.search.SelectionField',
- 'datetime': 'instance.web.search.DateTimeField',
- 'date': 'instance.web.search.DateField',
- 'many2one': 'instance.web.search.ManyToOneField',
- 'many2many': 'instance.web.search.CharField',
- 'one2many': 'instance.web.search.CharField'
-});
-
-instance.web.search.Input = instance.web.Widget.extend( /** @lends instance.web.search.Input# */{
- /**
- * @constructs instance.web.search.Input
- * @extends instance.web.Widget
- *
- * @param parent
- */
- init: function (parent) {
- this._super(parent);
- this.load_attrs({});
- },
- /**
- * Fetch auto-completion values for the widget.
- *
- * The completion values should be an array of objects with keys category,
- * label, value prefixed with an object with keys type=section and label
- *
- * @param {String} value value to complete
- * @returns {jQuery.Deferred<null|Array>}
- */
- complete: function (value) {
- return $.when(null);
- },
- /**
- * Returns a Facet instance for the provided defaults if they apply to
- * this widget, or null if they don't.
- *
- * This default implementation will try calling
- * :js:func:`instance.web.search.Input#facet_for` if the widget's name
- * matches the input key
- *
- * @param {Object} defaults
- * @returns {jQuery.Deferred<null|Object>}
- */
- facet_for_defaults: function (defaults) {
- if (!this.attrs ||
- !(this.attrs.name in defaults && defaults[this.attrs.name])) {
- return $.when(null);
- }
- return this.facet_for(defaults[this.attrs.name]);
- },
- get_context: function () {
- throw new Error(
- "get_context not implemented for widget " + this.attrs.type);
- },
- get_groupby: function () {
- throw new Error(
- "get_groupby not implemented for widget " + this.attrs.type);
- },
- get_domain: function () {
- throw new Error(
- "get_domain not implemented for widget " + this.attrs.type);
- },
- load_attrs: function (attrs) {
- attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
- this.attrs = attrs;
- },
- /**
- * Returns whether the input is "visible". The default behavior is to
- * query the ``modifiers.invisible`` flag on the input's description or
- * view node.
- *
- * @returns {Boolean}
- */
- visible: function () {
- return !this.attrs.modifiers.invisible;
- },
-});
-instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
- template: 'SearchView.filters',
- icon: "fa-filter",
- completion_label: _lt("Filter on: %s"),
- /**
- * Inclusive group of filters, creates a continuous "button" with clickable
- * sections (the normal display for filters is to be a self-contained button)
- *
- * @constructs instance.web.search.FilterGroup
- * @extends instance.web.search.Input
- *
- * @param {Array<instance.web.search.Filter>} filters elements of the group
- * @param {instance.web.SearchView} parent parent in which the filters are contained
- */
- init: function (filters, parent) {
- // If all filters are group_by and we're not initializing a GroupbyGroup,
- // create a GroupbyGroup instead of the current FilterGroup
- if (!(this instanceof instance.web.search.GroupbyGroup) &&
- _(filters).all(function (f) {
- if (!f.attrs.context) { return false; }
- var c = instance.web.pyeval.eval('context', f.attrs.context);
- return !_.isEmpty(c.group_by);})) {
- return new instance.web.search.GroupbyGroup(filters, parent);
- }
- this._super(parent);
- this.filters = filters;
- this.searchview = parent;
- this.searchview.query.on('add remove change reset', this.proxy('search_change'));
- },
- start: function () {
- this.$el.on('click', 'a', this.proxy('toggle_filter'));
- return $.when(null);
- },
- /**
- * Handles change of the search query: any of the group's filter which is
- * in the search query should be visually checked in the drawer
- */
- search_change: function () {
- var self = this;
- var $filters = this.$el.removeClass('selected');
- var facet = this.searchview.query.find(_.bind(this.match_facet, this));
- if (!facet) { return; }
- facet.values.each(function (v) {
- var i = _(self.filters).indexOf(v.get('value'));
- if (i === -1) { return; }
- $filters.filter(function () {
- return Number($(this).data('index')) === i;
- }).addClass('selected');
- });
- },
- /**
- * Matches the group to a facet, in order to find if the group is
- * represented in the current search query
- */
- match_facet: function (facet) {
- return facet.get('field') === this;
- },
- make_facet: function (values) {
- return {
- category: _t("Filter"),
- icon: this.icon,
- values: values,
- field: this
- };
- },
- make_value: function (filter) {
- return {
- label: filter.attrs.string || filter.attrs.help || filter.attrs.name,
- value: filter
- };
- },
- facet_for_defaults: function (defaults) {
- var self = this;
- var fs = _(this.filters).chain()
- .filter(function (f) {
- return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
- }).map(function (f) {
- return self.make_value(f);
- }).value();
- if (_.isEmpty(fs)) { return $.when(null); }
- return $.when(this.make_facet(fs));
- },
- /**
- * Fetches contexts for all enabled filters in the group
- *
- * @param {openerp.web.search.Facet} facet
- * @return {*} combined contexts of the enabled filters in this group
- */
- get_context: function (facet) {
- var contexts = facet.values.chain()
- .map(function (f) { return f.get('value').attrs.context; })
- .without('{}')
- .reject(_.isEmpty)
- .value();
-
- if (!contexts.length) { return; }
- if (contexts.length === 1) { return contexts[0]; }
- return _.extend(new instance.web.CompoundContext(), {
- __contexts: contexts
- });
- },
- /**
- * Fetches group_by sequence for all enabled filters in the group
- *
- * @param {VS.model.SearchFacet} facet
- * @return {Array} enabled filters in this group
- */
- get_groupby: function (facet) {
- return facet.values.chain()
- .map(function (f) { return f.get('value').attrs.context; })
- .without('{}')
- .reject(_.isEmpty)
- .value();
- },
- /**
- * Handles domains-fetching for all the filters within it: groups them.
- *
- * @param {VS.model.SearchFacet} facet
- * @return {*} combined domains of the enabled filters in this group
- */
- get_domain: function (facet) {
- var domains = facet.values.chain()
- .map(function (f) { return f.get('value').attrs.domain; })
- .without('[]')
- .reject(_.isEmpty)
- .value();
-
- if (!domains.length) { return; }
- if (domains.length === 1) { return domains[0]; }
- for (var i=domains.length; --i;) {
- domains.unshift(['|']);
- }
- return _.extend(new instance.web.CompoundDomain(), {
- __domains: domains
- });
- },
- toggle_filter: function (e) {
- e.stopPropagation();
- this.toggle(this.filters[Number($(e.target).parent().data('index'))]);
- },
- toggle: function (filter, options) {
- this.searchview.query.toggle(this.make_facet([this.make_value(filter)]), options);
- },
- is_visible: function () {
- return _.some(this.filters, function (filter) {
- return !filter.attrs.invisible;
- });
- },
- complete: function (item) {
- var self = this;
- item = item.toLowerCase();
- var facet_values = _(this.filters).chain()
- .filter(function (filter) { return filter.visible(); })
- .filter(function (filter) {
- var at = {
- string: filter.attrs.string || '',
- help: filter.attrs.help || '',
- name: filter.attrs.name || ''
- };
- var include = _.str.include;
- return include(at.string.toLowerCase(), item)
- || include(at.help.toLowerCase(), item)
- || include(at.name.toLowerCase(), item);
- })
- .map(this.make_value)
- .value();
- if (_(facet_values).isEmpty()) { return $.when(null); }
- return $.when(_.map(facet_values, function (facet_value) {
- return {
- label: _.str.sprintf(self.completion_label.toString(),
- _.escape(facet_value.label)),
- facet: self.make_facet([facet_value])
- };
- }));
- }
-});
-instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
- icon: 'fa-bars',
- completion_label: _lt("Group by: %s"),
- init: function (filters, parent) {
- this._super(filters, parent);
- this.searchview = parent;
- // Not flanders: facet unicity is handled through the
- // (category, field) pair of facet attributes. This is all well and
- // good for regular filter groups where a group matches a facet, but for
- // groupby we want a single facet. So cheat: add an attribute on the
- // view which proxies to the first GroupbyGroup, so it can be used
- // for every GroupbyGroup and still provides the various methods needed
- // by the search view. Use weirdo name to avoid risks of conflicts
- if (!this.searchview._s_groupby) {
- this.searchview._s_groupby = {
- help: "See GroupbyGroup#init",
- get_context: this.proxy('get_context'),
- get_domain: this.proxy('get_domain'),
- get_groupby: this.proxy('get_groupby')
- };
- }
- },
- match_facet: function (facet) {
- return facet.get('field') === this.searchview._s_groupby;
- },
- make_facet: function (values) {
- return {
- category: _t("GroupBy"),
- icon: this.icon,
- values: values,
- field: this.searchview._s_groupby
- };
- }
-});
-instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instance.web.search.Filter# */{
- template: 'SearchView.filter',
- /**
- * Implementation of the OpenERP filters (button with a context and/or
- * a domain sent as-is to the search view)
- *
- * Filters are only attributes holder, the actual work (compositing
- * domains and contexts, converting between facets and filters) is
- * performed by the filter group.
- *
- * @constructs instance.web.search.Filter
- * @extends instance.web.search.Input
- *
- * @param node
- * @param parent
- */
- init: function (node, parent) {
- this._super(parent);
- this.load_attrs(node.attrs);
- },
- facet_for: function () { return $.when(null); },
- get_context: function () { },
- get_domain: function () { },
-});
-
-instance.web.search.Field = instance.web.search.Input.extend( /** @lends instance.web.search.Field# */ {
- template: 'SearchView.field',
- default_operator: '=',
- /**
- * @constructs instance.web.search.Field
- * @extends instance.web.search.Input
- *
- * @param view_section
- * @param field
- * @param parent
- */
- init: function (view_section, field, parent) {
- this._super(parent);
- this.load_attrs(_.extend({}, field, view_section.attrs));
- },
- facet_for: function (value) {
- return $.when({
- field: this,
- category: this.attrs.string || this.attrs.name,
- values: [{label: String(value), value: value}]
- });
- },
- value_from: function (facetValue) {
- return facetValue.get('value');
- },
- get_context: function (facet) {
- var self = this;
- // A field needs a context to send when active
- var context = this.attrs.context;
- if (_.isEmpty(context) || !facet.values.length) {
- return;
- }
- var contexts = facet.values.map(function (facetValue) {
- return new instance.web.CompoundContext(context)
- .set_eval_context({self: self.value_from(facetValue)});
- });
-
- if (contexts.length === 1) { return contexts[0]; }
-
- return _.extend(new instance.web.CompoundContext(), {
- __contexts: contexts
- });
- },
- get_groupby: function () { },
- /**
- * Function creating the returned domain for the field, override this
- * methods in children if you only need to customize the field's domain
- * without more complex alterations or tests (and without the need to
- * change override the handling of filter_domain)
- *
- * @param {String} name the field's name
- * @param {String} operator the field's operator (either attribute-specified or default operator for the field
- * @param {Number|String} facet parsed value for the field
- * @returns {Array<Array>} domain to include in the resulting search
- */
- make_domain: function (name, operator, facet) {
- return [[name, operator, this.value_from(facet)]];
- },
- get_domain: function (facet) {
- if (!facet.values.length) { return; }
-
- var value_to_domain;
- var self = this;
- var domain = this.attrs.filter_domain;
- if (domain) {
- value_to_domain = function (facetValue) {
- return new instance.web.CompoundDomain(domain)
- .set_eval_context({self: self.value_from(facetValue)});
- };
- } else {
- value_to_domain = function (facetValue) {
- return self.make_domain(
- self.attrs.name,
- self.attrs.operator || self.default_operator,
- facetValue);
- };
- }
- var domains = facet.values.map(value_to_domain);
-
- if (domains.length === 1) { return domains[0]; }
- for (var i = domains.length; --i;) {
- domains.unshift(['|']);
- }
-
- return _.extend(new instance.web.CompoundDomain(), {
- __domains: domains
- });
- }
-});
-/**
- * Implementation of the ``char`` OpenERP field type:
- *
- * * Default operator is ``ilike`` rather than ``=``
- *
- * * The Javascript and the HTML values are identical (strings)
- *
- * @class
- * @extends instance.web.search.Field
- */
-instance.web.search.CharField = instance.web.search.Field.extend( /** @lends instance.web.search.CharField# */ {
- default_operator: 'ilike',
- complete: function (value) {
- if (_.isEmpty(value)) { return $.when(null); }
- var label = _.str.sprintf(_.str.escapeHTML(
- _t("Search %(field)s for: %(value)s")), {
- field: '<em>' + _.escape(this.attrs.string) + '</em>',
- value: '<strong>' + _.escape(value) + '</strong>'});
- return $.when([{
- label: label,
- facet: {
- category: this.attrs.string,
- field: this,
- values: [{label: value, value: value}]
- }
- }]);
- }
-});
-instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{
- complete: function (value) {
- var val = this.parse(value);
- if (isNaN(val)) { return $.when(); }
- var label = _.str.sprintf(
- _t("Search %(field)s for: %(value)s"), {
- field: '<em>' + _.escape(this.attrs.string) + '</em>',
- value: '<strong>' + _.escape(value) + '</strong>'});
- return $.when([{
- label: label,
- facet: {
- category: this.attrs.string,
- field: this,
- values: [{label: value, value: val}]
- }
- }]);
- },
-});
-/**
- * @class
- * @extends instance.web.search.NumberField
- */
-instance.web.search.IntegerField = instance.web.search.NumberField.extend(/** @lends instance.web.search.IntegerField# */{
- error_message: _t("not a valid integer"),
- parse: function (value) {
- try {
- return instance.web.parse_value(value, {'widget': 'integer'});
- } catch (e) {
- return NaN;
- }
- }
-});
-/**
- * @class
- * @extends instance.web.search.NumberField
- */
-instance.web.search.FloatField = instance.web.search.NumberField.extend(/** @lends instance.web.search.FloatField# */{
- error_message: _t("not a valid number"),
- parse: function (value) {
- try {
- return instance.web.parse_value(value, {'widget': 'float'});
- } catch (e) {
- return NaN;
- }
- }
-});
-
-/**
- * Utility function for m2o & selection fields taking a selection/name_get pair
- * (value, name) and converting it to a Facet descriptor
- *
- * @param {instance.web.search.Field} field holder field
- * @param {Array} pair pair value to convert
- */
-function facet_from(field, pair) {
- return {
- field: field,
- category: field.attrs.string,
- values: [{label: pair[1], value: pair[0]}]
- };
-}
-
-/**
- * @class
- * @extends instance.web.search.Field
- */
-instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends instance.web.search.SelectionField# */{
- // This implementation is a basic <select> field, but it may have to be
- // altered to be more in line with the GTK client, which uses a combo box
- // (~ jquery.autocomplete):
- // * If an option was selected in the list, behave as currently
- // * If something which is not in the list was entered (via the text input),
- // the default domain should become (`ilike` string_value) but **any
- // ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
- // is specified. So at least get_domain needs to be quite a bit
- // overridden (if there's no @value and there is no filter_domain and
- // there is no @operator, return [[name, 'ilike', str_val]]
- template: 'SearchView.field.selection',
- init: function () {
- this._super.apply(this, arguments);
- // prepend empty option if there is no empty option in the selection list
- this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
- return !item[1];
- });
- },
- complete: function (needle) {
- var self = this;
- var results = _(this.attrs.selection).chain()
- .filter(function (sel) {
- var value = sel[0], label = sel[1];
- if (value === undefined || !label) { return false; }
- return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
- })
- .map(function (sel) {
- return {
- label: _.escape(sel[1]),
- indent: true,
- facet: facet_from(self, sel)
- };
- }).value();
- if (_.isEmpty(results)) { return $.when(null); }
- return $.when.call(null, [{
- label: _.escape(this.attrs.string)
- }].concat(results));
- },
- facet_for: function (value) {
- var match = _(this.attrs.selection).detect(function (sel) {
- return sel[0] === value;
- });
- if (!match) { return $.when(null); }
- return $.when(facet_from(this, match));
- }
-});
-instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/** @lends instance.web.search.BooleanField# */{
- /**
- * @constructs instance.web.search.BooleanField
- * @extends instance.web.search.BooleanField
- */
- init: function () {
- this._super.apply(this, arguments);
- this.attrs.selection = [
- [true, _t("Yes")],
- [false, _t("No")]
- ];
- }
-});
-/**
- * @class
- * @extends instance.web.search.DateField
- */
-instance.web.search.DateField = instance.web.search.Field.extend(/** @lends instance.web.search.DateField# */{
- value_from: function (facetValue) {
- return instance.web.date_to_str(facetValue.get('value'));
- },
- complete: function (needle) {
- var m = moment(needle);
- if (!m.isValid()) { return $.when(null); }
- var d = m.toDate();
- var date_string = instance.web.format_value(d, this.attrs);
- var label = _.str.sprintf(_.str.escapeHTML(
- _t("Search %(field)s at: %(value)s")), {
- field: '<em>' + _.escape(this.attrs.string) + '</em>',
- value: '<strong>' + date_string + '</strong>'});
- return $.when([{
- label: label,
- facet: {
- category: this.attrs.string,
- field: this,
- values: [{label: date_string, value: d}]
- }
- }]);
- }
-});
-/**
- * Implementation of the ``datetime`` openerp field type:
- *
- * * Uses the same widget as the ``date`` field type (a simple date)
- *
- * * Builds a slighly more complex, it's a datetime range (includes time)
- * spanning the whole day selected by the date widget
- *
- * @class
- * @extends instance.web.DateField
- */
-instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @lends instance.web.search.DateTimeField# */{
- value_from: function (facetValue) {
- return instance.web.datetime_to_str(facetValue.get('value'));
- }
-});
-instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
- default_operator: {},
- init: function (view_section, field, parent) {
- this._super(view_section, field, parent);
- this.model = new instance.web.Model(this.attrs.relation);
- this.searchview = parent;
- },
-
- complete: function (value) {
- if (_.isEmpty(value)) { return $.when(null); }
- var label = _.str.sprintf(_.str.escapeHTML(
- _t("Search %(field)s for: %(value)s")), {
- field: '<em>' + _.escape(this.attrs.string) + '</em>',
- value: '<strong>' + _.escape(value) + '</strong>'});
- return $.when([{
- label: label,
- facet: {
- category: this.attrs.string,
- field: this,
- values: [{label: value, value: value, operator: 'ilike'}]
- },
- expand: this.expand.bind(this),
- }]);
- },
-
- expand: function (needle) {
- var self = this;
- // FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
- var context = instance.web.pyeval.eval(
- 'contexts', [this.searchview.dataset.get_context()]);
- return this.model.call('name_search', [], {
- name: needle,
- args: (typeof this.attrs.domain === 'string') ? [] : this.attrs.domain,
- limit: 8,
- context: context
- }).then(function (results) {
- if (_.isEmpty(results)) { return null; }
- return _(results).map(function (result) {
- return {
- label: _.escape(result[1]),
- facet: facet_from(self, result)
- };
- });
- });
- },
- facet_for: function (value) {
- var self = this;
- if (value instanceof Array) {
- if (value.length === 2 && _.isString(value[1])) {
- return $.when(facet_from(this, value));
- }
- assert(value.length <= 1,
- _t("M2O search fields do not currently handle multiple default values"));
- // there are many cases of {search_default_$m2ofield: [id]}, need
- // to handle this as if it were a single value.
- value = value[0];
- }
- return this.model.call('name_get', [value]).then(function (names) {
- if (_(names).isEmpty()) { return null; }
- return facet_from(self, names[0]);
- });
- },
- value_from: function (facetValue) {
- return facetValue.get('label');
- },
- make_domain: function (name, operator, facetValue) {
- operator = facetValue.get('operator') || operator;
-
- switch(operator){
- case this.default_operator:
- return [[name, '=', facetValue.get('value')]];
- case 'ilike':
- return [[name, 'ilike', facetValue.get('value')]];
- case 'child_of':
- return [[name, 'child_of', facetValue.get('value')]];
- }
- return this._super(name, operator, facetValue);
- },
- get_context: function (facet) {
- var values = facet.values;
- if (_.isEmpty(this.attrs.context) && values.length === 1) {
- var c = {};
- var v = values.at(0);
- if (v.get('operator') !== 'ilike') {
- c['default_' + this.attrs.name] = v.get('value');
- }
- return c;
- }
- return this._super(facet);
- }
-});
-
-instance.web.search.FilterMenu = instance.web.Widget.extend({
- template: 'SearchView.FilterMenu',
- events: {
- 'click .oe-add-filter': function () {
- this.toggle_custom_filter_menu();
- },
- 'click li': function (event) {event.stopImmediatePropagation();},
- 'hidden.bs.dropdown': function () {
- this.toggle_custom_filter_menu(false);
- },
- 'click .oe-add-condition': 'append_proposition',
- 'click .oe-apply-filter': 'commit_search',
- },
- init: function (parent, filters, fields_def) {
- var self = this;
- this._super(parent);
- this.filters = filters || [];
- this.searchview = parent;
- this.propositions = [];
- this.fields_def = fields_def.then(function (data) {
- var fields = {
- id: { string: 'ID', type: 'id', searchable: true }
- };
- _.each(data, function(field_def, field_name) {
- if (field_def.selectable !== false && field_name !== 'id') {
- fields[field_name] = field_def;
- }
- });
- return fields;
- });
- },
- start: function () {
- var self = this;
- this.$menu = this.$('.filters-menu');
- this.$add_filter = this.$('.oe-add-filter');
- this.$apply_filter = this.$('.oe-apply-filter');
- this.$add_filter_menu = this.$('.oe-add-filter-menu');
- _.each(this.filters, function (group) {
- if (group.is_visible()) {
- group.insertBefore(self.$add_filter);
- $('<li class="divider">').insertBefore(self.$add_filter);
- }
- });
- this.append_proposition().then(function (prop) {
- prop.$el.hide();
- });
- },
- update_max_height: function () {
- var max_height = $(window).height() - this.$menu[0].getBoundingClientRect().top - 10;
- this.$menu.css('max-height', max_height);
- },
- toggle_custom_filter_menu: function (is_open) {
- this.$add_filter
- .toggleClass('closed-menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
- .toggleClass('open-menu', is_open);
- this.$add_filter_menu.toggle(is_open);
- if (this.$add_filter.hasClass('closed-menu') && (!this.propositions.length)) {
- this.append_proposition();
- }
- this.$('.oe-filter-condition').toggle(is_open);
- this.update_max_height();
- },
- append_proposition: function () {
- var self = this;
- return this.fields_def.then(function (fields) {
- var prop = new instance.web.search.ExtendedSearchProposition(self, fields);
- self.propositions.push(prop);
- prop.insertBefore(self.$add_filter_menu);
- self.$apply_filter.prop('disabled', false);
- self.update_max_height();
- return prop;
- });
- },
- remove_proposition: function (prop) {
- this.propositions = _.without(this.propositions, prop);
- if (!this.propositions.length) {
- this.$apply_filter.prop('disabled', true);
- }
- prop.destroy();
- },
- commit_search: function () {
- var filters = _.invoke(this.propositions, 'get_filter'),
- filters_widgets = _.map(filters, function (filter) {
- return new my.Filter(filter, this);
- }),
- filter_group = new my.FilterGroup(filters_widgets, this.searchview),
- facets = filters_widgets.map(function (filter) {
- return filter_group.make_facet([filter_group.make_value(filter)]);
- });
- filter_group.insertBefore(this.$add_filter);
- $('<li class="divider">').insertBefore(this.$add_filter);
- this.searchview.query.add(facets, {silent: true});
- this.searchview.query.trigger('reset');
-
- _.invoke(this.propositions, 'destroy');
- this.propositions = [];
- this.append_proposition();
- this.toggle_custom_filter_menu(false);
- },
-});
-
-instance.web.search.GroupByMenu = instance.web.Widget.extend({
- template: 'SearchView.GroupByMenu',
- events: {
- 'click li': function (event) {
- event.stopImmediatePropagation();
- },
- 'hidden.bs.dropdown': function () {
- this.toggle_add_menu(false);
- },
- 'click .add-custom-group a': function () {
- this.toggle_add_menu();
- },
- },
- init: function (parent, groups, fields_def) {
- this._super(parent);
- this.groups = groups || [];
- this.groupable_fields = {};
- this.searchview = parent;
- this.fields_def = fields_def.then(this.proxy('get_groupable_fields'));
- },
- start: function () {
- var self = this;
- this.$menu = this.$('.group-by-menu');
- var divider = this.$menu.find('.divider');
- _.invoke(this.groups, 'insertBefore', divider);
- if (this.groups.length) {
- divider.show();
- }
- this.$add_group = this.$menu.find('.add-custom-group');
- this.fields_def.then(function () {
- self.$menu.append(QWeb.render('GroupByMenuSelector', self));
- self.$add_group_menu = self.$('.oe-add-group');
- self.$group_selector = self.$('.oe-group-selector');
- self.$('.oe-select-group').click(function (event) {
- self.toggle_add_menu(false);
- var field = self.$group_selector.find(':selected').data('name');
- self.add_groupby_to_menu(field);
- });
- });
- },
- get_groupable_fields: function (fields) {
- var self = this,
- groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
-
- _.each(fields, function (field, name) {
- if (field.store && _.contains(groupable_types, field.type)) {
- self.groupable_fields[name] = field;
- }
- });
- },
- toggle_add_menu: function (is_open) {
- this.$add_group
- .toggleClass('closed-menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
- .toggleClass('open-menu', is_open);
- this.$add_group_menu.toggle(is_open);
- if (this.$add_group.hasClass('open-menu')) {
- this.$group_selector.focus();
- }
- },
- add_groupby_to_menu: function (field_name) {
- var filter = new my.Filter({attrs:{
- context:"{'group_by':'" + field_name + "''}",
- name: this.groupable_fields[field_name].string,
- }}, this.searchview);
- var group = new my.FilterGroup([filter], this.searchview),
- divider = this.$('.divider').show();
- group.insertBefore(divider);
- group.toggle(filter);
- },
-});
-
-instance.web.search.FavoriteMenu = instance.web.Widget.extend({
- template: 'SearchView.FavoriteMenu',
- events: {
- 'click li': function (event) {
- event.stopImmediatePropagation();
- },
- 'click .oe-save-search a': function () {
- this.toggle_save_menu();
- },
- 'click .oe-save-name button': 'save_favorite',
- 'hidden.bs.dropdown': 'close_menus',
- },
- init: function (parent, query, target_model, action_id) {
- this._super.apply(this,arguments);
- this.searchview = parent;
- this.query = query;
- this.target_model = target_model;
- this.model = new instance.web.Model('ir.filters');
- this.filters = {};
- this.$filters = {};
- this.action_id = action_id;
- },
- start: function () {
- var self = this;
- this.$save_search = this.$('.oe-save-search');
- this.$save_name = this.$('.oe-save-name');
- this.$inputs = this.$save_name.find('input');
- this.$divider = this.$('.divider');
- this.$inputs.eq(0).val(this.searchview.getParent().title);
- var $shared_filter = this.$inputs.eq(1),
- $default_filter = this.$inputs.eq(2);
- $shared_filter.click(function () {$default_filter.prop('checked', false)});
- $default_filter.click(function () {$shared_filter.prop('checked', false)});
-
- this.query
- .on('remove', function (facet) {
- if (facet.get('is_custom_filter')) {
- self.clear_selection();
- }
- })
- .on('reset', this.proxy('clear_selection'));
- if (!this.action_id) {
- this.prepare_dropdown_menu([]);
- return $.when();
- }
- return this.model.call('get_filters', [this.target_model, this.action_id])
- .done(this.proxy('prepare_dropdown_menu'));
- },
- prepare_dropdown_menu: function (filters) {
- filters.map(this.append_filter.bind(this));
- },
- toggle_save_menu: function (is_open) {
- this.$save_search
- .toggleClass('closed-menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
- .toggleClass('open-menu', is_open);
- this.$save_name.toggle(is_open);
- if (this.$save_search.hasClass('open-menu')) {
- this.$save_name.find('input').first().focus();
- }
- },
- close_menus: function () {
- this.toggle_save_menu(false);
- },
- save_favorite: function () {
- var self = this,
- filter_name = this.$inputs[0].value,
- default_filter = this.$inputs[1].checked,
- shared_filter = this.$inputs[2].checked;
- if (!filter_name.length){
- this.do_warn(_t("Error"), _t("Filter name is required."));
- this.$inputs.first().focus();
- return;
- }
- if (_.chain(this.filters)
- .pluck('name')
- .contains(filter_name).value()) {
- this.do_warn(_t("Error"), _t("Filter with same name already exists."));
- this.$inputs.first().focus();
- return;
- }
- var search = this.searchview.build_search_data(),
- view_manager = this.findAncestor(function (a) {
- return a instanceof instance.web.ViewManager
- }),
- view_context = view_manager ? view_manager.active_view.controller.get_context() : {},
- results = instance.web.pyeval.sync_eval_domains_and_contexts({
- domains: search.domains,
- contexts: search.contexts.concat(view_context || []),
- group_by_seq: search.groupbys || [],
- });
- if (!_.isEmpty(results.group_by)) {
- results.context.group_by = results.group_by;
- }
- // Don't save user_context keys in the custom filter, otherwise end
- // up with e.g. wrong uid or lang stored *and used in subsequent
- // reqs*
- var ctx = results.context;
- _(_.keys(instance.session.user_context)).each(function (key) {
- delete ctx[key];
- });
- var filter = {
- name: filter_name,
- user_id: shared_filter ? false : instance.session.uid,
- model_id: this.searchview.dataset.model,
- context: results.context,
- domain: results.domain,
- is_default: default_filter,
- action_id: this.action_id,
- };
- return this.model.call('create_or_replace', [filter]).done(function (id) {
- filter.id = id;
- self.toggle_save_menu(false);
- self.$save_name.find('input').val('').prop('checked', false);
- self.append_filter(filter);
- self.toggle_filter(filter, true);
- });
- },
- get_default_filter: function () {
- var personal_filter = _.find(this.filters, function (filter) {
- return filter.user_id && filter.is_default;
- });
- if (personal_filter) {
- return personal_filter;
- }
- return _.find(this.filters, function (filter) {
- return !filter.user_id && filter.is_default;
- });
- },
- /**
- * Generates a mapping key (in the filters and $filter mappings) for the
- * filter descriptor object provided (as returned by ``get_filters``).
- *
- * The mapping key is guaranteed to be unique for a given (user_id, name)
- * pair.
- *
- * @param {Object} filter
- * @param {String} filter.name
- * @param {Number|Pair<Number, String>} [filter.user_id]
- * @return {String} mapping key corresponding to the filter
- */
- key_for: function (filter) {
- var user_id = filter.user_id,
- action_id = filter.action_id,
- uid = (user_id instanceof Array) ? user_id[0] : user_id,
- act_id = (action_id instanceof Array) ? action_id[0] : action_id;
- return _.str.sprintf('(%s)(%s)%s', uid, act_id, filter.name);
- },
- /**
- * Generates a :js:class:`~instance.web.search.Facet` descriptor from a
- * filter descriptor
- *
- * @param {Object} filter
- * @param {String} filter.name
- * @param {Object} [filter.context]
- * @param {Array} [filter.domain]
- * @return {Object}
- */
- facet_for: function (filter) {
- return {
- category: _t("Custom Filter"),
- icon: 'fa-star',
- field: {
- get_context: function () { return filter.context; },
- get_groupby: function () { return [filter.context]; },
- get_domain: function () { return filter.domain; }
- },
- _id: filter.id,
- is_custom_filter: true,
- values: [{label: filter.name, value: null}]
- };
- },
- clear_selection: function () {
- this.$('li.selected').removeClass('selected');
- },
- append_filter: function (filter) {
- var self = this,
- key = this.key_for(filter),
- $filter;
-
- this.$divider.show();
- if (key in this.$filters) {
- $filter = this.$filters[key];
- } else {
- this.filters[key] = filter;
- $filter = $('<li></li>')
- .insertBefore(this.$divider)
- .toggleClass('oe_searchview_custom_default', filter.is_default)
- .append($('<a>').text(filter.name));
-
- this.$filters[key] = $filter;
- this.$filters[key].addClass(filter.user_id ? 'oe_searchview_custom_private'
- : 'oe_searchview_custom_public')
- $('<span>')
- .addClass('fa fa-trash-o remove-filter')
- .click(function (event) {
- event.stopImmediatePropagation();
- self.remove_filter(filter, $filter, key);
- })
- .appendTo($filter);
- }
- this.$filters[key].unbind('click').click(function () {
- self.toggle_filter(filter);
- });
- },
- toggle_filter: function (filter, preventSearch) {
- var current = this.query.find(function (facet) {
- return facet.get('_id') === filter.id;
- });
- if (current) {
- this.query.remove(current);
- this.$filters[this.key_for(filter)].removeClass('selected');
- return;
- }
- this.query.reset([this.facet_for(filter)], {
- preventSearch: preventSearch || false});
- this.$filters[this.key_for(filter)].addClass('selected');
- },
- remove_filter: function (filter, $filter, key) {
- var self = this;
- var global_warning = _t("This filter is global and will be removed for everybody if you continue."),
- warning = _t("Are you sure that you want to remove this filter?");
- if (!confirm(filter.user_id ? warning : global_warning)) {
- return;
- }
- this.model.call('unlink', [filter.id]).done(function () {
- $filter.remove();
- delete self.$filters[key];
- delete self.filters[key];
- if (_.isEmpty(self.filters)) {
- self.$divider.hide();
- }
- });
- },
-});
-
-instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{
- template: 'SearchView.extended_search.proposition',
- events: {
- 'change .searchview_extended_prop_field': 'changed',
- 'change .searchview_extended_prop_op': 'operator_changed',
- 'click .searchview_extended_delete_prop': function (e) {
- e.stopPropagation();
- this.getParent().remove_proposition(this);
- },
- },
- /**
- * @constructs instance.web.search.ExtendedSearchProposition
- * @extends instance.web.Widget
- *
- * @param parent
- * @param fields
- */
- init: function (parent, fields) {
- this._super(parent);
- this.fields = _(fields).chain()
- .map(function(val, key) { return _.extend({}, val, {'name': key}); })
- .filter(function (field) { return !field.deprecated && field.searchable; })
- .sortBy(function(field) {return field.string;})
- .value();
- this.attrs = {_: _, fields: this.fields, selected: null};
- this.value = null;
- },
- start: function () {
- return this._super().done(this.proxy('changed'));
- },
- changed: function() {
- var nval = this.$(".searchview_extended_prop_field").val();
- if(this.attrs.selected === null || this.attrs.selected === undefined || nval != this.attrs.selected.name) {
- this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
- }
- },
- operator_changed: function (e) {
- var $value = this.$('.searchview_extended_prop_value');
- switch ($(e.target).val()) {
- case '∃':
- case '∄':
- $value.hide();
- break;
- default:
- $value.show();
- }
- },
- /**
- * Selects the provided field object
- *
- * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
- */
- select_field: function(field) {
- var self = this;
- if(this.attrs.selected !== null && this.attrs.selected !== undefined) {
- this.value.destroy();
- this.value = null;
- this.$('.searchview_extended_prop_op').html('');
- }
- this.attrs.selected = field;
- if(field === null || field === undefined) {
- return;
- }
-
- var type = field.type;
- var Field = instance.web.search.custom_filters.get_object(type);
- if(!Field) {
- Field = instance.web.search.custom_filters.get_object("char");
- }
- this.value = new Field(this, field);
- _.each(this.value.operators, function(operator) {
- $('<option>', {value: operator.value})
- .text(String(operator.text))
- .appendTo(self.$('.searchview_extended_prop_op'));
- });
- var $value_loc = this.$('.searchview_extended_prop_value').show().empty();
- this.value.appendTo($value_loc);
-
- },
- get_filter: function () {
- if (this.attrs.selected === null || this.attrs.selected === undefined)
- return null;
- var field = this.attrs.selected,
- op_select = this.$('.searchview_extended_prop_op')[0],
- operator = op_select.options[op_select.selectedIndex];
-
- return {
- attrs: {
- domain: [this.value.get_domain(field, operator)],
- string: this.value.get_label(field, operator),
- },
- children: [],
- tag: 'filter',
- };
- },
-});
-
-instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend({
- init: function (parent, field) {
- this._super(parent);
- this.field = field;
- },
- get_label: function (field, operator) {
- var format;
- switch (operator.value) {
- case '∃': case '∄': format = _t('%(field)s %(operator)s'); break;
- default: format = _t('%(field)s %(operator)s "%(value)s"'); break;
- }
- return this.format_label(format, field, operator);
- },
- format_label: function (format, field, operator) {
- return _.str.sprintf(format, {
- field: field.string,
- // According to spec, HTMLOptionElement#label should return
- // HTMLOptionElement#text when not defined/empty, but it does
- // not in older Webkit (between Safari 5.1.5 and Chrome 17) and
- // Gecko (pre Firefox 7) browsers, so we need a manual fallback
- // for those
- operator: operator.label || operator.text,
- value: this
- });
- },
- get_domain: function (field, operator) {
- switch (operator.value) {
- case '∃': return this.make_domain(field.name, '!=', false);
- case '∄': return this.make_domain(field.name, '=', false);
- default: return this.make_domain(
- field.name, operator.value, this.get_value());
- }
- },
- make_domain: function (field, operator, value) {
- return [field, operator, value];
- },
- /**
- * Returns a human-readable version of the value, in case the "logical"
- * and the "semantic" values of a field differ (as for selection fields,
- * for instance).
- *
- * The default implementation simply returns the value itself.
- *
- * @return {String} human-readable version of the value
- */
- toString: function () {
- return this.get_value();
- }
-});
-instance.web.search.ExtendedSearchProposition.Char = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.char',
- operators: [
- {value: "ilike", text: _lt("contains")},
- {value: "not ilike", text: _lt("doesn't contain")},
- {value: "=", text: _lt("is equal to")},
- {value: "!=", text: _lt("is not equal to")},
- {value: "∃", text: _lt("is set")},
- {value: "∄", text: _lt("is not set")}
- ],
- get_value: function() {
- return this.$el.val();
- }
-});
-instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.empty',
- operators: [
- {value: "=", text: _lt("is equal to")},
- {value: "!=", text: _lt("is not equal to")},
- {value: ">", text: _lt("greater than")},
- {value: "<", text: _lt("less than")},
- {value: ">=", text: _lt("greater or equal than")},
- {value: "<=", text: _lt("less or equal than")},
- {value: "∃", text: _lt("is set")},
- {value: "∄", text: _lt("is not set")}
- ],
- /**
- * Date widgets live in view_form which is not yet loaded when this is
- * initialized -_-
- */
- widget: function () { return instance.web.DateTimeWidget; },
- get_value: function() {
- return this.datewidget.get_value();
- },
- toString: function () {
- return instance.web.format_value(this.get_value(), { type:"datetime" });
- },
- start: function() {
- var ready = this._super();
- this.datewidget = new (this.widget())(this);
- this.datewidget.appendTo(this.$el);
- return ready;
- }
-});
-instance.web.search.ExtendedSearchProposition.Date = instance.web.search.ExtendedSearchProposition.DateTime.extend({
- widget: function () { return instance.web.DateWidget; },
- toString: function () {
- return instance.web.format_value(this.get_value(), { type:"date" });
- }
-});
-instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.integer',
- operators: [
- {value: "=", text: _lt("is equal to")},
- {value: "!=", text: _lt("is not equal to")},
- {value: ">", text: _lt("greater than")},
- {value: "<", text: _lt("less than")},
- {value: ">=", text: _lt("greater or equal than")},
- {value: "<=", text: _lt("less or equal than")},
- {value: "∃", text: _lt("is set")},
- {value: "∄", text: _lt("is not set")}
- ],
- toString: function () {
- return this.$el.val();
- },
- get_value: function() {
- try {
- var val =this.$el.val();
- return instance.web.parse_value(val === "" ? 0 : val, {'widget': 'integer'});
- } catch (e) {
- return "";
- }
- }
-});
-instance.web.search.ExtendedSearchProposition.Id = instance.web.search.ExtendedSearchProposition.Integer.extend({
- operators: [{value: "=", text: _lt("is")}]
-});
-instance.web.search.ExtendedSearchProposition.Float = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.float',
- operators: [
- {value: "=", text: _lt("is equal to")},
- {value: "!=", text: _lt("is not equal to")},
- {value: ">", text: _lt("greater than")},
- {value: "<", text: _lt("less than")},
- {value: ">=", text: _lt("greater or equal than")},
- {value: "<=", text: _lt("less or equal than")},
- {value: "∃", text: _lt("is set")},
- {value: "∄", text: _lt("is not set")}
- ],
- toString: function () {
- return this.$el.val();
- },
- get_value: function() {
- try {
- var val =this.$el.val();
- return instance.web.parse_value(val === "" ? 0.0 : val, {'widget': 'float'});
- } catch (e) {
- return "";
- }
- }
-});
-instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.selection',
- operators: [
- {value: "=", text: _lt("is")},
- {value: "!=", text: _lt("is not")},
- {value: "∃", text: _lt("is set")},
- {value: "∄", text: _lt("is not set")}
- ],
- toString: function () {
- var select = this.$el[0];
- var option = select.options[select.selectedIndex];
- return option.label || option.text;
- },
- get_value: function() {
- return this.$el.val();
- }
-});
-instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.empty',
- operators: [
- {value: "=", text: _lt("is true")},
- {value: "!=", text: _lt("is false")}
- ],
- get_label: function (field, operator) {
- return this.format_label(
- _t('%(field)s %(operator)s'), field, operator);
- },
- get_value: function() {
- return true;
- }
-});
-
-instance.web.search.custom_filters = new instance.web.Registry({
- 'char': 'instance.web.search.ExtendedSearchProposition.Char',
- 'text': 'instance.web.search.ExtendedSearchProposition.Char',
- 'one2many': 'instance.web.search.ExtendedSearchProposition.Char',
- 'many2one': 'instance.web.search.ExtendedSearchProposition.Char',
- 'many2many': 'instance.web.search.ExtendedSearchProposition.Char',
-
- 'datetime': 'instance.web.search.ExtendedSearchProposition.DateTime',
- 'date': 'instance.web.search.ExtendedSearchProposition.Date',
- 'integer': 'instance.web.search.ExtendedSearchProposition.Integer',
- 'float': 'instance.web.search.ExtendedSearchProposition.Float',
- 'boolean': 'instance.web.search.ExtendedSearchProposition.Boolean',
- 'selection': 'instance.web.search.ExtendedSearchProposition.Selection',
-
- 'id': 'instance.web.search.ExtendedSearchProposition.Id'
-});
-
-instance.web.search.AutoComplete = instance.web.Widget.extend({
- template: "SearchView.autocomplete",
-
- // Parameters for autocomplete constructor:
- //
- // parent: this is used to detect keyboard events
- //
- // options.source: function ({term:query}, callback). This function will be called to
- // obtain the search results corresponding to the query string. It is assumed that
- // options.source will call callback with the results.
- // options.delay: delay in millisecond before calling source. Useful if you don't want
- // to make too many rpc calls
- // options.select: function (ev, {item: {facet:facet}}). Autocomplete widget will call
- // that function when a selection is made by the user
- // options.get_search_string: function (). This function will be called by autocomplete
- // to obtain the current search string.
- init: function (parent, options) {
- this._super(parent);
- this.$input = parent.$el;
- this.source = options.source;
- this.delay = options.delay;
- this.select = options.select,
- this.get_search_string = options.get_search_string;
-
- this.current_result = null;
-
- this.searching = true;
- this.search_string = null;
- this.current_search = null;
- },
- start: function () {
- var self = this;
- this.$input.on('keyup', function (ev) {
- if (ev.which === $.ui.keyCode.RIGHT) {
- self.searching = true;
- ev.preventDefault();
- return;
- }
- // ENTER is caugth at KeyUp rather than KeyDown to avoid firing
- // before all regular keystrokes have been processed
- if (ev.which === $.ui.keyCode.ENTER) {
- if (self.current_result && self.get_search_string().length) {
- self.select_item(ev);
- }
- return;
- }
- var search_string = self.get_search_string();
- if (self.search_string !== search_string) {
- if (search_string.length) {
- self.search_string = search_string;
- setTimeout(function () { self.initiate_search(search_string);}, self.delay);
- } else {
- self.close();
- }
- }
- });
- this.$input.on('keypress', function (ev) {
- self.search_string = self.get_search_string() + String.fromCharCode(ev.which);
- if (self.search_string.length) {
- self.searching = true;
- var search_string = self.search_string;
- setTimeout(function () { self.initiate_search(search_string);}, self.delay);
- } else {
- self.close();
- }
- });
- this.$input.on('keydown', function (ev) {
- switch (ev.which) {
- // TAB and direction keys are handled at KeyDown because KeyUp
- // is not guaranteed to fire.
- // See e.g. https://github.com/aef-/jquery.masterblaster/issues/13
- case $.ui.keyCode.TAB:
- if (self.current_result && self.get_search_string().length) {
- self.select_item(ev);
- }
- break;
- case $.ui.keyCode.DOWN:
- self.move('down');
- self.searching = false;
- ev.preventDefault();
- break;
- case $.ui.keyCode.UP:
- self.move('up');
- self.searching = false;
- ev.preventDefault();
- break;
- case $.ui.keyCode.RIGHT:
- self.searching = false;
- var current = self.current_result
- if (current && current.expand && !current.expanded) {
- self.expand();
- self.searching = true;
- }
- ev.preventDefault();
- break;
- case $.ui.keyCode.ESCAPE:
- self.close();
- self.searching = false;
- break;
- }
- });
- },
- initiate_search: function (query) {
- if (query === this.search_string && query !== this.current_search) {
- this.search(query);
- }
- },
- search: function (query) {
- var self = this;
- this.current_search = query;
- this.source({term:query}, function (results) {
- if (results.length) {
- self.render_search_results(results);
- self.focus_element(self.$('li:first-child'));
- } else {
- self.close();
- }
- });
- },
- render_search_results: function (results) {
- var self = this;
- var $list = this.$('ul');
- $list.empty();
- var render_separator = false;
- results.forEach(function (result) {
- if (result.is_separator) {
- if (render_separator)
- $list.append($('<li>').addClass('oe-separator'));
- render_separator = false;
- } else {
- var $item = self.make_list_item(result).appendTo($list);
- result.$el = $item;
- render_separator = true;
- }
- });
- this.show();
- },
- make_list_item: function (result) {
- var self = this;
- var $li = $('<li>')
- .hover(function (ev) {self.focus_element($li);})
- .mousedown(function (ev) {
- if (ev.button === 0) { // left button
- self.select(ev, {item: {facet: result.facet}});
- self.close();
- } else {
- ev.preventDefault();
- }
- })
- .data('result', result);
- if (result.expand) {
- var $expand = $('<span class="oe-expand">').text('â–¶').appendTo($li);
- $expand.mousedown(function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
- if (result.expanded)
- self.fold();
- else
- self.expand();
- });
- result.expanded = false;
- }
- if (result.indent) $li.addClass('oe-indent');
- $li.append($('<span>').html(result.label));
- return $li;
- },
- expand: function () {
- var self = this;
- this.current_result.expand(this.get_search_string()).then(function (results) {
- (results || [{label: '(no result)'}]).reverse().forEach(function (result) {
- result.indent = true;
- var $li = self.make_list_item(result);
- self.current_result.$el.after($li);
- });
- self.current_result.expanded = true;
- self.current_result.$el.find('span.oe-expand').html('â–¼');
- });
- },
- fold: function () {
- var $next = this.current_result.$el.next();
- while ($next.hasClass('oe-indent')) {
- $next.remove();
- $next = this.current_result.$el.next();
- }
- this.current_result.expanded = false;
- this.current_result.$el.find('span.oe-expand').html('â–¶');
- },
- focus_element: function ($li) {
- this.$('li').removeClass('oe-selection-focus');
- $li.addClass('oe-selection-focus');
- this.current_result = $li.data('result');
- },
- select_item: function (ev) {
- if (this.current_result.facet) {
- this.select(ev, {item: {facet: this.current_result.facet}});
- this.close();
- }
- },
- show: function () {
- this.$el.show();
- },
- close: function () {
- this.current_search = null;
- this.search_string = null;
- this.searching = true;
- this.$el.hide();
- },
- move: function (direction) {
- var $next;
- if (direction === 'down') {
- $next = this.$('li.oe-selection-focus').nextAll(':not(.oe-separator)').first();
- if (!$next.length) $next = this.$('li:first-child');
- } else {
- $next = this.$('li.oe-selection-focus').prevAll(':not(.oe-separator)').first();
- if (!$next.length) $next = this.$('li:not(.oe-separator)').last();
- }
- this.focus_element($next);
- },
- is_expandable: function () {
- return !!this.$('.oe-selection-focus .oe-expand').length;
- },
-});
-
-})();
diff --git a/addons/web/static/src/js/test_support.js b/addons/web/static/src/js/test_support.js
deleted file mode 100644
index 006ccc580ed..00000000000
--- a/addons/web/static/src/js/test_support.js
+++ /dev/null
@@ -1,83 +0,0 @@
-(function() {
-
-openerp.test_support = {
-
- setup_session: function (session) {
- var origin = location.protocol+"//"+location.host;
- _.extend(session, {
- origin: origin,
- prefix: origin,
- server: origin, // keep chs happy
- //openerp.web.qweb.default_dict['_s'] = this.origin;
- rpc_function: session.rpc_json,
- session_id: false,
- uid: false,
- username: false,
- user_context: {},
- db: false,
-// this.module_list = openerp._modules.slice();
-// this.module_loaded = {};
-// _(this.module_list).each(function (mod) {
-// self.module_loaded[mod] = true;
-// });
- context: {},
- shortcuts: [],
- active_id: null
- });
- return session.session_reload();
- },
- module: function (title, tested_core, nonliterals) {
- var conf = QUnit.config.openerp = {};
- QUnit.module(title, {
- setup: function () {
- QUnit.stop();
- var oe = conf.openerp = window.openerp.init();
- window.openerp.web[tested_core](oe);
- var done = openerp.test_support.setup_session(oe.session);
- if (nonliterals) {
- done = done.then(function () {
- return oe.session.rpc('/tests/add_nonliterals', {
- domains: nonliterals.domains || [],
- contexts: nonliterals.contexts || []
- }).done(function (r) {
- oe.domains = r.domains;
- oe.contexts = r.contexts;
- });
- });
- }
- done.always(QUnit.start)
- .done(function () {
- conf.openerp = oe;
- }).fail(function (e) {
- QUnit.test(title, function () {
- console.error(e);
- QUnit.ok(false, 'Could not obtain a session:' + e.debug);
- });
- });
- }
- });
- },
- test: function (title, fn) {
- var conf = QUnit.config.openerp;
- QUnit.test(title, function () {
- QUnit.stop();
- fn(conf.openerp);
- });
- },
- expect: function (promise, fn) {
- promise.always(QUnit.start)
- .done(function () { QUnit.ok(false, 'RPC requests should not succeed'); })
- .fail(function (e) {
- if (e.code !== 200) {
- QUnit.equal(e.code, 200, 'Testing connector should raise RPC faults');
- if (typeof console !== 'undefined' && console.error) {
- console.error(e.data.debug);
- }
- return;
- }
- fn(e.data.name);
- });
- }
-};
-
-})();
diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js
deleted file mode 100644
index 5e9483addc1..00000000000
--- a/addons/web/static/src/js/testing.js
+++ /dev/null
@@ -1,304 +0,0 @@
-// Test support structures and methods for OpenERP
-openerp.testing = {};
-(function (testing) {
- var dependencies = {
- pyeval: [],
- core: ['pyeval'],
- data: ['core'],
- dates: [],
- formats: ['core', 'dates'],
- chrome: ['core'],
- views: ['core', 'data', 'chrome'],
- search: ['views', 'formats'],
- list: ['views', 'data'],
- form: ['data', 'views', 'list', 'formats'],
- list_editable: ['list', 'form', 'data'],
- };
-
- var initialized = [];
- /**
- * openerp.init is a broken-ass piece of shit, makes it impossible to
- * progressively add modules, using it, retarded openerp_inited flag
- * means the first tests being run get their dependencies loaded
- * (basically just base and web), and for *every other module* the tests
- * run without the dependencies being correctly loaded
- */
- function init(modules) {
- if (!initialized.length) {
- openerp.init(modules);
- initialized = openerp._modules.slice();
- return;
- }
- var to_initialize = _.difference(modules, initialized);
- for (var i = 0; i < to_initialize.length; i++) {
- var modname = to_initialize[i];
- var fct = openerp[modname];
- if (typeof fct === 'function') {
- var module = openerp[modname] = {};
- for(var k in fct) {
- if (!fct.hasOwnProperty(k)) { continue; }
- module[k] = fct[k];
- }
- fct(openerp, module)
- }
- initialized.push(modname);
- openerp._modules.push(modname);
- }
- }
-
- testing.dependencies = window['oe_all_dependencies'] || [];
- testing.current_module = null;
- /**
- * Function which does not do anything
- */
- testing.noop = function () { };
- /**
- * Alter provided instance's ``session`` attribute to make response
- * mockable:
- *
- * * The ``responses`` parameter can be used to provide a map of (RPC)
- * paths (e.g. ``/web/view/load``) to a function returning a response
- * to the query.
- * * ``instance.session`` grows a ``responses`` attribute which is
- * a map of the same (and is in fact initialized to the ``responses``
- * parameter if one is provided)
- *
- * Note that RPC requests to un-mocked URLs will be rejected with an
- * error message: only explicitly specified urls will get a response.
- *
- * Mocked sessions will *never* perform an actual RPC connection.
- *
- * @param instance openerp instance being initialized
- * @param {Object} [responses]
- */
- testing.mockifyRPC = function (instance, responses) {
- var session = instance.session;
- session.responses = responses || {};
- session.rpc = function(url, rparams, options) {
- if (_.isString(url)) {
- url = {url: url};
- }
- var fn, params;
- var needle = rparams.model + ':' + rparams.method;
- if (url.url.substr(0, 20) === '/web/dataset/call_kw' && needle in this.responses) {
- fn = this.responses[needle];
- params = [
- rparams.args || [],
- rparams.kwargs || {}
- ];
- } else {
- fn = this.responses[url.url];
- params = [{params: rparams}];
- }
-
- if (!fn) {
- return $.Deferred().reject({}, 'failed',
- _.str.sprintf("Url %s not found in mock responses, with arguments %s",
- url.url, JSON.stringify(rparams))
- ).promise();
- }
- try {
- return $.when(fn.apply(null, params)).then(function (result) {
- // Wrap for RPC layer unwrapper thingy
- return result;
- });
- } catch (e) {
- // not sure why this looks like that
- return $.Deferred().reject({}, 'failed', String(e));
- }
- };
- };
-
- var StackProto = {
- execute: function (fn) {
- var args = [].slice.call(arguments, 1);
- // Warning: here be dragons
- var i = 0, setups = this.setups, teardowns = this.teardowns;
- var d = $.Deferred();
-
- var succeeded, failed;
- var success = function () {
- succeeded = _.toArray(arguments);
- return teardown();
- };
- var failure = function () {
- // save first failure
- if (!failed) {
- failed = _.toArray(arguments);
- }
- // chain onto next teardown
- return teardown();
- };
-
- var setup = function () {
- // if setup to execute
- if (i < setups.length) {
- var f = setups[i] || testing.noop;
- $.when(f.apply(null, args)).then(function () {
- ++i;
- setup();
- }, failure);
- } else {
- $.when(fn.apply(null, args)).then(success, failure);
- }
- };
- var teardown = function () {
- // if teardown to execute
- if (i > 0) {
- var f = teardowns[--i] || testing.noop;
- $.when(f.apply(null, args)).then(teardown, failure);
- } else {
- if (failed) {
- d.reject.apply(d, failed);
- } else if (succeeded) {
- d.resolve.apply(d, succeeded);
- } else {
- throw new Error("Didn't succeed or fail?");
- }
- }
- };
- setup();
-
- return d;
- },
- push: function (setup, teardown) {
- return _.extend(Object.create(StackProto), {
- setups: this.setups.concat([setup]),
- teardowns: this.teardowns.concat([teardown])
- });
- },
- unshift: function (setup, teardown) {
- return _.extend(Object.create(StackProto), {
- setups: [setup].concat(this.setups),
- teardowns: [teardown].concat(this.teardowns)
- });
- }
- };
- /**
- *
- * @param {Function} [setup]
- * @param {Function} [teardown]
- * @return {*}
- */
- testing.Stack = function (setup, teardown) {
- return _.extend(Object.create(StackProto), {
- setups: setup ? [setup] : [],
- teardowns: teardown ? [teardown] : []
- });
- };
-
- testing.section = function (name, options, body) {
- if (_.isFunction(options)) {
- body = options;
- options = {};
- }
- _.defaults(options, {
- setup: testing.noop,
- teardown: testing.noop
- });
-
- QUnit.module(name, {_oe: options});
- body(testing['case']);
- };
- testing['case'] = function (name, options, callback) {
- if (_.isFunction(options)) {
- callback = options;
- options = {};
- }
-
- var module = testing.current_module;
- var module_index = _.indexOf(testing.dependencies, module);
- var module_deps = testing.dependencies.slice(
- // If module not in deps (because only tests, no JS) -> indexOf
- // returns -1 -> index becomes 0 -> replace with ``undefined`` so
- // Array#slice returns a full copy
- 0, module_index + 1 || undefined);
-
- // Serialize options for this precise test case
- var env = QUnit.config.currentModuleTestEnvironment;
- // section setup
- // case setup
- // test
- // case teardown
- // section teardown
- var case_stack = testing.Stack()
- .push(env._oe.setup, env._oe.teardown)
- .push(options.setup, options.teardown);
- var opts = _.defaults({}, options, env._oe);
-
- QUnit.test(name, function () {
- var instance = openerp;
- init(module_deps);
- instance.session = new instance.web.Session();
- instance.session.uid = 42;
- if (_.isNumber(opts.asserts)) {
- expect(opts.asserts);
- }
-
- var $fixture = $('#qunit-fixture');
-
- var mock, async = false;
- switch (opts.rpc) {
- case 'mock':
- async = true;
- testing.mockifyRPC(instance);
- mock = function (spec, handler) {
- instance.session.responses[spec] = handler;
- };
- break;
- }
-
- // Always execute tests asynchronously
- stop();
- var timeout;
- case_stack.execute(function () {
- var result = callback.apply(null, arguments);
- if (!(result && _.isFunction(result.then))) {
- if (async) {
- ok(false, "asynchronous test cases must return a promise");
- }
- } else {
- if (!_.isNumber(opts.asserts)) {
- ok(false, "asynchronous test cases must specify the "
- + "number of assertions they expect");
- }
- }
-
- return $.Deferred(function (d) {
- $.when(result).then(function () {
- d.resolve.apply(d, arguments);
- }, function () {
- d.reject.apply(d, arguments);
- });
- if (async || (result && result.then)) {
- // async test can be either implicit async (rpc) or
- // promise-returning
- timeout = setTimeout(function () {
- QUnit.config.semaphore = 1;
- d.reject({message: "Test timed out"});
- }, 2000);
- }
- });
- }, instance, $fixture, mock).always(function () {
- if (timeout) { clearTimeout(timeout); }
- start();
- }).fail(function (error) {
- if (options.fail_on_rejection === false) {
- return;
- }
- var message;
- if (typeof error !== 'object'
- || typeof error.message !== 'string') {
- message = JSON.stringify([].slice.apply(arguments));
- } else {
- message = error.message;
- if (error.data && error.data.debug) {
- message += '\n\n' + error.data.debug;
- }
- }
-
- ok(false, message);
- });
- });
- };
-})(openerp.testing);
diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js
index efc46c00d6e..7ee3e43f049 100644
--- a/addons/web/static/src/js/tour.js
+++ b/addons/web/static/src/js/tour.js
@@ -1,24 +1,29 @@
-(function () {
- 'use strict';
+odoo.define('web.Tour', ['web.core'], function (require) {
+"use strict";
-// raise an error in test mode if openerp don't exist
-if (typeof openerp === "undefined") {
- var error = "openerp is undefined"
- + "\nhref: " + window.location.href
- + "\nreferrer: " + document.referrer
- + "\nlocalStorage: " + window.localStorage.getItem("tour");
- if (typeof $ !== "undefined") {
- error += '\n\n' + $("body").html();
- }
- throw new Error(error);
-}
+var core = require('web.core');
-var website = openerp.website;
+var _t = core._t;
+var qweb = core.qweb;
-// don't rewrite T in test mode
-if (typeof openerp.Tour !== "undefined") {
- return;
-}
+// make this work
+// raise an error in test mode if openerp don't exist
+// if (typeof openerp === "undefined") {
+// var error = "openerp is undefined"
+// + "\nhref: " + window.location.href
+// + "\nreferrer: " + document.referrer
+// + "\nlocalStorage: " + window.localStorage.getItem("tour");
+// if (typeof $ !== "undefined") {
+// error += '\n\n' + $("body").html();
+// }
+// throw new Error(error);
+// }
+
+
+// // don't rewrite T in test mode
+// if (typeof openerp.Tour !== "undefined") {
+// return;
+// }
/////////////////////////////////////////////////
@@ -130,8 +135,8 @@ var Tour = {
id: index,
waitNot: step.waitNot,
waitFor: step.waitFor,
- title: openerp._t("Move your mouse"),
- content: openerp._t("Move your mouse here to open the insert block"),
+ title: _t("Move your mouse"),
+ content: _t("Move your mouse here to open the insert block"),
element: '#oe_snippets',
onend: function () {
$("#oe_snippets").addClass("o_open");
@@ -321,7 +326,7 @@ var Tour = {
Tour._load_template = true;
if (typeof QWeb2 === "undefined") return $.when();
var def = $.Deferred();
- openerp.qweb.add_template('/web/static/src/xml/website.tour.xml', function(err) {
+ qweb.add_template('/web/static/src/xml/website.tour.xml', function(err) {
if (err) {
def.reject(err);
} else {
@@ -331,10 +336,10 @@ var Tour = {
return def;
},
popoverTitle: function (tour, options) {
- return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover_title', options) : options.title;
+ return typeof QWeb2 !== "undefined" ? qweb.render('tour.popover_title', options) : options.title;
},
popover: function (options) {
- return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover', options) : options.title;
+ return typeof QWeb2 !== "undefined" ? qweb.render('tour.popover', options) : options.title;
},
getLang: function () {
return $("html").attr("lang") ? "/" + $("html").attr("lang").replace(/-/, '_') : "";
@@ -697,10 +702,12 @@ var Tour = {
}
}
};
-openerp.Tour = Tour;
+// openerp.Tour = Tour;
/////////////////////////////////////////////////
$(document).ready(Tour.running);
-}());
+return Tour;
+
+});
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
deleted file mode 100644
index bdd1ecb6225..00000000000
--- a/addons/web/static/src/js/view_form.js
+++ /dev/null
@@ -1,6443 +0,0 @@
-(function() {
-
-var instance = openerp;
-var _t = instance.web._t,
- _lt = instance.web._lt;
-var QWeb = instance.web.qweb;
-
-/** @namespace */
-instance.web.form = {};
-
-/**
- * Interface implemented by the form view or any other object
- * able to provide the features necessary for the fields to work.
- *
- * Properties:
- * - display_invalid_fields : if true, all fields where is_valid() return true should
- * be displayed as invalid.
- * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
- * Events:
- * - view_content_has_changed : when the values of the fields have changed. When
- * this event is triggered all fields should reprocess their modifiers.
- * - field_changed:<field_name> : when the value of a field change, an event is triggered
- * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
- * This event is not related to the on_change mechanism of OpenERP and is always called
- * when the value of a field is setted or changed. This event is only triggered when the
- * value of the field is syntactically valid, but it can be triggered when the value
- * is sematically invalid (ie, when a required field is false). It is possible that an event
- * about a precise field is never triggered even if that field exists in the view, in that
- * case the value of the field is assumed to be false.
- */
-instance.web.form.FieldManagerMixin = {
- /**
- * Must return the asked field as in fields_get.
- */
- get_field_desc: function(field_name) {},
- /**
- * Returns the current value of a field present in the view. See the get_value() method
- * method in FieldInterface for further information.
- */
- get_field_value: function(field_name) {},
- /**
- Gives new values for the fields contained in the view. The new values could not be setted
- right after the call to this method. Setting new values can trigger on_changes.
-
- @param {Object} values A dictonary with key = field name and value = new value.
- @return {$.Deferred} Is resolved after all the values are setted.
- */
- set_values: function(values) {},
- /**
- Computes an OpenERP domain.
-
- @param {Array} expression An OpenERP domain.
- @return {boolean} The computed value of the domain.
- */
- compute_domain: function(expression) {},
- /**
- Builds an evaluation context for the resolution of the fields' contexts. Please note
- the field are only supposed to use this context to evualuate their own, they should not
- extend it.
-
- @return {CompoundContext} An OpenERP context.
- */
- build_eval_context: function() {},
-};
-
-instance.web.views.add('form', 'instance.web.FormView');
-/**
- * Properties:
- * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
- * the mode used by the view.
- */
-instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
- /**
- * Indicates that this view is not searchable, and thus that no search
- * view should be displayed (if there is one active).
- */
- searchable: false,
- template: "FormView",
- display_name: _lt('Form'),
- view_type: "form",
- /**
- * @constructs instance.web.FormView
- * @extends instance.web.View
- *
- * @param {instance.web.Session} session the current openerp session
- * @param {instance.web.DataSet} dataset the dataset this view will work with
- * @param {String} view_id the identifier of the OpenERP view object
- *
- * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
- */
- init: function(parent, dataset, view_id, options) {
- var self = this;
- this._super(parent);
- this.ViewManager = parent;
- this.set_default_options(options);
- this.dataset = dataset;
- this.model = dataset.model;
- this.view_id = view_id || false;
- this.fields_view = {};
- this.fields = {};
- this.fields_order = [];
- this.datarecord = {};
- this._onchange_specs = {};
- this.onchanges_mutex = new $.Mutex();
- this.default_focus_field = null;
- this.default_focus_button = null;
- this.fields_registry = instance.web.form.widgets;
- this.tags_registry = instance.web.form.tags;
- this.widgets_registry = instance.web.form.custom_widgets;
- this.has_been_loaded = $.Deferred();
- this.translatable_fields = [];
- _.defaults(this.options, {
- "not_interactible_on_create": false,
- "initial_mode": "view",
- "disable_autofocus": false,
- "footer_to_buttons": false,
- });
- this.is_initialized = $.Deferred();
- this.mutating_mutex = new $.Mutex();
- this.save_list = [];
- this.render_value_defs = [];
- this.reload_mutex = new $.Mutex();
- this.__clicked_inside = false;
- this.__blur_timeout = null;
- this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
- self.set({actual_mode: self.options.initial_mode});
- this.has_been_loaded.done(function() {
- self._build_onchange_specs();
- self.on("change:actual_mode", self, self.check_actual_mode);
- self.check_actual_mode();
- self.on("change:actual_mode", self, self.init_pager);
- self.init_pager();
- });
- self.on("load_record", self, self.load_record);
- instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
- if (!this.can_be_discarded()) {
- e.preventDefault();
- }
- });
- },
- view_loading: function(r) {
- return this.load_form(r);
- },
- destroy: function() {
- _.each(this.get_widgets(), function(w) {
- w.off('focused blurred');
- w.destroy();
- });
- if (this.$el) {
- this.$el.off('.formBlur');
- }
- this._super();
- },
- load_form: function(data) {
- var self = this;
- if (!data) {
- throw new Error(_t("No data provided."));
- }
- if (this.arch) {
- throw "Form view does not support multiple calls to load_form";
- }
- this.fields_order = [];
- this.fields_view = data;
-
- this.rendering_engine.set_fields_registry(this.fields_registry);
- this.rendering_engine.set_tags_registry(this.tags_registry);
- this.rendering_engine.set_widgets_registry(this.widgets_registry);
- this.rendering_engine.set_fields_view(data);
- var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
- this.rendering_engine.render_to($dest);
-
- this.$el.on('mousedown.formBlur', function () {
- self.__clicked_inside = true;
- });
-
- this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
- if (this.options.$buttons) {
- this.$buttons.appendTo(this.options.$buttons);
- } else {
- this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
- }
- this.$buttons.on('click', '.oe_form_button_create',
- this.guard_active(this.on_button_create));
- this.$buttons.on('click', '.oe_form_button_edit',
- this.guard_active(this.on_button_edit));
- this.$buttons.on('click', '.oe_form_button_save',
- this.guard_active(this.on_button_save));
- this.$buttons.on('click', '.oe_form_button_cancel',
- this.guard_active(this.on_button_cancel));
- if (this.options.footer_to_buttons) {
- this.$el.find('footer').appendTo(this.$buttons);
- }
-
- this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
- if (!this.sidebar && this.options.$sidebar) {
- this.sidebar = new instance.web.Sidebar(this);
- this.sidebar.appendTo(this.$sidebar);
- if (this.fields_view.toolbar) {
- this.sidebar.add_toolbar(this.fields_view.toolbar);
- }
- this.sidebar.add_items('other', _.compact([
- self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
- self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
- ]));
- }
-
- this.has_been_loaded.resolve();
-
- // Add bounce effect on button 'Edit' when click on readonly page view.
- this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
- if(self.get("actual_mode") == "view") {
- var $button = self.options.$buttons.find(".oe_form_button_edit");
- $button.openerpBounce();
- e.stopPropagation();
- instance.web.bus.trigger('click', e);
- }
- });
- //bounce effect on red button when click on statusbar.
- this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
- if((self.get("actual_mode") == "view")) {
- var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
- $button.openerpBounce();
- e.stopPropagation();
- }
- });
- this.trigger('form_view_loaded', data);
- return $.when();
- },
- widgetFocused: function() {
- // Clear click flag if used to focus a widget
- this.__clicked_inside = false;
- if (this.__blur_timeout) {
- clearTimeout(this.__blur_timeout);
- this.__blur_timeout = null;
- }
- },
- widgetBlurred: function() {
- if (this.__clicked_inside) {
- // clicked in an other section of the form (than the currently
- // focused widget) => just ignore the blurring entirely?
- this.__clicked_inside = false;
- return;
- }
- var self = this;
- // clear timeout, if any
- this.widgetFocused();
- this.__blur_timeout = setTimeout(function () {
- self.trigger('blurred');
- }, 0);
- },
-
- do_load_state: function(state, warm) {
- if (state.id && this.datarecord.id != state.id) {
- if (this.dataset.get_id_index(state.id) === null) {
- this.dataset.ids.push(state.id);
- }
- this.dataset.select_id(state.id);
- this.do_show();
- }
- },
- /**
- *
- * @param {Object} [options]
- * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
- * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
- * @return {$.Deferred}
- */
- do_show: function (options) {
- var self = this;
- options = options || {};
- if (this.sidebar) {
- this.sidebar.$el.show();
- }
- if (this.$buttons) {
- this.$buttons.show();
- }
- this.$el.show().css({
- opacity: '0',
- filter: 'alpha(opacity = 0)'
- });
- this.$el.add(this.$buttons).removeClass('oe_form_dirty');
-
- var shown = this.has_been_loaded;
- if (options.reload !== false) {
- shown = shown.then(function() {
- if (self.dataset.index === null) {
- // null index means we should start a new record
- return self.on_button_new();
- }
- var fields = _.keys(self.fields_view.fields);
- fields.push('display_name');
- return self.dataset.read_index(fields, {
- context: { 'bin_size': true, 'future_display_name' : true }
- }).then(function(r) {
- self.trigger('load_record', r);
- });
- });
- }
- return $.when(shown, this._super()).then(function() {
- self._actualize_mode(options.mode || self.options.initial_mode);
- self.$el.css({
- opacity: '1',
- filter: 'alpha(opacity = 100)'
- });
- instance.web.bus.trigger('form_view_shown', self);
- });
- },
- do_hide: function () {
- if (this.sidebar) {
- this.sidebar.$el.hide();
- }
- if (this.$pager) {
- this.$pager.hide();
- }
- this._super();
- },
- load_record: function(record) {
- var self = this, set_values = [];
- if (!record) {
- this.set({ 'title' : undefined });
- this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
- return $.Deferred().reject();
- }
- this.datarecord = record;
- this._actualize_mode();
- this.set({ 'title' : record.id ? record.display_name : _t("New") });
-
- _(this.fields).each(function (field, f) {
- field._dirty_flag = false;
- field._inhibit_on_change_flag = true;
- var result = field.set_value(self.datarecord[f] || false);
- field._inhibit_on_change_flag = false;
- set_values.push(result);
- });
- return $.when.apply(null, set_values).then(function() {
- if (!record.id) {
- // trigger onchanges
- self.do_onchange(null);
- }
- self.on_form_changed();
- self.rendering_engine.init_fields();
- self.is_initialized.resolve();
- self.do_update_pager(record.id === null || record.id === undefined);
- if (self.sidebar) {
- self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
- }
- if (record.id) {
- self.do_push_state({id:record.id});
- } else {
- self.do_push_state({});
- }
- self.$el.add(self.$buttons).removeClass('oe_form_dirty');
- self.autofocus();
- });
- },
- /**
- * Loads and sets up the default values for the model as the current
- * record
- *
- * @return {$.Deferred}
- */
- load_defaults: function () {
- var self = this;
- var keys = _.keys(this.fields_view.fields);
- if (keys.length) {
- return this.dataset.default_get(keys).then(function(r) {
- self.trigger('load_record', r);
- });
- }
- return self.trigger('load_record', {});
- },
- on_form_changed: function() {
- this.trigger("view_content_has_changed");
- },
- do_notify_change: function() {
- this.$el.add(this.$buttons).addClass('oe_form_dirty');
- },
- execute_pager_action: function(action) {
- if (this.can_be_discarded()) {
- switch (action) {
- case 'first':
- this.dataset.index = 0;
- break;
- case 'previous':
- this.dataset.previous();
- break;
- case 'next':
- this.dataset.next();
- break;
- case 'last':
- this.dataset.index = this.dataset.ids.length - 1;
- break;
- }
- var def = this.reload();
- this.trigger('pager_action_executed');
- return def;
- }
- return $.when();
- },
- init_pager: function() {
- var self = this;
- if (this.$pager)
- this.$pager.remove();
- if (this.get("actual_mode") === "create")
- return;
- this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
- if (this.options.$pager) {
- this.$pager.appendTo(this.options.$pager);
- } else {
- this.$el.find('.oe_form_pager').replaceWith(this.$pager);
- }
- this.$pager.on('click','a[data-pager-action]',function() {
- var $el = $(this);
- if ($el.attr("disabled"))
- return;
- var action = $el.data('pager-action');
- var def = $.when(self.execute_pager_action(action));
- $el.attr("disabled");
- def.always(function() {
- $el.removeAttr("disabled");
- });
- });
- this.do_update_pager();
- },
- do_update_pager: function(hide_index) {
- this.$pager.toggle(this.dataset.ids.length > 1);
- if (hide_index) {
- $(".oe_form_pager_state", this.$pager).html("");
- } else {
- $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
- }
- },
-
- _build_onchange_specs: function() {
- var self = this;
- var find = function(field_name, root) {
- var fields = [root];
- while (fields.length) {
- var node = fields.pop();
- if (!node) {
- continue;
- }
- if (node.tag === 'field' && node.attrs.name === field_name) {
- return node.attrs.on_change || "";
- }
- fields = _.union(fields, node.children);
- }
- return "";
- };
-
- self._onchange_fields = [];
- self._onchange_specs = {};
- _.each(this.fields, function(field, name) {
- self._onchange_fields.push(name);
- self._onchange_specs[name] = find(name, field.node);
- _.each(field.field.views, function(view) {
- _.each(view.fields, function(_, subname) {
- self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
- });
- });
- });
- },
- _get_onchange_values: function() {
- var field_values = this.get_fields_values();
- if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
- delete field_values.id;
- }
- if (this.dataset.parent_view) {
- // this belongs to a parent view: add parent field if possible
- var parent_view = this.dataset.parent_view;
- var child_name = this.dataset.child_name;
- var parent_name = parent_view.get_field_desc(child_name).relation_field;
- if (parent_name) {
- // consider all fields except the inverse of the parent field
- var parent_values = parent_view.get_fields_values();
- delete parent_values[child_name];
- field_values[parent_name] = parent_values;
- }
- }
- return field_values;
- },
-
- do_onchange: function(widget) {
- var self = this;
- var onchange_specs = self._onchange_specs;
- try {
- var def = $.when({});
- var change_spec = widget ? onchange_specs[widget.name] : null;
- if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
- var ids = [],
- trigger_field_name = widget ? widget.name : self._onchange_fields,
- values = self._get_onchange_values(),
- context = new instance.web.CompoundContext(self.dataset.get_context());
-
- if (widget && widget.build_context()) {
- context.add(widget.build_context());
- }
- if (self.dataset.parent_view) {
- var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
- context.add({field_parent: parent_name});
- }
-
- if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
- // In case of a o2m virtual id, we should pass an empty ids list
- ids.push(self.datarecord.id);
- }
- def = self.alive(new instance.web.Model(self.dataset.model).call(
- "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
- }
- this.onchanges_mutex.exec(function(){
- return def.then(function(response) {
- var fields = {};
- if (widget){
- fields[widget.name] = widget.field;
- }
- else{
- fields = self.fields_view.fields;
- }
- var defs = [];
- _.each(fields, function(field, fieldname){
- if (field && field.change_default) {
- var value_;
- if (response.value && (fieldname in response.value)) {
- // Use value from onchange if onchange executed
- value_ = response.value[fieldname];
- } else {
- // otherwise get form value for field
- value_ = self.fields[fieldname].get_value();
- }
- var condition = fieldname + '=' + value_;
-
- if (value_) {
- defs.push(self.alive(new instance.web.Model('ir.values').call(
- 'get_defaults', [self.model, condition]
- )).then(function (results) {
- if (!results.length) {
- return response;
- }
- if (!response.value) {
- response.value = {};
- }
- for(var i=0; i<results.length; ++i) {
- // [whatever, key, value]
- var triplet = results[i];
- response.value[triplet[1]] = triplet[2];
- }
- return response;
- }));
- }
- }
- });
- return _.isEmpty(defs) ? response : $.when.apply(null, defs);
- }).then(function(response) {
- return self.on_processed_onchange(response);
- });
- });
- return this.onchanges_mutex.def;
- } catch(e) {
- console.error(e);
- instance.webclient.crashmanager.show_message(e);
- return $.Deferred().reject();
- }
- },
- on_processed_onchange: function(result) {
- try {
- var fields = this.fields;
- _(result.domain).each(function (domain, fieldname) {
- var field = fields[fieldname];
- if (!field) { return; }
- field.node.attrs.domain = domain;
- });
-
- if (!_.isEmpty(result.value)) {
- this._internal_set_values(result.value);
- }
- // FIXME XXX a list of warnings?
- if (!_.isEmpty(result.warning)) {
- new instance.web.Dialog(this, {
- size: 'medium',
- title:result.warning.title,
- buttons: [
- {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
- ]
- }, QWeb.render("CrashManager.warning", result.warning)).open();
- }
-
- return $.Deferred().resolve();
- } catch(e) {
- console.error(e);
- instance.webclient.crashmanager.show_message(e);
- return $.Deferred().reject();
- }
- },
- _process_operations: function() {
- var self = this;
- return this.mutating_mutex.exec(function() {
- function iterate() {
-
- var mutex = new $.Mutex();
- _.each(self.fields, function(field) {
- self.onchanges_mutex.def.then(function(){
- mutex.exec(function(){
- return field.commit_value();
- });
- });
- });
-
- var args = _.toArray(arguments);
- return $.when.apply(null, [mutex.def, self.onchanges_mutex.def]).then(function() {
- var save_obj = self.save_list.pop();
- if (save_obj) {
- return self._process_save(save_obj).then(function() {
- save_obj.ret = _.toArray(arguments);
- return iterate();
- }, function() {
- save_obj.error = true;
- });
- }
- return $.when();
- }).fail(function() {
- self.save_list.pop();
- return $.when();
- });
- }
- return iterate();
- });
- },
- _internal_set_values: function(values) {
- for (var f in values) {
- if (!values.hasOwnProperty(f)) { continue; }
- var field = this.fields[f];
- // If field is not defined in the view, just ignore it
- if (field) {
- var value_ = values[f];
- if (field.get_value() != value_) {
- field._inhibit_on_change_flag = true;
- field.set_value(value_);
- field._inhibit_on_change_flag = false;
- field._dirty_flag = true;
- }
- }
- }
- this.on_form_changed();
- },
- set_values: function(values) {
- var self = this;
- return this.mutating_mutex.exec(function() {
- self._internal_set_values(values);
- });
- },
- /**
- * Ask the view to switch to view mode if possible. The view may not do it
- * if the current record is not yet saved. It will then stay in create mode.
- */
- to_view_mode: function() {
- this._actualize_mode("view");
- },
- /**
- * Ask the view to switch to edit mode if possible. The view may not do it
- * if the current record is not yet saved. It will then stay in create mode.
- */
- to_edit_mode: function() {
- this.onchanges_mutex = new $.Mutex();
- this._actualize_mode("edit");
- },
- /**
- * Ask the view to switch to a precise mode if possible. The view is free to
- * not respect this command if the state of the dataset is not compatible with
- * the new mode. For example, it is not possible to switch to edit mode if
- * the current record is not yet saved in database.
- *
- * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
- * undefined the view will test the actual mode to check if it is still consistent
- * with the dataset state.
- */
- _actualize_mode: function(switch_to) {
- var mode = switch_to || this.get("actual_mode");
- if (! this.datarecord.id) {
- mode = "create";
- } else if (mode === "create") {
- mode = "edit";
- }
- this.render_value_defs = [];
- this.set({actual_mode: mode});
- },
- check_actual_mode: function(source, options) {
- var self = this;
- if(this.get("actual_mode") === "view") {
- self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
- self.$buttons.find('.oe_form_buttons_edit').hide();
- self.$buttons.find('.oe_form_buttons_view').show();
- self.$sidebar.show();
- } else {
- self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
- self.$buttons.find('.oe_form_buttons_edit').show();
- self.$buttons.find('.oe_form_buttons_view').hide();
- self.$sidebar.hide();
- this.autofocus();
- }
- },
- autofocus: function() {
- if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
- var fields_order = this.fields_order.slice(0);
- if (this.default_focus_field) {
- fields_order.unshift(this.default_focus_field.name);
- }
- for (var i = 0; i < fields_order.length; i += 1) {
- var field = this.fields[fields_order[i]];
- if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
- if (field.focus() !== false) {
- break;
- }
- }
- }
- }
- },
- on_button_save: function(e) {
- var self = this;
- $(e.target).attr("disabled", true);
- return this.save().done(function(result) {
- self.trigger("save", result);
- self.reload().then(function() {
- self.to_view_mode();
- var menu = instance.webclient.menu;
- if (menu) {
- menu.do_reload_needaction();
- }
- instance.web.bus.trigger('form_view_saved', self);
- });
- }).always(function(){
- $(e.target).attr("disabled", false);
- });
- },
- on_button_cancel: function(event) {
- var self = this;
- if (this.can_be_discarded()) {
- if (this.get('actual_mode') === 'create') {
- this.trigger('history_back');
- } else {
- this.to_view_mode();
- $.when.apply(null, this.render_value_defs).then(function(){
- self.trigger('load_record', self.datarecord);
- });
- }
- }
- this.trigger('on_button_cancel');
- return false;
- },
- on_button_new: function() {
- var self = this;
- this.to_edit_mode();
- return $.when(this.has_been_loaded).then(function() {
- if (self.can_be_discarded()) {
- return self.load_defaults();
- }
- });
- },
- on_button_edit: function() {
- return this.to_edit_mode();
- },
- on_button_create: function() {
- this.dataset.index = null;
- this.do_show();
- },
- on_button_duplicate: function() {
- var self = this;
- return this.has_been_loaded.then(function() {
- return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
- self.record_created(new_id);
- self.to_edit_mode();
- });
- });
- },
- on_button_delete: function() {
- var self = this;
- var def = $.Deferred();
- this.has_been_loaded.done(function() {
- if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
- self.dataset.unlink([self.datarecord.id]).done(function() {
- if (self.dataset.size()) {
- self.execute_pager_action('next');
- } else {
- self.do_action('history_back');
- }
- def.resolve();
- });
- } else {
- $.async_when().done(function () {
- def.reject();
- });
- }
- });
- return def.promise();
- },
- can_be_discarded: function() {
- if (this.$el.is('.oe_form_dirty')) {
- if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
- return false;
- }
- this.$el.removeClass('oe_form_dirty');
- }
- return true;
- },
- /**
- * Triggers saving the form's record. Chooses between creating a new
- * record or saving an existing one depending on whether the record
- * already has an id property.
- *
- * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
- * record, should that record be inserted at the start of the dataset (by
- * default, records are added at the end)
- */
- save: function(prepend_on_create) {
- var self = this;
- var save_obj = {prepend_on_create: prepend_on_create, ret: null};
- this.save_list.push(save_obj);
- return self._process_operations().then(function() {
- if (save_obj.error)
- return $.Deferred().reject();
- return $.when.apply($, save_obj.ret);
- }).done(function(result) {
- self.$el.removeClass('oe_form_dirty');
- });
- },
- _process_save: function(save_obj) {
- var self = this;
- var prepend_on_create = save_obj.prepend_on_create;
- try {
- var form_invalid = false,
- values = {},
- first_invalid_field = null,
- readonly_values = {};
- for (var f in self.fields) {
- if (!self.fields.hasOwnProperty(f)) { continue; }
- f = self.fields[f];
- if (!f.is_valid()) {
- form_invalid = true;
- if (!first_invalid_field) {
- first_invalid_field = f;
- }
- } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
- // Special case 'id' field, do not save this field
- // on 'create' : save all non readonly fields
- // on 'edit' : save non readonly modified fields
- if (!f.get("readonly")) {
- values[f.name] = f.get_value(true);
- } else {
- readonly_values[f.name] = f.get_value(true);
- }
- }
- }
- if (form_invalid) {
- self.set({'display_invalid_fields': true});
- first_invalid_field.focus();
- self.on_invalid();
- return $.Deferred().reject();
- } else {
- self.set({'display_invalid_fields': false});
- var save_deferral;
- if (!self.datarecord.id) {
- // Creation save
- save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
- return self.record_created(r, prepend_on_create);
- }, null);
- } else if (_.isEmpty(values)) {
- // Not dirty, noop save
- save_deferral = $.Deferred().resolve({}).promise();
- } else {
- // Write save
- save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
- return self.record_saved(r);
- }, null);
- }
- return save_deferral;
- }
- } catch (e) {
- console.error(e);
- return $.Deferred().reject();
- }
- },
- on_invalid: function() {
- var warnings = _(this.fields).chain()
- .filter(function (f) { return !f.is_valid(); })
- .map(function (f) {
- return _.str.sprintf('<li>%s</li>',
- _.escape(f.string));
- }).value();
- warnings.unshift('<ul>');
- warnings.push('</ul>');
- this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
- },
- /**
- * Reload the form after saving
- *
- * @param {Object} r result of the write function.
- */
- record_saved: function(r) {
- this.trigger('record_saved', r);
- if (!r) {
- // should not happen in the server, but may happen for internal purpose
- return $.Deferred().reject();
- }
- return r;
- },
- /**
- * Updates the form' dataset to contain the new record:
- *
- * * Adds the newly created record to the current dataset (at the end by
- * default)
- * * Selects that record (sets the dataset's index to point to the new
- * record's id).
- * * Updates the pager and sidebar displays
- *
- * @param {Object} r
- * @param {Boolean} [prepend_on_create=false] adds the newly created record
- * at the beginning of the dataset instead of the end
- */
- record_created: function(r, prepend_on_create) {
- var self = this;
- if (!r) {
- // should not happen in the server, but may happen for internal purpose
- this.trigger('record_created', r);
- return $.Deferred().reject();
- } else {
- this.datarecord.id = r;
- if (!prepend_on_create) {
- this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
- this.dataset.index = this.dataset.ids.length - 1;
- } else {
- this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
- this.dataset.index = 0;
- }
- this.do_update_pager();
- if (this.sidebar) {
- this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
- }
- //openerp.log("The record has been created with id #" + this.datarecord.id);
- return $.when(this.reload()).then(function () {
- self.trigger('record_created', r);
- return _.extend(r, {created: true});
- });
- }
- },
- on_action: function (action) {
- console.debug('Executing action', action);
- },
- reload: function() {
- var self = this;
- return this.reload_mutex.exec(function() {
- if (self.dataset.index === null || self.dataset.index === undefined) {
- self.trigger("previous_view");
- return $.Deferred().reject().promise();
- }
- if (self.dataset.index < 0) {
- return $.when(self.on_button_new());
- } else {
- var fields = _.keys(self.fields_view.fields);
- fields.push('display_name');
- return self.dataset.read_index(fields,
- {
- context: {
- 'bin_size': true,
- 'future_display_name': true
- },
- check_access_rule: true
- }).then(function(r) {
- self.trigger('load_record', r);
- }).fail(function (){
- self.do_action('history_back');
- });
- }
- });
- },
- get_widgets: function() {
- return _.filter(this.getChildren(), function(obj) {
- return obj instanceof instance.web.form.FormWidget;
- });
- },
- get_fields_values: function() {
- var values = {};
- var ids = this.get_selected_ids();
- values["id"] = ids.length > 0 ? ids[0] : false;
- _.each(this.fields, function(value_, key) {
- values[key] = value_.get_value();
- });
- return values;
- },
- get_selected_ids: function() {
- var id = this.dataset.ids[this.dataset.index];
- return id ? [id] : [];
- },
- recursive_save: function() {
- var self = this;
- return $.when(this.save()).then(function(res) {
- if (self.dataset.parent_view)
- return self.dataset.parent_view.recursive_save();
- });
- },
- recursive_reload: function() {
- var self = this;
- var pre = $.when();
- if (self.dataset.parent_view)
- pre = self.dataset.parent_view.recursive_reload();
- return pre.then(function() {
- return self.reload();
- });
- },
- is_dirty: function() {
- return _.any(this.fields, function (value_) {
- return value_._dirty_flag;
- });
- },
- is_interactible_record: function() {
- var id = this.datarecord.id;
- if (!id) {
- if (this.options.not_interactible_on_create)
- return false;
- } else if (typeof(id) === "string") {
- if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
- return false;
- }
- return true;
- },
- sidebar_eval_context: function () {
- return $.when(this.build_eval_context());
- },
- open_defaults_dialog: function () {
- var self = this;
- var display = function (field, value) {
- if (!value) { return value; }
- if (field instanceof instance.web.form.FieldSelection) {
- return _(field.get('values')).find(function (option) {
- return option[0] === value;
- })[1];
- } else if (field instanceof instance.web.form.FieldMany2One) {
- return field.get_displayed();
- }
- return value;
- };
- var fields = _.chain(this.fields)
- .map(function (field) {
- var value = field.get_value();
- // ignore fields which are empty, invisible, readonly, o2m
- // or m2m
- if (!value
- || field.get('invisible')
- || field.get("readonly")
- || field.field.type === 'one2many'
- || field.field.type === 'many2many'
- || field.field.type === 'binary'
- || field.password) {
- return false;
- }
-
- return {
- name: field.name,
- string: field.string,
- value: value,
- displayed: display(field, value),
- };
- })
- .compact()
- .sortBy(function (field) { return field.string; })
- .value();
- var conditions = _.chain(self.fields)
- .filter(function (field) { return field.field.change_default; })
- .map(function (field) {
- var value = field.get_value();
- return {
- name: field.name,
- string: field.string,
- value: value,
- displayed: display(field, value),
- };
- })
- .value();
- var d = new instance.web.Dialog(this, {
- title: _t("Set Default"),
- args: {
- fields: fields,
- conditions: conditions
- },
- buttons: [
- {text: _t("Close"), click: function () { d.close(); }},
- {text: _t("Save default"), click: function () {
- var $defaults = d.$el.find('#formview_default_fields');
- var field_to_set = $defaults.val();
- if (!field_to_set) {
- $defaults.parent().addClass('oe_form_invalid');
- return;
- }
- var condition = d.$el.find('#formview_default_conditions').val(),
- all_users = d.$el.find('#formview_default_all').is(':checked');
- new instance.web.DataSet(self, 'ir.values').call(
- 'set_default', [
- self.dataset.model,
- field_to_set,
- self.fields[field_to_set].get_value(),
- all_users,
- true,
- condition || false
- ]).done(function () { d.close(); });
- }}
- ]
- });
- d.template = 'FormView.set_default';
- d.open();
- },
- register_field: function(field, name) {
- this.fields[name] = field;
- this.fields_order.push(name);
- if (JSON.parse(field.node.attrs.default_focus || "0")) {
- this.default_focus_field = field;
- }
-
- field.on('focused', null, this.proxy('widgetFocused'))
- .on('blurred', null, this.proxy('widgetBlurred'));
- if (this.get_field_desc(name).translate) {
- this.translatable_fields.push(field);
- }
- field.on('changed_value', this, function() {
- if (field.is_syntax_valid()) {
- this.trigger('field_changed:' + name);
- }
- if (field._inhibit_on_change_flag) {
- return;
- }
- field._dirty_flag = true;
- if (field.is_syntax_valid()) {
- this.do_onchange(field);
- this.on_form_changed(true);
- this.do_notify_change();
- }
- });
- },
- get_field_desc: function(field_name) {
- return this.fields_view.fields[field_name];
- },
- get_field_value: function(field_name) {
- return this.fields[field_name].get_value();
- },
- compute_domain: function(expression) {
- return instance.web.form.compute_domain(expression, this.fields);
- },
- _build_view_fields_values: function() {
- var a_dataset = this.dataset;
- var fields_values = this.get_fields_values();
- var active_id = a_dataset.ids[a_dataset.index];
- _.extend(fields_values, {
- active_id: active_id || false,
- active_ids: active_id ? [active_id] : [],
- active_model: a_dataset.model,
- parent: {}
- });
- if (a_dataset.parent_view) {
- fields_values.parent = a_dataset.parent_view.get_fields_values();
- }
- return fields_values;
- },
- build_eval_context: function() {
- var a_dataset = this.dataset;
- return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
- },
-});
-
-/**
- * Interface to be implemented by rendering engines for the form view.
- */
-instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
- set_fields_view: function(fields_view) {},
- set_fields_registry: function(fields_registry) {},
- render_to: function($el) {},
-});
-
-/**
- * Default rendering engine for the form view.
- *
- * It is necessary to set the view using set_view() before usage.
- */
-instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
- init: function(view) {
- this.view = view;
- },
- set_fields_view: function(fvg) {
- this.fvg = fvg;
- this.version = parseFloat(this.fvg.arch.attrs.version);
- if (isNaN(this.version)) {
- this.version = 7.0;
- }
- },
- set_tags_registry: function(tags_registry) {
- this.tags_registry = tags_registry;
- },
- set_fields_registry: function(fields_registry) {
- this.fields_registry = fields_registry;
- },
- set_widgets_registry: function(widgets_registry) {
- this.widgets_registry = widgets_registry;
- },
- // Backward compatibility tools, current default version: v7
- process_version: function() {
- if (this.version < 7.0) {
- this.$form.find('form:first').wrapInner('<group col="4"/>');
- this.$form.find('page').each(function() {
- if (!$(this).parents('field').length) {
- $(this).wrapInner('<group col="4"/>');
- }
- });
- }
- },
- get_arch_fragment: function() {
- var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
- // IE won't allow custom button@type and will revert it to spec default : 'submit'
- $('button', doc).each(function() {
- $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
- });
- // IE's html parser is also a css parser. How convenient...
- $('board', doc).each(function() {
- $(this).attr('layout', $(this).attr('style'));
- });
- return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
- },
- render_to: function($target) {
- var self = this;
- this.$target = $target;
-
- this.$form = this.get_arch_fragment();
-
- this.process_version();
-
- this.fields_to_init = [];
- this.tags_to_init = [];
- this.widgets_to_init = [];
- this.labels = {};
- this.process(this.$form);
-
- this.$form.appendTo(this.$target);
-
- this.to_replace = [];
-
- _.each(this.fields_to_init, function($elem) {
- var name = $elem.attr("name");
- if (!self.fvg.fields[name]) {
- throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
- }
- var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
- if (!obj) {
- throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
- }
- var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
- var $label = self.labels[$elem.attr("name")];
- if ($label) {
- w.set_input_id($label.attr("for"));
- }
- self.alter_field(w);
- self.view.register_field(w, $elem.attr("name"));
- self.to_replace.push([w, $elem]);
- });
- _.each(this.tags_to_init, function($elem) {
- var tag_name = $elem[0].tagName.toLowerCase();
- var obj = self.tags_registry.get_object(tag_name);
- var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
- self.to_replace.push([w, $elem]);
- });
- _.each(this.widgets_to_init, function($elem) {
- var widget_type = $elem.attr("type");
- var obj = self.widgets_registry.get_object(widget_type);
- var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
- self.to_replace.push([w, $elem]);
- });
- },
- init_fields: function() {
- var defs = [];
- _.each(this.to_replace, function(el) {
- defs.push(el[0].replace(el[1]));
- if (el[1].children().length) {
- el[0].$el.append(el[1].children());
- }
- });
- this.to_replace = [];
- return $.when.apply($, defs);
- },
- render_element: function(template /* dictionaries */) {
- var dicts = [].slice.call(arguments).slice(1);
- var dict = _.extend.apply(_, dicts);
- dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
- return $(QWeb.render(template, dict));
- },
- alter_field: function(field) {
- },
- toggle_layout_debugging: function() {
- if (!this.$target.has('.oe_layout_debug_cell:first').length) {
- this.$target.find('[title]').removeAttr('title');
- this.$target.find('.oe_form_group_cell').each(function() {
- var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
- $(this).attr('title', text);
- });
- }
- this.$target.toggleClass('oe_layout_debugging');
- },
- process: function($tag) {
- var self = this;
- var tagname = $tag[0].nodeName.toLowerCase();
- if (this.tags_registry.contains(tagname)) {
- this.tags_to_init.push($tag);
- return (tagname === 'button') ? this.process_button($tag) : $tag;
- }
- var fn = self['process_' + tagname];
- if (fn) {
- var args = [].slice.call(arguments);
- args[0] = $tag;
- return fn.apply(self, args);
- } else {
- // generic tag handling, just process children
- $tag.children().each(function() {
- self.process($(this));
- });
- self.handle_common_properties($tag, $tag);
- $tag.removeAttr("modifiers");
- return $tag;
- }
- },
- process_button: function ($button) {
- var self = this;
- $button.children().each(function() {
- self.process($(this));
- });
- return $button;
- },
- process_widget: function($widget) {
- this.widgets_to_init.push($widget);
- return $widget;
- },
- process_sheet: function($sheet) {
- var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
- this.handle_common_properties($new_sheet, $sheet);
- var $dst = $new_sheet.find('.oe_form_sheet');
- $sheet.contents().appendTo($dst);
- $sheet.before($new_sheet).remove();
- this.process($new_sheet);
- },
- process_form: function($form) {
- if ($form.find('> sheet').length === 0) {
- $form.addClass('oe_form_nosheet');
- }
- var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
- this.handle_common_properties($new_form, $form);
- $form.contents().appendTo($new_form);
- if ($form[0] === this.$form[0]) {
- // If root element, replace it
- this.$form = $new_form;
- } else {
- $form.before($new_form).remove();
- }
- this.process($new_form);
- },
- /*
- * Used by direct <field> children of a <group> tag only
- * This method will add the implicit <label...> for every field
- * in the <group>
- */
- preprocess_field: function($field) {
- var self = this;
- var name = $field.attr('name'),
- field_colspan = parseInt($field.attr('colspan'), 10),
- field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
-
- if ($field.attr('nolabel') === '1')
- return;
- $field.attr('nolabel', '1');
- var found = false;
- this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
- $(el).parents().each(function(unused, tag) {
- var name = tag.tagName.toLowerCase();
- if (name === "field" || name in self.tags_registry.map)
- found = true;
- });
- });
- if (found)
- return;
-
- var $label = $('<label/>').attr({
- 'for' : name,
- "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
- "string": $field.attr('string'),
- "help": $field.attr('help'),
- "class": $field.attr('class'),
- });
- $label.insertBefore($field);
- if (field_colspan > 1) {
- $field.attr('colspan', field_colspan - 1);
- }
- return $label;
- },
- process_field: function($field) {
- if ($field.parent().is('group')) {
- // No implicit labels for normal fields, only for <group> direct children
- var $label = this.preprocess_field($field);
- if ($label) {
- this.process($label);
- }
- }
- this.fields_to_init.push($field);
- return $field;
- },
- process_group: function($group) {
- var self = this;
- $group.children('field').each(function() {
- self.preprocess_field($(this));
- });
- var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
- var $table;
- if ($new_group.first().is('table.oe_form_group')) {
- $table = $new_group;
- } else if ($new_group.filter('table.oe_form_group').length) {
- $table = $new_group.filter('table.oe_form_group').first();
- } else {
- $table = $new_group.find('table.oe_form_group').first();
- }
-
- var $tr, $td,
- cols = parseInt($group.attr('col') || 2, 10),
- row_cols = cols;
-
- var children = [];
- $group.children().each(function(a,b,c) {
- var $child = $(this);
- var colspan = parseInt($child.attr('colspan') || 1, 10);
- var tagName = $child[0].tagName.toLowerCase();
- var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
- var newline = tagName === 'newline';
-
- // Note FME: those classes are used in layout debug mode
- if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
- $tr.addClass('oe_form_group_row_incomplete');
- if (newline) {
- $tr.addClass('oe_form_group_row_newline');
- }
- }
- if (newline) {
- $tr = null;
- return;
- }
- if (!$tr || row_cols < colspan) {
- $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
- row_cols = cols;
- } else if (tagName==='group') {
- // When <group> <group/><group/> </group>, we need a spacing between the two groups
- $td.addClass('oe_group_right');
- }
- row_cols -= colspan;
-
- // invisibility transfer
- var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
- var invisible = field_modifiers.invisible;
- self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
-
- $tr.append($td.append($child));
- children.push($child[0]);
- });
- if (row_cols && $td) {
- $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
- }
- $group.before($new_group).remove();
-
- $table.find('> tbody > tr').each(function() {
- var to_compute = [],
- row_cols = cols,
- total = 100;
- $(this).children().each(function() {
- var $td = $(this),
- $child = $td.children(':first');
- if ($child.attr('cell-class')) {
- $td.addClass($child.attr('cell-class'));
- }
- switch ($child[0].tagName.toLowerCase()) {
- case 'separator':
- break;
- case 'label':
- if ($child.attr('for')) {
- $td.attr('width', '1%').addClass('oe_form_group_cell_label');
- row_cols-= $td.attr('colspan') || 1;
- total--;
- }
- break;
- default:
- var width = _.str.trim($child.attr('width') || ''),
- iwidth = parseInt(width, 10);
- if (iwidth) {
- if (width.substr(-1) === '%') {
- total -= iwidth;
- width = iwidth + '%';
- } else {
- // Absolute width
- $td.css('min-width', width + 'px');
- }
- $td.attr('width', width);
- $child.removeAttr('width');
- row_cols-= $td.attr('colspan') || 1;
- } else {
- to_compute.push($td);
- }
-
- }
- });
- if (row_cols) {
- var unit = Math.floor(total / row_cols);
- if (!$(this).is('.oe_form_group_row_incomplete')) {
- _.each(to_compute, function($td, i) {
- var width = parseInt($td.attr('colspan'), 10) * unit;
- $td.attr('width', width + '%');
- total -= width;
- });
- }
- }
- });
- _.each(children, function(el) {
- self.process($(el));
- });
- this.handle_common_properties($new_group, $group);
- return $new_group;
- },
- process_notebook: function($notebook) {
- var self = this;
-
- // Extract useful info from the notebook xml declaration
- var pages = [];
- $notebook.find('> page').each(function() {
- var $page = $(this);
- var page_attrs = $page.getAttributes();
- page_attrs.id = _.uniqueId('notebook_page_');
- page_attrs.contents = $page.html();
- page_attrs.ref = $page; // reference to the current page node in the xml declaration
- pages.push(page_attrs);
- });
-
- // Render the notebook and replace $notebook with it
- var $new_notebook = self.render_element('FormRenderingNotebook', {'pages': pages});
- $notebook.before($new_notebook).remove();
-
- // Bind the invisibility changers and find the page to display
- var pageid_to_display;
- _.each(pages, function(page) {
- var $tab = $new_notebook.find('a[href=#' + page.id + ']').parent();
- var $content = $new_notebook.find('#' + page.id);
-
- // Case: <page autofocus="autofocus">;
- // We attach the autofocus attribute to the node because it can be useful during the
- // page execution
- self.attach_node_attr($tab, page.ref, 'autofocus');
- self.attach_node_attr($content, page.ref, 'autofocus');
- if (!pageid_to_display && page.autofocus) {
- // If multiple autofocus, keep the first one
- pageid_to_display = page.id;
- }
-
- // Case: <page attrs="{'invisible': domain}">;
- self.handle_common_properties($tab, page.ref, instance.web.form.NotebookInvisibilityChanger);
- self.handle_common_properties($content, page.ref, instance.web.form.NotebookInvisibilityChanger);
- });
- if (!pageid_to_display) {
- pageid_to_display = $new_notebook.find('div[role="tabpanel"]:not(.oe_form_invisible):first').attr('id');
- }
-
- // Display page. Note: we can't use bootstrap's show function because it is looking for
- // document attached DOM, and the form view is only attached when everything is processed
- $new_notebook.find('a[href=#' + pageid_to_display + ']').parent().addClass('active');
- $new_notebook.find('#' + pageid_to_display).addClass('active');
-
- self.process($new_notebook.children());
- self.handle_common_properties($new_notebook, $notebook);
-
- return $new_notebook;
- },
- process_separator: function($separator) {
- var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
- $separator.before($new_separator).remove();
- this.handle_common_properties($new_separator, $separator);
- return $new_separator;
- },
- process_label: function($label) {
- var name = $label.attr("for"),
- field_orm = this.fvg.fields[name];
- var dict = {
- string: $label.attr('string') || (field_orm || {}).string || '',
- help: $label.attr('help') || (field_orm || {}).help || '',
- _for: name ? _.uniqueId('oe-field-input-') : undefined,
- };
- var align = parseFloat(dict.align);
- if (isNaN(align) || align === 1) {
- align = 'right';
- } else if (align === 0) {
- align = 'left';
- } else {
- align = 'center';
- }
- dict.align = align;
- var $new_label = this.render_element('FormRenderingLabel', dict);
- $label.before($new_label).remove();
- this.handle_common_properties($new_label, $label);
- if (name) {
- this.labels[name] = $new_label;
- }
- return $new_label;
- },
- handle_common_properties: function($new_element, $node, InvisibilityChanger) {
- var str_modifiers = $node.attr("modifiers") || "{}";
- var modifiers = JSON.parse(str_modifiers);
- var ic = null;
- if (modifiers.invisible !== undefined) {
- var InvisibilityChangerCls = InvisibilityChanger || instance.web.form.InvisibilityChanger;
- ic = new InvisibilityChangerCls(this.view, this.view, modifiers.invisible, $new_element);
- }
- $new_element.addClass($node.attr("class") || "");
- $new_element.attr('style', $node.attr('style'));
- return {invisibility_changer: ic,};
- },
- /**
- * Attach a node attribute to an html element to be able to use it after
- * the processing of the page.
- */
- attach_node_attr: function($new_element, $node, attr) {
- $new_element.data(attr, $node.attr(attr));
- }
-});
-
-/**
- Welcome.
-
- If you read this documentation, it probably means that you were asked to use a form view widget outside of
- a form view. Before going further, you must understand that those fields were never really created for
- that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
- you to hack the system with more style.
-*/
-instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
- init: function(parent, eval_context) {
- this._super(parent);
- this.field_descs = {};
- this.eval_context = eval_context || {};
- this.set({
- display_invalid_fields: false,
- actual_mode: 'create',
- });
- },
- get_field_desc: function(field_name) {
- if (this.field_descs[field_name] === undefined) {
- this.field_descs[field_name] = {
- string: field_name,
- };
- }
- return this.field_descs[field_name];
- },
- extend_field_desc: function(fields) {
- var self = this;
- _.each(fields, function(v, k) {
- _.extend(self.get_field_desc(k), v);
- });
- },
- get_field_value: function(field_name) {
- return false;
- },
- set_values: function(values) {
- // nothing
- },
- compute_domain: function(expression) {
- return instance.web.form.compute_domain(expression, {});
- },
- build_eval_context: function() {
- return new instance.web.CompoundContext(this.eval_context);
- },
-});
-
-instance.web.form.compute_domain = function(expr, fields) {
- if (! (expr instanceof Array))
- return !! expr;
- var stack = [];
- for (var i = expr.length - 1; i >= 0; i--) {
- var ex = expr[i];
- if (ex.length == 1) {
- var top = stack.pop();
- switch (ex) {
- case '|':
- stack.push(stack.pop() || top);
- continue;
- case '&':
- stack.push(stack.pop() && top);
- continue;
- case '!':
- stack.push(!top);
- continue;
- default:
- throw new Error(_.str.sprintf(
- _t("Unknown operator %s in domain %s"),
- ex, JSON.stringify(expr)));
- }
- }
-
- var field = fields[ex[0]];
- if (!field) {
- throw new Error(_.str.sprintf(
- _t("Unknown field %s in domain %s"),
- ex[0], JSON.stringify(expr)));
- }
- var field_value = field.get_value ? field.get_value() : field.value;
- var op = ex[1];
- var val = ex[2];
-
- switch (op.toLowerCase()) {
- case '=':
- case '==':
- stack.push(_.isEqual(field_value, val));
- break;
- case '!=':
- case '<>':
- stack.push(!_.isEqual(field_value, val));
- break;
- case '<':
- stack.push(field_value < val);
- break;
- case '>':
- stack.push(field_value > val);
- break;
- case '<=':
- stack.push(field_value <= val);
- break;
- case '>=':
- stack.push(field_value >= val);
- break;
- case 'in':
- if (!_.isArray(val)) val = [val];
- stack.push(_(val).contains(field_value));
- break;
- case 'not in':
- if (!_.isArray(val)) val = [val];
- stack.push(!_(val).contains(field_value));
- break;
- default:
- console.warn(
- _t("Unsupported operator %s in domain %s"),
- op, JSON.stringify(expr));
- }
- }
- return _.all(stack, _.identity);
-};
-
-instance.web.form.is_bin_size = function(v) {
- return (/^\d+(\.\d*)? \w+$/).test(v);
-};
-
-/**
- * Must be applied over an class already possessing the PropertiesMixin.
- *
- * Apply the result of the "invisible" domain to this.$el.
- */
-instance.web.form.InvisibilityChangerMixin = {
- init: function(field_manager, invisible_domain) {
- var self = this;
- this._ic_field_manager = field_manager;
- this._ic_invisible_modifier = invisible_domain;
- this._ic_field_manager.on("view_content_has_changed", this, function() {
- var result = self._ic_invisible_modifier === undefined ? false :
- self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
- self.set({"invisible": result});
- });
- this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
- var check = function() {
- if (self.get("invisible") || self.get('force_invisible')) {
- self.set({"effective_invisible": true});
- } else {
- self.set({"effective_invisible": false});
- }
- };
- this.on('change:invisible', this, check);
- this.on('change:force_invisible', this, check);
- check.call(this);
- },
- start: function() {
- this.on("change:effective_invisible", this, this._check_visibility);
- this._check_visibility();
- },
- _check_visibility: function() {
- this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
- },
-};
-
-instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
- init: function(parent, field_manager, invisible_domain, $el) {
- this.setParent(parent);
- instance.web.PropertiesMixin.init.call(this);
- instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
- this.$el = $el;
- this.start();
- },
-});
-
-// Specialization of InvisibilityChanger for the `notebook` form element to handle more
-// elegantly its special cases (i.e. activate the closest visible sibling)
-instance.web.form.NotebookInvisibilityChanger = instance.web.form.InvisibilityChanger.extend({
- // Override start so that it does not call _check_visibility since it will be
- // called again when view content will be loaded (event view_content_has_changed)
- start: function() {
- this.on("change:effective_invisible", this, this._check_visibility);
- },
- _check_visibility: function() {
- this._super();
- if (this.get("effective_invisible") === true) {
- // Switch to invisible
- // Remove this element as active and set a visible sibling active (if there is one)
- if (this.$el.hasClass('active')) {
- this.$el.removeClass('active');
- var visible_siblings = this.$el.siblings(':not(.oe_form_invisible)');
- if (visible_siblings.length) {
- $(visible_siblings[0]).addClass('active');
- }
- }
- } else {
- // Switch to visible
- // If there is no visible active sibling, set this element as active,
- // otherwise if that sibling hasn't autofocus and if we are in edit mode,
- // remove that sibling as active and set this element as active
- var visible_active_sibling = this.$el.siblings(':not(.oe_form_invisible).active');
- if (!(visible_active_sibling.length)) {
- this.$el.addClass('active');
- } else if (!$(visible_active_sibling[0]).data('autofocus') && this._ic_field_manager.get('actual_mode') === "edit") {
- this.$el.addClass('active');
- $(visible_active_sibling[0]).removeClass('active');
- }
- }
- },
-});
-
-
-/**
- Base class for all fields, custom widgets and buttons to be displayed in the form view.
-
- Properties:
- - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
- the values of the "readonly" property and the "mode" property on the field manager.
-*/
-instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
- /**
- * @constructs instance.web.form.FormWidget
- * @extends instance.web.Widget
- *
- * @param field_manager
- * @param node
- */
- init: function(field_manager, node) {
- this._super(field_manager);
- this.field_manager = field_manager;
- if (this.field_manager instanceof instance.web.FormView)
- this.view = this.field_manager;
- this.node = node;
- this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
- instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
-
- this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
-
- this.set({
- required: false,
- readonly: false,
- });
- // some events to make the property "effective_readonly" sync automatically with "readonly" and
- // "mode" on field_manager
- var self = this;
- var test_effective_readonly = function() {
- self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
- };
- this.on("change:readonly", this, test_effective_readonly);
- this.field_manager.on("change:actual_mode", this, test_effective_readonly);
- test_effective_readonly.call(this);
- },
- renderElement: function() {
- this.process_modifiers();
- this._super();
- this.$el.addClass(this.node.attrs["class"] || "");
- },
- destroy: function() {
- $.fn.tooltip('destroy');
- this._super.apply(this, arguments);
- },
- /**
- * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
- *
- * This method is an utility method that is meant to be called by child classes.
- *
- * @param {jQuery} $e jQuery object of elements to bind focus/blur on
- */
- setupFocus: function ($e) {
- var self = this;
- $e.on({
- focus: function () { self.trigger('focused'); },
- blur: function () { self.trigger('blurred'); }
- });
- },
- process_modifiers: function() {
- var to_set = {};
- for (var a in this.modifiers) {
- if (!this.modifiers.hasOwnProperty(a)) { continue; }
- if (!_.include(["invisible"], a)) {
- var val = this.field_manager.compute_domain(this.modifiers[a]);
- to_set[a] = val;
- }
- }
- this.set(to_set);
- },
- do_attach_tooltip: function(widget, trigger, options) {
- widget = widget || this;
- trigger = trigger || this.$el;
- options = _.extend({
- delay: { show: 500, hide: 0 },
- title: function() {
- var template = widget.template + '.tooltip';
- if (!QWeb.has_template(template)) {
- template = 'WidgetLabel.tooltip';
- }
- return QWeb.render(template, {
- debug: instance.session.debug,
- widget: widget
- });
- },
- }, options || {});
- //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
- //as empty
- if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
- $(trigger).tooltip(options);
- }
- },
- /**
- * Builds a new context usable for operations related to fields by merging
- * the fields'context with the action's context.
- */
- build_context: function() {
- // only use the model's context if there is not context on the node
- var v_context = this.node.attrs.context;
- if (! v_context) {
- v_context = (this.field || {}).context || {};
- }
-
- if (v_context.__ref || true) { //TODO: remove true
- var fields_values = this.field_manager.build_eval_context();
- v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
- }
- return v_context;
- },
- build_domain: function() {
- var f_domain = this.field.domain || [];
- var n_domain = this.node.attrs.domain || null;
- // if there is a domain on the node, overrides the model's domain
- var final_domain = n_domain !== null ? n_domain : f_domain;
- if (!(final_domain instanceof Array) || true) { //TODO: remove true
- var fields_values = this.field_manager.build_eval_context();
- final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
- }
- return final_domain;
- }
-});
-
-instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
- template: 'WidgetButton',
- init: function(field_manager, node) {
- node.attrs.type = node.attrs['data-button-type'];
- this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
- this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
- this._super(field_manager, node);
- this.force_disabled = false;
- this.string = (this.node.attrs.string || '').replace(/_/g, '');
- if (JSON.parse(this.node.attrs.default_focus || "0")) {
- // TODO fme: provide enter key binding to widgets
- this.view.default_focus_button = this;
- }
- if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
- this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
- }
- },
- start: function() {
- this._super.apply(this, arguments);
- this.view.on('view_content_has_changed', this, this.check_disable);
- this.check_disable();
- this.$el.click(this.on_click);
- if (this.node.attrs.help || instance.session.debug) {
- this.do_attach_tooltip();
- }
- this.setupFocus(this.$el);
- },
- on_click: function() {
- var self = this;
- this.force_disabled = true;
- this.check_disable();
- this.execute_action().always(function() {
- self.force_disabled = false;
- self.check_disable();
- });
- },
- execute_action: function() {
- var self = this;
- var exec_action = function() {
- if (self.node.attrs.confirm) {
- var def = $.Deferred();
- var dialog = new instance.web.Dialog(this, {
- title: _t('Confirm'),
- buttons: [
- {text: _t("Cancel"), click: function() {
- this.parents('.modal').modal('hide');
- }
- },
- {text: _t("Ok"), click: function() {
- var self2 = this;
- self.on_confirmed().always(function() {
- self2.parents('.modal').modal('hide');
- });
- }
- }
- ],
- }, $('<div/>').text(self.node.attrs.confirm)).open();
- dialog.on("closing", null, function() {def.resolve();});
- return def.promise();
- } else {
- return self.on_confirmed();
- }
- };
- if (!this.node.attrs.special) {
- return this.view.recursive_save().then(exec_action);
- } else {
- return exec_action();
- }
- },
- on_confirmed: function() {
- var self = this;
-
- var context = this.build_context();
- return this.view.do_execute_action(
- _.extend({}, this.node.attrs, {context: context}),
- this.view.dataset, this.view.datarecord.id, function (reason) {
- if (!_.isObject(reason)) {
- self.view.recursive_reload();
- }
- });
- },
- check_disable: function() {
- var disabled = (this.force_disabled || !this.view.is_interactible_record());
- this.$el.prop('disabled', disabled);
- this.$el.css('color', disabled ? 'grey' : '');
- }
-});
-
-/**
- * Interface to be implemented by fields.
- *
- * Events:
- * - changed_value: triggered when the value of the field has changed. This can be due
- * to a user interaction or a call to set_value().
- *
- */
-instance.web.form.FieldInterface = {
- /**
- * Constructor takes 2 arguments:
- * - field_manager: Implements FieldManagerMixin
- * - node: the "<field>" node in json form
- */
- init: function(field_manager, node) {},
- /**
- * Called by the form view to indicate the value of the field.
- *
- * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
- * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
- * before the widget is inserted into the DOM.
- *
- * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
- * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
- * also be able to handle any other format commonly used in the _defaults key on the models in the addons
- * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
- * no information is ever given to know which format is used.
- */
- set_value: function(value_) {},
- /**
- * Get the current value of the widget.
- *
- * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
- * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
- * For example if the field is marked as "required", a call to get_value() can return false.
- *
- * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
- * return a default value according to the type of field.
- *
- * This method is always assumed to perform synchronously, it can not return a promise.
- *
- * If there was no user interaction to modify the value of the field, it is always assumed that
- * get_value() return the same semantic value than the one passed in the last call to set_value(),
- * although the syntax can be different. This can be the case for type of fields that have a different
- * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
- */
- get_value: function() {},
- /**
- * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
- * view.
- */
- set_input_id: function(id) {},
- /**
- * Returns true if is_syntax_valid() returns true and the value is semantically
- * valid too according to the semantic restrictions applied to the field.
- */
- is_valid: function() {},
- /**
- * Returns true if the field holds a value which is syntactically correct, ignoring
- * the potential semantic restrictions applied to the field.
- */
- is_syntax_valid: function() {},
- /**
- * Must set the focus on the field. Return false if field is not focusable.
- */
- focus: function() {},
- /**
- * Called when the translate button is clicked.
- */
- on_translate: function() {},
- /**
- This method is called by the form view before reading on_change values and before saving. It tells
- the field to save its value before reading it using get_value(). Must return a promise.
- */
- commit_value: function() {},
-};
-
-/**
- * Abstract class for classes implementing FieldInterface.
- *
- * Properties:
- * - value: useful property to hold the value of the field. By default, set_value() and get_value()
- * set and retrieve the value property. Changing the value property also triggers automatically
- * a 'changed_value' event that inform the view to trigger on_changes.
- *
- */
-instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
- /**
- * @constructs instance.web.form.AbstractField
- * @extends instance.web.form.FormWidget
- *
- * @param field_manager
- * @param node
- */
- init: function(field_manager, node) {
- var self = this;
- this._super(field_manager, node);
- this.name = this.node.attrs.name;
- this.field = this.field_manager.get_field_desc(this.name);
- this.widget = this.node.attrs.widget;
- this.string = this.node.attrs.string || this.field.string || this.name;
- this.options = instance.web.py_eval(this.node.attrs.options || '{}');
- this.set({'value': false});
-
- this.on("change:value", this, function() {
- this.trigger('changed_value');
- this._check_css_flags();
- });
- },
- renderElement: function() {
- var self = this;
- this._super();
- if (this.field.translate && this.view) {
- this.$el.addClass('oe_form_field_translatable');
- this.$el.find('.oe_field_translate').click(this.on_translate);
- }
- this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
- this.do_attach_tooltip(this, this.$label[0] || this.$el);
- if (instance.session.debug) {
- this.$label.off('dblclick').on('dblclick', function() {
- console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
- window.w = self;
- console.log("window.w =", window.w);
- });
- }
- if (!this.disable_utility_classes) {
- this.off("change:required", this, this._set_required);
- this.on("change:required", this, this._set_required);
- this._set_required();
- }
- this._check_visibility();
- this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
- this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
- this._check_css_flags();
- },
- start: function() {
- var tmp = this._super();
- this.on("change:value", this, function() {
- if (! this.no_rerender)
- this.render_value();
- });
- this.render_value();
- },
- /**
- * Private. Do not use.
- */
- _set_required: function() {
- this.$el.toggleClass('oe_form_required', this.get("required"));
- },
- set_value: function(value_) {
- this.set({'value': value_});
- },
- get_value: function() {
- return this.get('value');
- },
- /**
- Utility method that all implementations should use to change the
- value without triggering a re-rendering.
- */
- internal_set_value: function(value_) {
- var tmp = this.no_rerender;
- this.no_rerender = true;
- this.set({'value': value_});
- this.no_rerender = tmp;
- },
- /**
- This method is called each time the value is modified.
- */
- render_value: function() {},
- is_valid: function() {
- return this.is_syntax_valid() && !(this.get('required') && this.is_false());
- },
- is_syntax_valid: function() {
- return true;
- },
- /**
- * Method useful to implement to ease validity testing. Must return true if the current
- * value is similar to false in OpenERP.
- */
- is_false: function() {
- return this.get('value') === false;
- },
- _check_css_flags: function() {
- if (this.field.translate) {
- this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
- }
- if (!this.disable_utility_classes) {
- if (this.field_manager.get('display_invalid_fields')) {
- this.$el.toggleClass('oe_form_invalid', !this.is_valid());
- }
- }
- },
- focus: function() {
- return false;
- },
- set_input_id: function(id) {
- this.id_for_label = id;
- },
- on_translate: function() {
- var self = this;
- var trans = new instance.web.DataSet(this, 'ir.translation');
- return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
- self.do_action(r);
- });
- },
-
- set_dimensions: function (height, width) {
- this.$el.css({
- width: width,
- minHeight: height
- });
- },
- commit_value: function() {
- return $.when();
- },
-});
-
-/**
- * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
- * switch.
- */
-instance.web.form.ReinitializeWidgetMixin = {
- /**
- * Default implementation of, you should not override it, use initialize_field() instead.
- */
- start: function() {
- this.initialize_field();
- this._super();
- },
- initialize_field: function() {
- this.on("change:effective_readonly", this, this.reinitialize);
- this.initialize_content();
- },
- reinitialize: function() {
- this.destroy_content();
- this.renderElement();
- this.initialize_content();
- },
- /**
- * Called to destroy anything that could have been created previously, called before a
- * re-initialization.
- */
- destroy_content: function() {},
- /**
- * Called to initialize the content.
- */
- initialize_content: function() {},
-};
-
-/**
- * A mixin to apply on any field that has to completely re-render when its readonly state
- * switch.
- */
-instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
- reinitialize: function() {
- instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
- var res = this.render_value();
- if (this.view && this.view.render_value_defs){
- this.view.render_value_defs.push(res);
- }
- },
-});
-
-/**
- Some hack to make placeholders work in ie9.
-*/
-if (!('placeholder' in document.createElement('input'))) {
- document.addEventListener("DOMNodeInserted",function(event){
- var nodename = event.target.nodeName.toLowerCase();
- if ( nodename === "input" || nodename == "textarea" ) {
- $(event.target).placeholder();
- }
- });
-}
-
-instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: 'FieldChar',
- widget_class: 'oe_form_field_char',
- events: {
- 'change input': 'store_dom_value',
- },
- init: function (field_manager, node) {
- this._super(field_manager, node);
- this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
- },
- initialize_content: function() {
- this.setupFocus(this.$('input'));
- },
- store_dom_value: function () {
- if (!this.get('effective_readonly')
- && this.$('input').length
- && this.is_syntax_valid()) {
- this.internal_set_value(
- this.parse_value(
- this.$('input').val()));
- }
- },
- commit_value: function () {
- this.store_dom_value();
- return this._super();
- },
- render_value: function() {
- var show_value = this.format_value(this.get('value'), '');
- if (!this.get("effective_readonly")) {
- this.$el.find('input').val(show_value);
- } else {
- if (this.password) {
- show_value = new Array(show_value.length + 1).join('*');
- }
- this.$(".oe_form_char_content").text(show_value);
- }
- },
- is_syntax_valid: function() {
- if (!this.get("effective_readonly") && this.$("input").size() > 0) {
- try {
- this.parse_value(this.$('input').val(), '');
- return true;
- } catch(e) {
- return false;
- }
- }
- return true;
- },
- parse_value: function(val, def) {
- return instance.web.parse_value(val, this, def);
- },
- format_value: function(val, def) {
- return instance.web.format_value(val, this, def);
- },
- is_false: function() {
- return this.get('value') === '' || this._super();
- },
- focus: function() {
- var input = this.$('input:first')[0];
- return input ? input.focus() : false;
- },
- set_dimensions: function (height, width) {
- this._super(height, width);
- this.$('input').css({
- height: height,
- width: width
- });
- }
-});
-
-instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({
- init: function (field_manager, node) {
- this._super(field_manager, node);
- },
- start: function () {
- var self = this;
- this.states = [];
- this._super.apply(this, arguments);
- // hook on form view content changed: recompute the states, because it may be related to the current stage
- this.getParent().on('view_content_has_changed', self, function () {
- self.render_value();
- });
- },
- prepare_dropdown_selection: function() {
- var self = this;
- var data = [];
- var selection = this.field.selection || [];
- var stage_id = _.isArray(this.view.datarecord.stage_id) ? this.view.datarecord.stage_id[0] : this.view.datarecord.stage_id;
- var legend_field = this.options && this.options.states_legend_field || false;
- var fields_to_read = _.map(
- this.options && this.options.states_legend || {},
- function (value, key, list) { return value; });
- if (legend_field && fields_to_read && stage_id) {
- var fetch_stage = new openerp.web.DataSet(
- this,
- self.view.fields[legend_field].field.relation).read_ids([stage_id],
- fields_to_read);
- }
- else { var fetch_stage = $.Deferred().resolve(false); }
- return $.when(fetch_stage).then(function (stage_data) {
- _.map(selection, function(res) {
- var value = {
- 'name': res[0],
- 'tooltip': res[1],
- 'state_name': res[1],
- }
- if (stage_data && stage_data[0][self.options.states_legend[res[0]]]) {
- value['state_name'] = stage_data[0][self.options.states_legend[res[0]]];
- value['tooltip'] = stage_data[0][self.options.states_legend[res[0]]];
- }
- if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
- else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
- else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
- data.push(value);
- });
- return data;
- });
- },
- render_value: function() {
- var self = this;
- var dd_fetched = this.prepare_dropdown_selection();;
- return $.when(dd_fetched).then(function (states) {
- self.states = states;
- self.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
- self.$el.find('li').on('click', self.set_kanban_selection.bind(self));
- })
- },
- /* setting the value: in view mode, perform an asynchronous call and reload
- the form view; in edit mode, use set_value to save the new value that will
- be written when saving the record. */
- set_kanban_selection: function (ev) {
- var self = this;
- var li = $(ev.target).closest('li');
- if (li.length) {
- var value = String(li.data('value'));
- if (this.view.get('actual_mode') == 'view') {
- var write_values = {}
- write_values[self.name] = value;
- return this.view.dataset._model.call(
- 'write', [
- [this.view.datarecord.id],
- write_values,
- self.view.dataset.get_context()
- ]).done(self.reload_record.bind(self));
- }
- else {
- return this.set_value(value);
- }
- }
- },
- reload_record: function() {
- this.view.reload();
- },
-});
-
-instance.web.form.Priority = instance.web.form.FieldChar.extend({
- init: function (field_manager, node) {
- this._super(field_manager, node);
- },
- prepare_priority: function() {
- var self = this;
- var selection = this.field.selection || [];
- var init_value = selection && selection[0][0] || 0;
- var data = _.map(selection.slice(1), function(element, index) {
- var value = {
- 'value': element[0],
- 'name': element[1],
- 'click_value': element[0],
- }
- if (index == 0 && self.get('value') == element[0]) {
- value['click_value'] = init_value;
- }
- return value;
- });
- return data;
- },
- render_value: function() {
- var self = this;
- this.priorities = this.prepare_priority();
- this.$el.html(QWeb.render("Priority", {'widget': this}));
- if (!this.get('readonly')){
- this.$el.find('li').on('click', this.set_priority.bind(this));
- }
- },
- /* setting the value: in view mode, perform an asynchronous call and reload
- the form view; in edit mode, use set_value to save the new value that will
- be written when saving the record. */
- set_priority: function (ev) {
- var self = this;
- var li = $(ev.target).closest('li');
- if (li.length) {
- var value = String(li.data('value'));
- if (this.view.get('actual_mode') == 'view') {
- var write_values = {}
- write_values[self.name] = value;
- return this.view.dataset._model.call(
- 'write', [
- [this.view.datarecord.id],
- write_values,
- self.view.dataset.get_context()
- ]).done(self.reload_record.bind(self));
- }
- else {
- return this.set_value(value);
- }
- }
-
- },
- reload_record: function() {
- this.view.reload();
- },
-});
-
-instance.web.form.FieldID = instance.web.form.FieldChar.extend({
- process_modifiers: function () {
- this._super();
- this.set({ readonly: true });
- },
-});
-
-instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
- template: 'FieldEmail',
- initialize_content: function() {
- this._super();
- var $button = this.$el.find('button');
- $button.click(this.on_button_clicked);
- this.setupFocus($button);
- },
- render_value: function() {
- if (!this.get("effective_readonly")) {
- this._super();
- } else {
- this.$el.find('a')
- .attr('href', 'mailto:' + this.get('value'))
- .text(this.get('value') || '');
- }
- },
- on_button_clicked: function() {
- if (!this.get('value') || !this.is_syntax_valid()) {
- this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
- } else {
- location.href = 'mailto:' + this.get('value');
- }
- }
-});
-
-instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
- template: 'FieldUrl',
- initialize_content: function() {
- this._super();
- var $button = this.$el.find('button');
- $button.click(this.on_button_clicked);
- this.setupFocus($button);
- },
- render_value: function() {
- if (!this.get("effective_readonly")) {
- this._super();
- } else {
- var tmp = this.get('value');
- var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
- if (!s) {
- tmp = "http://" + this.get('value');
- }
- var text = this.get('value') ? this.node.attrs.text || tmp : '';
- this.$el.find('a').attr('href', tmp).text(text);
- }
- },
- on_button_clicked: function() {
- if (!this.get('value')) {
- this.do_warn(_t("Resource Error"), _t("This resource is empty"));
- } else {
- var url = $.trim(this.get('value'));
- if(/^www\./i.test(url))
- url = 'http://'+url;
- window.open(url);
- }
- }
-});
-
-instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
- is_field_number: true,
- widget_class: 'oe_form_field_float',
- init: function (field_manager, node) {
- this._super(field_manager, node);
- this.internal_set_value(0);
- if (this.node.attrs.digits) {
- this.digits = this.node.attrs.digits;
- } else {
- this.digits = this.field.digits;
- }
- },
- set_value: function(value_) {
- if (value_ === false || value_ === undefined) {
- // As in GTK client, floats default to 0
- value_ = 0;
- }
- if (this.digits !== undefined && this.digits.length === 2) {
- value_ = instance.web.round_decimals(value_, this.digits[1]);
- }
- this._super.apply(this, [value_]);
- },
- focus: function () {
- var $input = this.$('input:first');
- return $input.length ? $input.select() : false;
- }
-});
-
-
-instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- render_value: function() {
- var self = this;
- this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
- if (this.get('value')) {
- var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
- var domain = instance.web.pyeval.eval('domain', this.get('value'));
- var ds = new instance.web.DataSetStatic(self, model, self.build_context());
- ds.call('search_count', [domain]).then(function (results) {
- $('.oe_domain_count', self.$el).text(results + ' records selected');
- if (self.get('effective_readonly')) {
- $('button span', self.$el).text(' See selection');
- }
- else {
- $('button span', self.$el).text(' Change selection');
- }
- });
- } else {
- $('.oe_domain_count', this.$el).text('0 record selected');
- $('button span', this.$el).text(' Select records');
- };
- this.$('.select_records').on('click', self.on_click);
- },
- on_click: function(event) {
- event.preventDefault();
- var self = this;
- var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
-
- var options = {
- title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
- readonly: this.get('effective_readonly'),
- disable_multiple_selection: this.get('effective_readonly'),
- no_create: this.get('effective_readonly'),
- };
- var domain = this.get('value');
- popup = new instance.web.form.DomainEditorPopup(this);
- popup.select_element(model, options, domain, {});
- popup.on("elements_selected", self, function(selected_ids) {
- if (!self.get('effective_readonly')) {
- self.set_value(popup.get_domain(selected_ids));
- }
- });
- },
-});
-
-
-instance.web.DateTimeWidget = instance.web.Widget.extend({
- template: "web.datepicker",
- type_of_date: "datetime",
- events: {
- 'dp.change .oe_datepicker_main': 'change_datetime',
- 'dp.show .oe_datepicker_main': 'set_datetime_default',
- 'keypress .oe_datepicker_master': 'change_datetime',
- },
- init: function(parent) {
- this._super(parent);
- this.name = parent.name;
- },
- start: function() {
- var self = this;
- var l10n = _t.database.parameters;
- var options = {
- pickTime: true,
- useSeconds: true,
- startDate: moment({ y: 1900 }),
- endDate: moment().add(200, "y"),
- calendarWeeks: true,
- icons : {
- time: 'fa fa-clock-o',
- date: 'fa fa-calendar',
- up: 'fa fa-chevron-up',
- down: 'fa fa-chevron-down'
- },
- language : moment.locale(),
- format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format),
- };
- this.$input = this.$el.find('input.oe_datepicker_master');
- if (this.type_of_date === 'date') {
- options['pickTime'] = false;
- options['useSeconds'] = false;
- options['format'] = instance.web.normalize_format(l10n.date_format);
- }
- this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
- this.set_readonly(false);
- this.set({'value': false});
- },
- set_value: function(value_) {
- this.set({'value': value_});
- this.$input.val(value_ ? this.format_client(value_) : '');
- },
- get_value: function() {
- return this.get('value');
- },
- set_value_from_ui_: function() {
- var value_ = this.$input.val() || false;
- this.set_value(this.parse_client(value_));
- },
- set_readonly: function(readonly) {
- this.readonly = readonly;
- this.$input.prop('readonly', this.readonly);
- },
- is_valid_: function() {
- var value_ = this.$input.val();
- if (value_ === "") {
- return true;
- } else {
- try {
- this.parse_client(value_);
- return true;
- } catch(e) {
- return false;
- }
- }
- },
- parse_client: function(v) {
- return instance.web.parse_value(v, {"widget": this.type_of_date});
- },
- format_client: function(v) {
- return instance.web.format_value(v, {"widget": this.type_of_date});
- },
- set_datetime_default: function(){
- //when opening datetimepicker the date and time by default should be the one from
- //the input field if any or the current day otherwise
- if (this.type_of_date === 'datetime') {
- value = moment().second(0);
- if (this.$input.val().length !== 0 && this.is_valid_()){
- var value = this.$input.val();
- }
- this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
- }
- },
- change_datetime: function(e) {
- if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
- this.set_value_from_ui_();
- this.trigger("datetime_changed");
- }
- },
- commit_value: function () {
- this.change_datetime();
- },
-});
-
-instance.web.DateWidget = instance.web.DateTimeWidget.extend({
- type_of_date: "date"
-});
-
-instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: "FieldDatetime",
- build_widget: function() {
- return new instance.web.DateTimeWidget(this);
- },
- destroy_content: function() {
- if (this.datewidget) {
- this.datewidget.destroy();
- this.datewidget = undefined;
- }
- },
- initialize_content: function() {
- if (!this.get("effective_readonly")) {
- this.datewidget = this.build_widget();
- this.datewidget.on('datetime_changed', this, _.bind(function() {
- this.internal_set_value(this.datewidget.get_value());
- }, this));
- this.datewidget.appendTo(this.$el);
- this.setupFocus(this.datewidget.$input);
- }
- },
- render_value: function() {
- if (!this.get("effective_readonly")) {
- this.datewidget.set_value(this.get('value'));
- } else {
- this.$el.text(instance.web.format_value(this.get('value'), this, ''));
- }
- },
- is_syntax_valid: function() {
- if (!this.get("effective_readonly") && this.datewidget) {
- return this.datewidget.is_valid_();
- }
- return true;
- },
- is_false: function() {
- return this.get('value') === '' || this._super();
- },
- focus: function() {
- var input = this.datewidget && this.datewidget.$input[0];
- return input ? input.focus() : false;
- },
- set_dimensions: function (height, width) {
- this._super(height, width);
- if (!this.get("effective_readonly")) {
- this.datewidget.$input.css('height', height);
- }
- }
-});
-
-instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
- template: "FieldDate",
- build_widget: function() {
- return new instance.web.DateWidget(this);
- }
-});
-
-instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: 'FieldText',
- events: {
- 'keyup': function (e) {
- if (e.which === $.ui.keyCode.ENTER) {
- e.stopPropagation();
- }
- },
- 'keypress': function (e) {
- if (e.which === $.ui.keyCode.ENTER) {
- e.stopPropagation();
- }
- },
- 'change textarea': 'store_dom_value',
- },
- initialize_content: function() {
- var self = this;
- if (! this.get("effective_readonly")) {
- this.$textarea = this.$el.find('textarea');
- this.auto_sized = false;
- this.default_height = this.$textarea.css('height');
- if (this.default_height === '0px') this.default_height = '90px';
- if (this.get("effective_readonly")) {
- this.$textarea.attr('disabled', 'disabled');
- }
- this.setupFocus(this.$textarea);
- } else {
- this.$textarea = undefined;
- }
- },
- commit_value: function () {
- if (! this.get("effective_readonly") && this.$textarea) {
- this.store_dom_value();
- }
- return this._super();
- },
- store_dom_value: function () {
- this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
- },
- render_value: function() {
- if (! this.get("effective_readonly")) {
- var show_value = instance.web.format_value(this.get('value'), this, '');
- if (show_value === '') {
- this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
- }
- this.$textarea.val(show_value);
- if (! this.auto_sized) {
- this.auto_sized = true;
- this.$textarea.autosize();
- } else {
- this.$textarea.trigger("autosize");
- }
- } else {
- var txt = this.get("value") || '';
- this.$(".oe_form_text_content").text(txt);
- }
- },
- is_syntax_valid: function() {
- if (!this.get("effective_readonly") && this.$textarea) {
- try {
- instance.web.parse_value(this.$textarea.val(), this, '');
- return true;
- } catch(e) {
- return false;
- }
- }
- return true;
- },
- is_false: function() {
- return this.get('value') === '' || this._super();
- },
- focus: function($el) {
- var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
- return input ? input.focus() : false;
- },
- set_dimensions: function (height, width) {
- this._super(height, width);
- if (!this.get("effective_readonly") && this.$textarea) {
- this.$textarea.css({
- width: width,
- minHeight: height
- });
- }
- },
-});
-
-/**
- * FieldTextHtml Widget
- * Intended for FieldText widgets meant to display HTML content. This
- * widget will instantiate the CLEditor (see cleditor in static/src/lib)
- * To find more information about CLEditor configutation: go to
- * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
- */
-instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: 'FieldTextHtml',
- init: function() {
- this._super.apply(this, arguments);
- },
- initialize_content: function() {
- var self = this;
- if (! this.get("effective_readonly")) {
- self._updating_editor = false;
- this.$textarea = this.$el.find('textarea');
- var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
- var height = ((this.node.attrs || {}).editor_height || 250);
- this.$textarea.cleditor({
- width: width, // width not including margins, borders or padding
- height: height, // height not including margins, borders or padding
- controls: // controls to add to the toolbar
- "bold italic underline strikethrough " +
- "| removeformat | bullets numbering | outdent " +
- "indent | link unlink | source",
- bodyStyle: // style to assign to document body contained within the editor
- "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
- });
- this.$cleditor = this.$textarea.cleditor()[0];
- this.$cleditor.change(function() {
- if (! self._updating_editor) {
- self.$cleditor.updateTextArea();
- self.internal_set_value(self.$textarea.val());
- }
- });
- if (this.field.translate) {
- var $img = $('<img class="oe_field_translate oe_input_icon" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>')
- .click(this.on_translate);
- this.$cleditor.$toolbar.append($img);
- }
- }
- },
- render_value: function() {
- if (! this.get("effective_readonly")) {
- this.$textarea.val(this.get('value') || '');
- this._updating_editor = true;
- this.$cleditor.updateFrame();
- this._updating_editor = false;
- } else {
- this.$el.html(this.get('value'));
- }
- },
-});
-
-instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
- template: 'FieldBoolean',
- start: function() {
- var self = this;
- this.$checkbox = $("input", this.$el);
- this.setupFocus(this.$checkbox);
- this.$el.click(_.bind(function() {
- this.internal_set_value(this.$checkbox.is(':checked'));
- }, this));
- var check_readonly = function() {
- self.$checkbox.prop('disabled', self.get("effective_readonly"));
- self.click_disabled_boolean();
- };
- this.on("change:effective_readonly", this, check_readonly);
- check_readonly.call(this);
- this._super.apply(this, arguments);
- },
- render_value: function() {
- this.$checkbox[0].checked = this.get('value');
- },
- focus: function() {
- var input = this.$checkbox && this.$checkbox[0];
- return input ? input.focus() : false;
- },
- click_disabled_boolean: function(){
- var $disabled = this.$el.find('input[type=checkbox]:disabled');
- $disabled.each(function (){
- $(this).next('div').remove();
- $(this).closest("span").append($('<div class="boolean"></div>'));
- });
- }
-});
-
-/**
- The progressbar field expect a float from 0 to 100.
-*/
-instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
- template: 'FieldProgressBar',
- render_value: function() {
- this.$el.progressbar({
- value: this.get('value') || 0,
- disabled: this.get("effective_readonly")
- });
- var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
- this.$('span').html(formatted_value + '%');
- }
-});
-
-/**
- The PercentPie field expect a float from 0 to 100.
-*/
-instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
- template: 'FieldPercentPie',
-
- render_value: function() {
- var value = this.get('value'),
- formatted_value = Math.round(value || 0) + '%',
- svg = this.$('svg')[0];
-
- svg.innerHTML = "";
- nv.addGraph(function() {
- var width = 42, height = 42;
- var chart = nv.models.pieChart()
- .width(width)
- .height(height)
- .margin({top: 0, right: 0, bottom: 0, left: 0})
- .donut(true)
- .showLegend(false)
- .showLabels(false)
- .tooltips(false)
- .color(['#7C7BAD','#DDD'])
- .donutRatio(0.62);
-
- d3.select(svg)
- .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
- .transition()
- .call(chart)
- .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
-
- d3.select(svg)
- .append("text")
- .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
- .style({"font-size": "10px", "font-weight": "bold"})
- .text(formatted_value);
-
- return chart;
- });
-
- }
-});
-
-/**
- The FieldBarChart expectsa list of values (indeed)
-*/
-instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
- template: 'FieldBarChart',
-
- render_value: function() {
- var value = JSON.parse(this.get('value'));
- var svg = this.$('svg')[0];
- svg.innerHTML = "";
- nv.addGraph(function() {
- var width = 34, height = 34;
- var chart = nv.models.discreteBarChart()
- .x(function (d) { return d.tooltip })
- .y(function (d) { return d.value })
- .width(width)
- .height(height)
- .margin({top: 0, right: 0, bottom: 0, left: 0})
- .tooltips(false)
- .showValues(false)
- .transitionDuration(350)
- .showXAxis(false)
- .showYAxis(false);
-
- d3.select(svg)
- .datum([{key: 'values', values: value}])
- .transition()
- .call(chart)
- .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
-
- nv.utils.windowResize(chart.update);
-
- return chart;
- });
-
- }
-});
-
-
-instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: 'FieldSelection',
- events: {
- 'change select': 'store_dom_value',
- },
- init: function(field_manager, node) {
- var self = this;
- this._super(field_manager, node);
- this.set("value", false);
- this.set("values", []);
- this.records_orderer = new instance.web.DropMisordered();
- this.field_manager.on("view_content_has_changed", this, function() {
- var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
- if (! _.isEqual(domain, this.get("domain"))) {
- this.set("domain", domain);
- }
- });
- },
- initialize_field: function() {
- instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
- this.on("change:domain", this, this.query_values);
- this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
- this.on("change:values", this, this.render_value);
- },
- query_values: function() {
- var self = this;
- var def;
- if (this.field.type === "many2one") {
- var model = new openerp.Model(openerp.session, this.field.relation);
- def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
- } else {
- var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
- def = $.when(values);
- }
- this.records_orderer.add(def).then(function(values) {
- if (! _.isEqual(values, self.get("values"))) {
- self.set("values", values);
- }
- });
- },
- initialize_content: function() {
- // Flag indicating whether we're in an event chain containing a change
- // event on the select, in order to know what to do on keyup[RETURN]:
- // * If the user presses [RETURN] as part of changing the value of a
- // selection, we should just let the value change and not let the
- // event broadcast further (e.g. to validating the current state of
- // the form in editable list view, which would lead to saving the
- // current row or switching to the next one)
- // * If the user presses [RETURN] with a select closed (side-effect:
- // also if the user opened the select and pressed [RETURN] without
- // changing the selected value), takes the action as validating the
- // row
- var ischanging = false;
- var $select = this.$el.find('select')
- .change(function () { ischanging = true; })
- .click(function () { ischanging = false; })
- .keyup(function (e) {
- if (e.which !== 13 || !ischanging) { return; }
- e.stopPropagation();
- ischanging = false;
- });
- this.setupFocus($select);
- },
- commit_value: function () {
- this.store_dom_value();
- return this._super();
- },
- store_dom_value: function () {
- if (!this.get('effective_readonly') && this.$('select').length) {
- var val = JSON.parse(this.$('select').val());
- this.internal_set_value(val);
- }
- },
- set_value: function(value_) {
- value_ = value_ === null ? false : value_;
- value_ = value_ instanceof Array ? value_[0] : value_;
- this._super(value_);
- },
- render_value: function() {
- var values = this.get("values");
- values = [[false, this.node.attrs.placeholder || '']].concat(values);
- var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
- if (! found) {
- found = [this.get("value"), _t('Unknown')];
- values = [found].concat(values);
- }
- if (! this.get("effective_readonly")) {
- this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
- this.$("select").val(JSON.stringify(found[0]));
- } else {
- this.$el.text(found[1]);
- }
- },
- focus: function() {
- var input = this.$('select:first')[0];
- return input ? input.focus() : false;
- },
- set_dimensions: function (height, width) {
- this._super(height, width);
- this.$('select').css({
- height: height,
- width: width
- });
- }
-});
-
-instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: 'FieldRadio',
- events: {
- 'click input': 'click_change_value'
- },
- init: function(field_manager, node) {
- /* Radio button widget: Attributes options:
- * - "horizontal" to display in column
- * - "no_radiolabel" don't display text values
- */
- this._super(field_manager, node);
- this.selection = _.clone(this.field.selection) || [];
- this.domain = false;
- this.uniqueId = _.uniqueId("radio");
- },
- initialize_content: function () {
- this.on("change:effective_readonly", this, this.render_value);
- this.field_manager.on("view_content_has_changed", this, this.get_selection);
- this.get_selection();
- },
- click_change_value: function (event) {
- var val = $(event.target).val();
- val = this.field.type == "selection" ? val : +val;
- if (val !== this.get_value()) {
- this.set_value(val);
- }
- },
- /** Get the selection and render it
- * selection: [[identifier, value_to_display], ...]
- * For selection fields: this is directly given by this.field.selection
- * For many2one fields: perform a search on the relation of the many2one field
- */
- get_selection: function() {
- var self = this;
- var selection = [];
- var def = $.Deferred();
- if (self.field.type == "many2one") {
- var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
- if (! _.isEqual(self.domain, domain)) {
- self.domain = domain;
- var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
- ds.call('search', [self.domain])
- .then(function (records) {
- ds.name_get(records).then(function (records) {
- selection = records;
- def.resolve();
- });
- });
- } else {
- selection = self.selection;
- def.resolve();
- }
- }
- else if (self.field.type == "selection") {
- selection = self.field.selection || [];
- def.resolve();
- }
- return def.then(function () {
- if (! _.isEqual(selection, self.selection)) {
- self.selection = _.clone(selection);
- self.renderElement();
- self.render_value();
- }
- });
- },
- set_value: function (value_) {
- if (this.field.type == "selection") {
- value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
- }
- else if (!this.selection.length) {
- this.selection = [value_];
- }
-
- this._super(value_);
- },
- get_value: function () {
- var value = this.get('value');
- return value instanceof Array ? value[0] : value;
- },
- render_value: function () {
- var self = this;
- this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
- this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
- this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
- }
-});
-
-// jquery autocomplete tweak to allow html and classnames
-(function() {
- var proto = $.ui.autocomplete.prototype,
- initSource = proto._initSource;
-
- function filter( array, term ) {
- var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
- return $.grep( array, function(value_) {
- return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
- });
- }
-
- $.extend( proto, {
- _initSource: function() {
- if ( this.options.html && $.isArray(this.options.source) ) {
- this.source = function( request, response ) {
- response( filter( this.options.source, request.term ) );
- };
- } else {
- initSource.call( this );
- }
- },
-
- _renderItem: function( ul, item) {
- return $( "<li></li>" )
- .data( "item.autocomplete", item )
- .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
- .appendTo( ul )
- .addClass(item.classname);
- }
- });
-})();
-
-/**
- A mixin containing some useful methods to handle completion inputs.
-
- The widget containing this option can have these arguments in its widget options:
- - no_quick_create: if true, it will disable the quick create
-*/
-instance.web.form.CompletionFieldMixin = {
- init: function() {
- this.limit = 7;
- this.orderer = new instance.web.DropMisordered();
- this.can_create = this.node.attrs.can_create || true;
- this.can_write = this.node.attrs.can_write || true;
- },
- /**
- * Call this method to search using a string.
- */
- get_search_result: function(search_val) {
- var self = this;
-
- var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
- this.last_query = search_val;
- var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
- if (!_(ids_blacklist).isEmpty()) {
- exclusion_domain.push(['id', 'not in', ids_blacklist]);
- }
-
- return this.orderer.add(dataset.name_search(
- search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
- 'ilike', this.limit + 1, self.build_context())).then(function(data) {
- self.last_search = data;
- // possible selections for the m2o
- var values = _.map(data, function(x) {
- x[1] = x[1].split("\n")[0];
- return {
- label: _.str.escapeHTML(x[1]),
- value: x[1],
- name: x[1],
- id: x[0],
- };
- });
-
- // search more... if more results that max
- if (values.length > self.limit) {
- values = values.slice(0, self.limit);
- values.push({
- label: _t("Search More..."),
- action: function() {
- dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
- self._search_create_popup("search", data);
- });
- },
- classname: 'oe_m2o_dropdown_option'
- });
- }
- // quick create
- var raw_result = _(data.result).map(function(x) {return x[1];});
- if (search_val.length > 0 && !_.include(raw_result, search_val) &&
- ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
- self.can_create && values.push({
- label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
- $('<span />').text(search_val).html()),
- action: function() {
- self._quick_create(search_val);
- },
- classname: 'oe_m2o_dropdown_option'
- });
- }
- // create...
- if (!(self.options && (self.options.no_create || self.options.no_create_edit)) && self.can_create){
- values.push({
- label: _t("Create and Edit..."),
- action: function() {
- self._search_create_popup("form", undefined, self._create_context(search_val));
- },
- classname: 'oe_m2o_dropdown_option'
- });
- }
- else if (values.length == 0)
- values.push({
- label: _t("No results to show..."),
- action: function() {},
- classname: 'oe_m2o_dropdown_option'
- });
-
- return values;
- });
- },
- get_search_blacklist: function() {
- return [];
- },
- _quick_create: function(name) {
- var self = this;
- var slow_create = function () {
- self._search_create_popup("form", undefined, self._create_context(name));
- };
- if (self.options.quick_create === undefined || self.options.quick_create) {
- new instance.web.DataSet(this, this.field.relation, self.build_context())
- .name_create(name).done(function(data) {
- if (!self.get('effective_readonly'))
- self.add_id(data[0]);
- }).fail(function(error, event) {
- event.preventDefault();
- slow_create();
- });
- } else
- slow_create();
- },
- // all search/create popup handling
- _search_create_popup: function(view, ids, context) {
- var self = this;
- var pop = new instance.web.form.SelectCreatePopup(this);
- pop.select_element(
- self.field.relation,
- {
- title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
- initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
- initial_view: view,
- disable_multiple_selection: true
- },
- self.build_domain(),
- new instance.web.CompoundContext(self.build_context(), context || {})
- );
- pop.on("elements_selected", self, function(element_ids) {
- self.add_id(element_ids[0]);
- self.focus();
- });
- },
- /**
- * To implement.
- */
- add_id: function(id) {},
- _create_context: function(name) {
- var tmp = {};
- var field = (this.options || {}).create_name_field;
- if (field === undefined)
- field = "name";
- if (field !== false && name && (this.options || {}).quick_create !== false)
- tmp["default_" + field] = name;
- return tmp;
- },
-};
-
-instance.web.form.M2ODialog = instance.web.Dialog.extend({
- template: "M2ODialog",
- init: function(parent) {
- this.name = parent.string;
- this._super(parent, {
- title: _.str.sprintf(_t("Create a %s"), parent.string),
- size: 'medium',
- });
- },
- start: function() {
- var self = this;
- var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
- this.$("p").text( text );
- this.$buttons.html(QWeb.render("M2ODialog.buttons"));
- this.$("input").val(this.getParent().$input.val());
- this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
- if (self.$("input").val() != ''){
- self.getParent()._quick_create(self.$("input").val());
- self.destroy();
- } else{
- e.preventDefault();
- self.$("input").focus();
- }
- });
- this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
- self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
- self.destroy();
- });
- this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
- self.destroy();
- });
- },
-});
-
-instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
- template: "FieldMany2One",
- events: {
- 'keydown input': function (e) {
- switch (e.which) {
- case $.ui.keyCode.UP:
- case $.ui.keyCode.DOWN:
- e.stopPropagation();
- }
- },
- },
- init: function(field_manager, node) {
- this._super(field_manager, node);
- instance.web.form.CompletionFieldMixin.init.call(this);
- this.set({'value': false});
- this.display_value = {};
- this.display_value_backup = {};
- this.last_search = [];
- this.floating = false;
- this.current_display = null;
- this.is_started = false;
- this.ignore_focusout = false;
- },
- reinit_value: function(val) {
- this.internal_set_value(val);
- this.floating = false;
- if (this.is_started && !this.no_rerender)
- this.render_value();
- },
- initialize_field: function() {
- this.is_started = true;
- instance.web.bus.on('click', this, function() {
- if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
- this.$input.autocomplete("close");
- }
- });
- instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
- },
- initialize_content: function() {
- if (!this.get("effective_readonly"))
- this.render_editable();
- },
- destroy_content: function () {
- if (this.$drop_down) {
- this.$drop_down.off('click');
- delete this.$drop_down;
- }
- if (this.$input) {
- this.$input.closest(".modal .modal-content").off('scroll');
- this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
- 'focus focusout change keydown');
- delete this.$input;
- }
- if (this.$follow_button) {
- this.$follow_button.off('blur focus click');
- delete this.$follow_button;
- }
- },
- destroy: function () {
- this.destroy_content();
- return this._super();
- },
- init_error_displayer: function() {
- // nothing
- },
- hide_error_displayer: function() {
- // doesn't work
- },
- show_error_displayer: function() {
- new instance.web.form.M2ODialog(this).open();
- },
- render_editable: function() {
- var self = this;
- this.$input = this.$el.find("input");
-
- this.init_error_displayer();
-
- self.$input.on('focus', function() {
- self.hide_error_displayer();
- });
-
- this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
- this.$follow_button = $(".oe_m2o_cm_button", this.$el);
-
- this.$follow_button.click(function(ev) {
- ev.preventDefault();
- if (!self.get('value')) {
- self.focus();
- return;
- }
- var pop = new instance.web.form.FormOpenPopup(self);
- var context = self.build_context().eval();
- var model_obj = new instance.web.Model(self.field.relation);
- model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
- pop.show_element(
- self.field.relation,
- self.get("value"),
- self.build_context(),
- {
- title: _t("Open: ") + self.string,
- view_id: view_id,
- readonly: !self.can_write
- }
- );
- pop.on('write_completed', self, function(){
- self.display_value = {};
- self.display_value_backup = {};
- self.render_value();
- self.focus();
- self.trigger('changed_value');
- });
- });
- });
-
- // some behavior for input
- var input_changed = function() {
- if (self.current_display !== self.$input.val()) {
- self.current_display = self.$input.val();
- if (self.$input.val() === "") {
- self.internal_set_value(false);
- self.floating = false;
- } else {
- self.floating = true;
- }
- }
- };
- this.$input.keydown(input_changed);
- this.$input.change(input_changed);
- this.$drop_down.click(function() {
- self.$input.focus();
- if (self.$input.autocomplete("widget").is(":visible")) {
- self.$input.autocomplete("close");
- } else {
- if (self.get("value") && ! self.floating) {
- self.$input.autocomplete("search", "");
- } else {
- self.$input.autocomplete("search");
- }
- }
- });
-
- // Autocomplete close on dialog content scroll
- var close_autocomplete = _.debounce(function() {
- if (self.$input.autocomplete("widget").is(":visible")) {
- self.$input.autocomplete("close");
- }
- }, 50);
- this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
-
- self.ed_def = $.Deferred();
- self.uned_def = $.Deferred();
- var ed_delay = 200;
- var ed_duration = 15000;
- var anyoneLoosesFocus = function (e) {
- if (self.ignore_focusout) { return; }
- var used = false;
- if (self.floating) {
- if (self.last_search.length > 0) {
- if (self.last_search[0][0] != self.get("value")) {
- self.display_value = {};
- self.display_value_backup = {};
- self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
- self.reinit_value(self.last_search[0][0]);
- self.last_search = []
- } else {
- used = true;
- self.render_value();
- }
- } else {
- used = true;
- }
- self.floating = false;
- }
- if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
- self.ed_def.reject();
- self.uned_def.reject();
- self.ed_def = $.Deferred();
- self.ed_def.done(function() {
- self.can_create && self.show_error_displayer();
- ignore_blur = false;
- self.trigger('focused');
- });
- ignore_blur = true;
- setTimeout(function() {
- self.ed_def.resolve();
- self.uned_def.reject();
- self.uned_def = $.Deferred();
- self.uned_def.done(function() {
- self.hide_error_displayer();
- });
- setTimeout(function() {self.uned_def.resolve();}, ed_duration);
- }, ed_delay);
- } else {
- self.no_ed = false;
- self.ed_def.reject();
- }
- };
- var ignore_blur = false;
- this.$input.on({
- focusout: anyoneLoosesFocus,
- focus: function () { self.trigger('focused'); },
- autocompleteopen: function () { ignore_blur = true; },
- autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
- blur: function () {
- // autocomplete open
- if (ignore_blur) { $(this).focus(); return; }
- if (_(self.getChildren()).any(function (child) {
- return child instanceof instance.web.form.AbstractFormPopup;
- })) { return; }
- self.trigger('blurred');
- }
- });
-
- var isSelecting = false;
- // autocomplete
- this.$input.autocomplete({
- source: function(req, resp) {
- self.get_search_result(req.term).done(function(result) {
- resp(result);
- });
- },
- select: function(event, ui) {
- isSelecting = true;
- var item = ui.item;
- if (item.id) {
- self.display_value = {};
- self.display_value_backup = {};
- self.display_value["" + item.id] = item.name;
- self.reinit_value(item.id);
- } else if (item.action) {
- item.action();
- // Cancel widget blurring, to avoid form blur event
- self.trigger('focused');
- return false;
- }
- },
- focus: function(e, ui) {
- e.preventDefault();
- },
- html: true,
- // disabled to solve a bug, but may cause others
- //close: anyoneLoosesFocus,
- minLength: 0,
- delay: 200,
- });
- var appendTo = this.$input.parents('.oe-view-manager-content, .modal-dialog').last();
- if (appendTo.length === 0) {
- appendTo = '.oe_application > *';
- }
- this.$input.autocomplete({
- appendTo: appendTo
- });
- // set position for list of suggestions box
- this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
- this.$input.autocomplete("widget").openerpClass();
- // used to correct a bug when selecting an element by pushing 'enter' in an editable list
- this.$input.keyup(function(e) {
- if (e.which === 13) { // ENTER
- if (isSelecting)
- e.stopPropagation();
- }
- isSelecting = false;
- });
- this.setupFocus(this.$follow_button);
- },
- render_value: function(no_recurse) {
- var self = this;
- if (! this.get("value")) {
- this.display_string("");
- return;
- }
- var display = this.display_value["" + this.get("value")];
- if (display) {
- this.display_string(display);
- return;
- }
- if (! no_recurse) {
- var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
- var def = this.alive(dataset.name_get([self.get("value")])).done(function(data) {
- if (!data[0]) {
- self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
- return;
- }
- self.display_value["" + self.get("value")] = data[0][1];
- self.render_value(true);
- }).fail( function (data, event) {
- // avoid displaying crash errors as many2One should be name_get compliant
- event.preventDefault();
- self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
- self.render_value(true);
- });
- if (this.view && this.view.render_value_defs){
- this.view.render_value_defs.push(def);
- }
- }
- },
- display_string: function(str) {
- var self = this;
- if (!this.get("effective_readonly")) {
- this.$input.val(str.split("\n")[0]);
- this.current_display = this.$input.val();
- if (this.is_false()) {
- this.$('.oe_m2o_cm_button').css({'display':'none'});
- } else {
- this.$('.oe_m2o_cm_button').css({'display':'inline'});
- }
- } else {
- var lines = _.escape(str).split("\n");
- var link = "";
- var follow = "";
- link = lines[0];
- follow = _.rest(lines).join("<br />");
- if (follow)
- link += "<br />";
- var $link = this.$el.find('.oe_form_uri')
- .unbind('click')
- .html(link);
- if (! this.options.no_open)
- $link.click(function () {
- var context = self.build_context().eval();
- var model_obj = new instance.web.Model(self.field.relation);
- model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
- self.do_action(action);
- });
- return false;
- });
- $(".oe_form_m2o_follow", this.$el).html(follow);
- }
- },
- set_value: function(value_) {
- var self = this;
- if (value_ instanceof Array) {
- this.display_value = {};
- this.display_value_backup = {};
- if (! this.options.always_reload) {
- this.display_value["" + value_[0]] = value_[1];
- }
- else {
- this.display_value_backup["" + value_[0]] = value_[1];
- }
- value_ = value_[0];
- }
- value_ = value_ || false;
- this.reinit_value(value_);
- },
- get_displayed: function() {
- return this.display_value["" + this.get("value")];
- },
- add_id: function(id) {
- this.display_value = {};
- this.display_value_backup = {};
- this.reinit_value(id);
- },
- is_false: function() {
- return ! this.get("value");
- },
- focus: function () {
- var input = !this.get('effective_readonly') && this.$input && this.$input[0];
- return input ? input.focus() : false;
- },
- _quick_create: function() {
- this.no_ed = true;
- this.ed_def.reject();
- return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
- },
- _search_create_popup: function() {
- this.no_ed = true;
- this.ed_def.reject();
- this.ignore_focusout = true;
- this.reinit_value(false);
- var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
- this.ignore_focusout = false;
- this.no_ed = false;
- return res;
- },
- set_dimensions: function (height, width) {
- this._super(height, width);
- if (!this.get("effective_readonly") && this.$input)
- this.$input.css('height', height);
- }
-});
-
-instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
- template: 'Many2OneButton',
- init: function(field_manager, node) {
- this._super.apply(this, arguments);
- },
- start: function() {
- this._super.apply(this, arguments);
- this.set_button();
- },
- set_button: function() {
- var self = this;
- if (this.$button) {
- this.$button.remove();
- }
- this.string = '';
- this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
- this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
- this.$button.addClass('oe_link').css({'padding':'4px'});
- this.$el.append(this.$button);
- this.$button.on('click', self.on_click);
- },
- on_click: function(ev) {
- var self = this;
- this.popup = new instance.web.form.FormOpenPopup(this);
- this.popup.show_element(
- this.field.relation,
- this.get('value'),
- this.build_context(),
- {title: this.string}
- );
- this.popup.on('create_completed', self, function(r) {
- self.set_value(r);
- });
- },
- set_value: function(value_) {
- var self = this;
- if (value_ instanceof Array) {
- value_ = value_[0];
- }
- value_ = value_ || false;
- this.set('value', value_);
- this.set_button();
- },
-});
-
-/**
- * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
- * the big ugly button in the header.
- *
- * Requires the implementation of a ``is_readonly`` method (usually a proxy to
- * the corresponding field's readonly or effective_readonly property) to
- * decide whether the special row should or should not be inserted.
- *
- * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
- * set on the insertion row.
- */
-instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
- pad_table_to: function (count) {
- if (!this.view.is_action_enabled('create') || this.is_readonly()) {
- this._super(count);
- return;
- }
-
- this._super(count > 0 ? count - 1 : 0);
-
- var self = this;
- var columns = _(this.columns).filter(function (column) {
- return column.invisible !== '1';
- }).length;
- if (this.options.selectable) { columns++; }
- if (this.options.deletable) { columns++; }
-
- var $cell = $('<td>', {
- colspan: columns,
- 'class': this._add_row_class || ''
- }).append(
- $('<a>', {href: '#'}).text(_t("Add an item"))
- .mousedown(function () {
- // FIXME: needs to be an official API somehow
- if (self.view.editor.is_editing()) {
- self.view.__ignore_blur = true;
- }
- })
- .click(function (e) {
- e.preventDefault();
- e.stopPropagation();
- // FIXME: there should also be an API for that one
- if (self.view.editor.form.__blur_timeout) {
- clearTimeout(self.view.editor.form.__blur_timeout);
- self.view.editor.form.__blur_timeout = false;
- }
- self.view.ensure_saved().done(function () {
- self.view.do_add_record();
- });
- }));
-
- var $padding = this.$current.find('tr:not([data-id]):first');
- var $newrow = $('<tr>').append($cell);
- if ($padding.length) {
- $padding.before($newrow);
- } else {
- this.$current.append($newrow)
- }
- }
-});
-
-/*
-# Values: (0, 0, { fields }) create
-# (1, ID, { fields }) update
-# (2, ID) remove (delete)
-# (3, ID) unlink one (target id or target of relation)
-# (4, ID) link
-# (5) unlink all (only valid for one2many)
-*/
-var commands = {
- // (0, _, {values})
- CREATE: 0,
- 'create': function (values) {
- return [commands.CREATE, false, values];
- },
- // (1, id, {values})
- UPDATE: 1,
- 'update': function (id, values) {
- return [commands.UPDATE, id, values];
- },
- // (2, id[, _])
- DELETE: 2,
- 'delete': function (id) {
- return [commands.DELETE, id, false];
- },
- // (3, id[, _]) removes relation, but not linked record itself
- FORGET: 3,
- 'forget': function (id) {
- return [commands.FORGET, id, false];
- },
- // (4, id[, _])
- LINK_TO: 4,
- 'link_to': function (id) {
- return [commands.LINK_TO, id, false];
- },
- // (5[, _[, _]])
- DELETE_ALL: 5,
- 'delete_all': function () {
- return [5, false, false];
- },
- // (6, _, ids) replaces all linked records with provided ids
- REPLACE_WITH: 6,
- 'replace_with': function (ids) {
- return [6, false, ids];
- }
-};
-instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
- multi_selection: false,
- disable_utility_classes: true,
- init: function(field_manager, node) {
- this._super(field_manager, node);
- this.is_loaded = $.Deferred();
- this.initial_is_loaded = this.is_loaded;
- this.form_last_update = $.Deferred();
- this.init_form_last_update = this.form_last_update;
- this.is_started = false;
- this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
- this.dataset.o2m = this;
- this.dataset.parent_view = this.view;
- this.dataset.child_name = this.name;
- var self = this;
- this.dataset.on('dataset_changed', this, function() {
- self.trigger_on_change();
- });
- this.set_value([]);
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$el.addClass('oe_form_field oe_form_field_one2many');
-
- var self = this;
-
- self.load_views();
- var destroy = function() {
- self.is_loaded = self.is_loaded.then(function() {
- self.viewmanager.destroy();
- return $.when(self.load_views()).done(function() {
- self.reload_current_view();
- });
- });
- };
- this.is_loaded.done(function() {
- self.on("change:effective_readonly", self, destroy);
- });
- this.view.on("on_button_cancel", self, destroy);
- this.is_started = true;
- this.reload_current_view();
- },
- trigger_on_change: function() {
- this.trigger('changed_value');
- },
- load_views: function() {
- var self = this;
-
- var modes = this.node.attrs.mode;
- modes = !!modes ? modes.split(",") : ["tree"];
- var views = [];
- _.each(modes, function(mode) {
- if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
- throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
- }
- var view = {
- view_id: false,
- view_type: mode == "tree" ? "list" : mode,
- options: {}
- };
- if (self.field.views && self.field.views[mode]) {
- view.embedded_view = self.field.views[mode];
- }
- if(view.view_type === "list") {
- _.extend(view.options, {
- addable: null,
- selectable: self.multi_selection,
- sortable: true,
- import_enabled: false,
- deletable: true
- });
- if (self.get("effective_readonly")) {
- _.extend(view.options, {
- deletable: null,
- reorderable: false,
- });
- }
- } else if (view.view_type === "form") {
- if (self.get("effective_readonly")) {
- view.view_type = 'form';
- }
- _.extend(view.options, {
- not_interactible_on_create: true,
- });
- } else if (view.view_type === "kanban") {
- _.extend(view.options, {
- confirm_on_delete: false,
- });
- if (self.get("effective_readonly")) {
- _.extend(view.options, {
- action_buttons: false,
- quick_creatable: false,
- creatable: false,
- read_only_mode: true,
- });
- }
- }
- views.push(view);
- });
- this.views = views;
-
- this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
- this.viewmanager.o2m = self;
- var once = $.Deferred().done(function() {
- self.init_form_last_update.resolve();
- });
- var def = $.Deferred().done(function() {
- self.initial_is_loaded.resolve();
- });
- this.viewmanager.on("controller_inited", self, function(view_type, controller) {
- controller.o2m = self;
- if (view_type == "list") {
- if (self.get("effective_readonly")) {
- controller.on('edit:before', self, function (e) {
- e.cancel = true;
- });
- _(controller.columns).find(function (column) {
- if (!(column instanceof instance.web.list.Handle)) {
- return false;
- }
- column.modifiers.invisible = true;
- return true;
- });
- }
- } else if (view_type === "form") {
- if (self.get("effective_readonly")) {
- $(".oe_form_buttons", controller.$el).children().remove();
- }
- controller.on("load_record", self, function(){
- once.resolve();
- });
- controller.on('pager_action_executed',self,self.save_any_view);
- } else if (view_type == "graph") {
- self.reload_current_view();
- }
- def.resolve();
- });
- this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
- $.when(self.save_any_view()).done(function() {
- if (n_mode === "list") {
- $.async_when().done(function() {
- self.reload_current_view();
- });
- }
- });
- });
- $.async_when().done(function () {
- self.viewmanager.appendTo(self.$el);
- });
- return def;
- },
- reload_current_view: function() {
- var self = this;
- self.is_loaded = self.is_loaded.then(function() {
- var view = self.get_active_view();
- if (view.type === "list") {
- return view.controller.reload_content();
- } else if (view.type === "form") {
- if (self.dataset.index === null && self.dataset.ids.length >= 1) {
- self.dataset.index = 0;
- }
- var act = function() {
- return view.controller.do_show();
- };
- self.form_last_update = self.form_last_update.then(act, act);
- return self.form_last_update;
- } else if (view.controller.do_search) {
- return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
- }
- }, undefined);
- return self.is_loaded;
- },
- get_active_view: function () {
- /**
- * Returns the current active view if any.
- */
- return (this.viewmanager && this.viewmanager.active_view);
- },
- set_value: function(value_) {
- value_ = value_ || [];
- var self = this;
- var view = this.get_active_view();
- this.dataset.reset_ids([]);
- var ids;
- if(value_.length >= 1 && value_[0] instanceof Array) {
- ids = [];
- _.each(value_, function(command) {
- var obj = {values: command[2]};
- switch (command[0]) {
- case commands.CREATE:
- obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
- obj.defaults = {};
- self.dataset.to_create.push(obj);
- self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
- ids.push(obj.id);
- return;
- case commands.UPDATE:
- obj['id'] = command[1];
- self.dataset.to_write.push(obj);
- self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
- ids.push(obj.id);
- return;
- case commands.DELETE:
- self.dataset.to_delete.push({id: command[1]});
- return;
- case commands.LINK_TO:
- ids.push(command[1]);
- return;
- case commands.DELETE_ALL:
- self.dataset.delete_all = true;
- return;
- }
- });
- this._super(ids);
- this.dataset.set_ids(ids);
- } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
- ids = [];
- this.dataset.delete_all = true;
- _.each(value_, function(command) {
- var obj = {values: command};
- obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
- obj.defaults = {};
- self.dataset.to_create.push(obj);
- self.dataset.cache.push(_.clone(obj));
- ids.push(obj.id);
- });
- this._super(ids);
- this.dataset.set_ids(ids);
- } else {
- this._super(value_);
- this.dataset.reset_ids(value_);
- }
- if (this.dataset.index === null && this.dataset.ids.length > 0) {
- this.dataset.index = 0;
- }
- this.trigger_on_change();
- if (this.is_started && !this.no_rerender) {
- return self.reload_current_view();
- } else {
- return $.when();
- }
- },
- get_value: function() {
- var self = this;
- if (!this.dataset)
- return [];
- var val = this.dataset.delete_all ? [commands.delete_all()] : [];
- val = val.concat(_.map(this.dataset.ids, function(id) {
- var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
- if (alter_order) {
- return commands.create(alter_order.values);
- }
- alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
- if (alter_order) {
- return commands.update(alter_order.id, alter_order.values);
- }
- return commands.link_to(id);
- }));
- return val.concat(_.map(
- this.dataset.to_delete, function(x) {
- return commands['delete'](x.id);}));
- },
- commit_value: function() {
- return this.save_any_view();
- },
- save_any_view: function() {
- var view = this.get_active_view();
- if (view) {
- if (this.viewmanager.active_view.type === "form") {
- if (view.controller.is_initialized.state() !== 'resolved') {
- return $.when(false);
- }
- return $.when(view.controller.save());
- } else if (this.viewmanager.active_view.type === "list") {
- return $.when(view.controller.ensure_saved());
- }
- }
- return $.when(false);
- },
- is_syntax_valid: function() {
- var view = this.get_active_view();
- if (!view){
- return true;
- }
- switch (this.viewmanager.active_view.type) {
- case 'form':
- return _(view.controller.fields).chain()
- .invoke('is_valid')
- .all(_.identity)
- .value();
- case 'list':
- return view.controller.is_valid();
- }
- return true;
- },
-});
-
-instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
- template: 'One2Many.viewmanager',
- init: function(parent, dataset, views, flags) {
- this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
- this.registry = instance.web.views.extend({
- list: 'instance.web.form.One2ManyListView',
- form: 'instance.web.form.One2ManyFormView',
- });
- this.__ignore_blur = false;
- },
- switch_mode: function(mode, unused) {
- if (mode !== 'form') {
- return this._super(mode, unused);
- }
- var self = this;
- var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
- var pop = new instance.web.form.FormOpenPopup(this);
- pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
- title: _t("Open: ") + self.o2m.string,
- create_function: function(data, options) {
- return self.o2m.dataset.create(data, options).done(function(r) {
- self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
- self.o2m.dataset.trigger("dataset_changed", r);
- });
- },
- write_function: function(id, data, options) {
- return self.o2m.dataset.write(id, data, {}).done(function() {
- self.o2m.reload_current_view();
- });
- },
- alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
- parent_view: self.o2m.view,
- child_name: self.o2m.name,
- read_function: function() {
- return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
- },
- form_view_options: {'not_interactible_on_create':true},
- readonly: self.o2m.get("effective_readonly")
- });
- pop.on("elements_selected", self, function() {
- self.o2m.reload_current_view();
- });
- },
-});
-
-instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
- get_context: function() {
- this.context = this.o2m.build_context();
- return this.context;
- }
-});
-
-instance.web.form.One2ManyListView = instance.web.ListView.extend({
- _template: 'One2Many.listview',
- init: function (parent, dataset, view_id, options) {
- this._super(parent, dataset, view_id, _.extend(options || {}, {
- GroupsType: instance.web.form.One2ManyGroups,
- ListType: instance.web.form.One2ManyList
- }));
- this.on('edit:after', this, this.proxy('_after_edit'));
- this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
-
- this.records
- .bind('add', this.proxy("changed_records"))
- .bind('remove', this.proxy("changed_records"));
- this.on('save:after', this, this.proxy("changed_records"));
- },
- start: function () {
- var ret = this._super();
- this.$el
- .off('mousedown.handleButtons')
- .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
- return ret;
- },
- changed_records: function () {
- this.o2m.trigger_on_change();
- },
- is_valid: function () {
- var self = this;
- if (!this.fields_view || !this.editable()){
- return true;
- }
- if (_.isEmpty(this.records.records)){
- return true;
- }
- current_values = {};
- _.each(this.editor.form.fields, function(field){
- field._inhibit_on_change_flag = true;
- field.no_rerender = true;
- current_values[field.name] = field.get('value');
- });
- var valid = _.every(this.records.records, function(record){
- _.each(self.editor.form.fields, function(field){
- field.set_value(record.attributes[field.name]);
- });
- return _.every(self.editor.form.fields, function(field){
- field.process_modifiers();
- field._check_css_flags();
- return field.is_valid();
- });
- });
- _.each(this.editor.form.fields, function(field){
- field.set('value', current_values[field.name]);
- field._inhibit_on_change_flag = false;
- field.no_rerender = false;
- });
- return valid;
- },
- do_add_record: function () {
- if (this.editable()) {
- this._super.apply(this, arguments);
- } else {
- var self = this;
- var pop = new instance.web.form.SelectCreatePopup(this);
- pop.select_element(
- self.o2m.field.relation,
- {
- title: _t("Create: ") + self.o2m.string,
- initial_view: "form",
- alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
- create_function: function(data, options) {
- return self.o2m.dataset.create(data, options).done(function(r) {
- self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
- self.o2m.dataset.trigger("dataset_changed", r);
- });
- },
- read_function: function() {
- return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
- },
- parent_view: self.o2m.view,
- child_name: self.o2m.name,
- form_view_options: {'not_interactible_on_create':true}
- },
- self.o2m.build_domain(),
- self.o2m.build_context()
- );
- pop.on("elements_selected", self, function() {
- self.o2m.reload_current_view();
- });
- }
- },
- do_activate_record: function(index, id) {
- var self = this;
- var pop = new instance.web.form.FormOpenPopup(self);
- pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
- title: _t("Open: ") + self.o2m.string,
- write_function: function(id, data) {
- return self.o2m.dataset.write(id, data, {}).done(function() {
- self.o2m.reload_current_view();
- });
- },
- alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
- parent_view: self.o2m.view,
- child_name: self.o2m.name,
- read_function: function() {
- return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
- },
- form_view_options: {'not_interactible_on_create':true},
- readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
- });
- },
- do_button_action: function (name, id, callback) {
- if (!_.isNumber(id)) {
- instance.webclient.notification.warn(
- _t("Action Button"),
- _t("The o2m record must be saved before an action can be used"));
- return;
- }
- var parent_form = this.o2m.view;
- var self = this;
- this.ensure_saved().then(function () {
- if (parent_form)
- return parent_form.save();
- else
- return $.when();
- }).done(function () {
- var ds = self.o2m.dataset;
- var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
- return value.length;
- });
- if (!self.o2m.options.reload_on_button && !cached_records) {
- self.handle_button(name, id, callback);
- }else {
- self.handle_button(name, id, function(){
- self.o2m.view.reload();
- });
- }
- });
- },
-
- _after_edit: function () {
- this.__ignore_blur = false;
- this.editor.form.on('blurred', this, this._on_form_blur);
-
- // The form's blur thing may be jiggered during the edition setup,
- // potentially leading to the o2m instasaving the row. Cancel any
- // blurring triggered the edition startup here
- this.editor.form.widgetFocused();
- },
- _before_unedit: function () {
- this.editor.form.off('blurred', this, this._on_form_blur);
- },
- _button_down: function () {
- // If a button is clicked (usually some sort of action button), it's
- // the button's responsibility to ensure the editable list is in the
- // correct state -> ignore form blurring
- this.__ignore_blur = true;
- },
- /**
- * Handles blurring of the nested form (saves the currently edited row),
- * unless the flag to ignore the event is set to ``true``
- *
- * Makes the internal form go away
- */
- _on_form_blur: function () {
- if (this.__ignore_blur) {
- this.__ignore_blur = false;
- return;
- }
- // FIXME: why isn't there an API for this?
- if (this.editor.form.$el.hasClass('oe_form_dirty')) {
- this.ensure_saved();
- return;
- }
- this.cancel_edition();
- },
- keypress_ENTER: function () {
- // blurring caused by hitting the [Return] key, should skip the
- // autosave-on-blur and let the handler for [Return] do its thing (save
- // the current row *anyway*, then create a new one/edit the next one)
- this.__ignore_blur = true;
- this._super.apply(this, arguments);
- },
- do_delete: function (ids) {
- var confirm = window.confirm;
- window.confirm = function () { return true; };
- try {
- return this._super(ids);
- } finally {
- window.confirm = confirm;
- }
- },
- reload_record: function (record, options) {
- if (!options || !options['do_not_evict']) {
- // Evict record.id from cache to ensure it will be reloaded correctly
- this.dataset.evict_record(record.get('id'));
- }
-
- return this._super(record);
- }
-});
-instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
- setup_resequence_rows: function () {
- if (!this.view.o2m.get('effective_readonly')) {
- this._super.apply(this, arguments);
- }
- }
-});
-instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
- _add_row_class: 'oe_form_field_one2many_list_row_add',
- is_readonly: function () {
- return this.view.o2m.get('effective_readonly');
- },
-});
-
-instance.web.form.One2ManyFormView = instance.web.FormView.extend({
- form_template: 'One2Many.formview',
- load_form: function(data) {
- this._super(data);
- var self = this;
- this.$buttons.find('button.oe_form_button_create').click(function() {
- self.save().done(self.on_button_new);
- });
- },
- do_notify_change: function() {
- if (this.dataset.parent_view) {
- this.dataset.parent_view.do_notify_change();
- } else {
- this._super.apply(this, arguments);
- }
- }
-});
-
-instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
- template: "FieldMany2ManyTags",
- tag_template: "FieldMany2ManyTag",
- init: function() {
- this._super.apply(this, arguments);
- instance.web.form.CompletionFieldMixin.init.call(this);
- this.set({"value": []});
- this._display_orderer = new instance.web.DropMisordered();
- this._drop_shown = false;
- },
- initialize_texttext: function(){
- var self = this;
- return {
- plugins : 'tags arrow autocomplete',
- autocomplete: {
- render: function(suggestion) {
- return $('<span class="text-label"/>').
- data('index', suggestion['index']).html(suggestion['label']);
- }
- },
- ext: {
- autocomplete: {
- selectFromDropdown: function() {
- this.trigger('hideDropdown');
- var index = Number(this.selectedSuggestionElement().children().children().data('index'));
- var data = self.search_result[index];
- if (data.id) {
- self.add_id(data.id);
- } else {
- self.ignore_blur = true;
- data.action();
- }
- this.trigger('setSuggestions', {result : []});
- },
- },
- tags: {
- isTagAllowed: function(tag) {
- return !!tag.name;
-
- },
- removeTag: function(tag) {
- var id = tag.data("id");
- self.set({"value": _.without(self.get("value"), id)});
- },
- renderTag: function(stuff) {
- return $.fn.textext.TextExtTags.prototype.renderTag.
- call(this, stuff).data("id", stuff.id);
- },
- },
- itemManager: {
- itemToString: function(item) {
- return item.name;
- },
- },
- core: {
- onSetInputData: function(e, data) {
- if (data === '') {
- this._plugins.autocomplete._suggestions = null;
- }
- this.input().val(data);
- },
- },
- },
- }
- },
- initialize_content: function() {
- if (this.get("effective_readonly"))
- return;
- var self = this;
- self.ignore_blur = false;
- self.$text = this.$("textarea");
- self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
- var _this = this;
- var str = !!data ? data.query || '' : '';
- self.get_search_result(str).done(function(result) {
- self.search_result = result;
- $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
- return _.extend(el, {index:i});
- })});
- });
- }).bind('hideDropdown', function() {
- self._drop_shown = false;
- }).bind('showDropdown', function() {
- self._drop_shown = true;
- });
- self.tags = self.$text.textext()[0].tags();
- self.$text
- .focusin(function () {
- self.trigger('focused');
- self.ignore_blur = false;
- })
- .focusout(function() {
- self.$text.trigger("setInputData", "");
- if (!self.ignore_blur) {
- self.trigger('blurred');
- }
- }).keydown(function(e) {
- if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
- self.$text.textext()[0].autocomplete().selectFromDropdown();
- }
- });
- },
- // WARNING: duplicated in 4 other M2M widgets
- set_value: function(value_) {
- value_ = value_ || [];
- if (value_.length >= 1 && value_[0] instanceof Array) {
- // value_ is a list of m2m commands. We only process
- // LINK_TO and REPLACE_WITH in this context
- var val = [];
- _.each(value_, function (command) {
- if (command[0] === commands.LINK_TO) {
- val.push(command[1]); // (4, id[, _])
- } else if (command[0] === commands.REPLACE_WITH) {
- val = command[2]; // (6, _, ids)
- }
- });
- value_ = val;
- }
- this._super(value_);
- },
- is_false: function() {
- return _(this.get("value")).isEmpty();
- },
- get_value: function() {
- var tmp = [commands.replace_with(this.get("value"))];
- return tmp;
- },
- get_search_blacklist: function() {
- return this.get("value");
- },
- map_tag: function(data){
- return _.map(data, function(el) {return {name: el[1], id:el[0]};})
- },
- get_render_data: function(ids){
- var self = this;
- var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
- return dataset.name_get(ids);
- },
- render_tag: function(data) {
- var self = this;
- if (! self.get("effective_readonly")) {
- self.tags.containerElement().children().remove();
- self.$('textarea').css("padding-left", "3px");
- self.tags.addTags(self.map_tag(data));
- } else {
- self.$el.html(QWeb.render(self.tag_template, {elements: data}));
- }
- },
- render_value: function() {
- var self = this;
- var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
- var values = self.get("value");
- var handle_names = function(data) {
- if (self.isDestroyed())
- return;
- var indexed = {};
- _.each(data, function(el) {
- indexed[el[0]] = el;
- });
- data = _.map(values, function(el) { return indexed[el]; });
- self.render_tag(data);
- }
- if (! values || values.length > 0) {
- return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
- } else {
- handle_names([]);
- }
- },
- add_id: function(id) {
- this.set({'value': _.uniq(this.get('value').concat([id]))});
- },
- focus: function () {
- var input = this.$text && this.$text[0];
- return input ? input.focus() : false;
- },
- set_dimensions: function (height, width) {
- this._super(height, width);
- this.$("textarea").css({
- width: width,
- minHeight: height
- });
- },
- _search_create_popup: function() {
- self.ignore_blur = true;
- return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
- },
-});
-
-/**
- widget options:
- - reload_on_button: Reload the whole form view if click on a button in a list view.
- If you see this options, do not use it, it's basically a dirty hack to make one
- precise o2m to behave the way we want.
-*/
-instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- multi_selection: false,
- disable_utility_classes: true,
- init: function(field_manager, node) {
- this._super(field_manager, node);
- this.is_loaded = $.Deferred();
- this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
- this.dataset.m2m = this;
- var self = this;
- this.dataset.on('unlink', self, function(ids) {
- self.dataset_changed();
- });
- this.set_value([]);
- this.list_dm = new instance.web.DropMisordered();
- this.render_value_dm = new instance.web.DropMisordered();
- },
- initialize_content: function() {
- var self = this;
-
- this.$el.addClass('oe_form_field oe_form_field_many2many');
-
- this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
- 'addable': null,
- 'deletable': this.get("effective_readonly") ? false : true,
- 'selectable': this.multi_selection,
- 'sortable': false,
- 'reorderable': false,
- 'import_enabled': false,
- });
- var embedded = (this.field.views || {}).tree;
- if (embedded) {
- this.list_view.set_embedded_view(embedded);
- }
- this.list_view.m2m_field = this;
- var loaded = $.Deferred();
- this.list_view.on("list_view_loaded", this, function() {
- loaded.resolve();
- });
- this.list_view.appendTo(this.$el);
-
- var old_def = self.is_loaded;
- self.is_loaded = $.Deferred().done(function() {
- old_def.resolve();
- });
- this.list_dm.add(loaded).then(function() {
- self.is_loaded.resolve();
- });
- },
- destroy_content: function() {
- this.list_view.destroy();
- this.list_view = undefined;
- },
- // WARNING: duplicated in 4 other M2M widgets
- set_value: function(value_) {
- value_ = value_ || [];
- if (value_.length >= 1 && value_[0] instanceof Array) {
- // value_ is a list of m2m commands. We only process
- // LINK_TO and REPLACE_WITH in this context
- var val = [];
- _.each(value_, function (command) {
- if (command[0] === commands.LINK_TO) {
- val.push(command[1]); // (4, id[, _])
- } else if (command[0] === commands.REPLACE_WITH) {
- val = command[2]; // (6, _, ids)
- }
- });
- value_ = val;
- }
- this._super(value_);
- },
- get_value: function() {
- return [commands.replace_with(this.get('value'))];
- },
- is_false: function () {
- return _(this.get("value")).isEmpty();
- },
- render_value: function() {
- var self = this;
- this.dataset.set_ids(this.get("value"));
- this.render_value_dm.add(this.is_loaded).then(function() {
- return self.list_view.reload_content();
- });
- },
- dataset_changed: function() {
- this.internal_set_value(this.dataset.ids);
- },
-});
-
-instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
- get_context: function() {
- this.context = this.m2m.build_context();
- return this.context;
- }
-});
-
-/**
- * @class
- * @extends instance.web.ListView
- */
-instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
- init: function (parent, dataset, view_id, options) {
- this._super(parent, dataset, view_id, _.extend(options || {}, {
- ListType: instance.web.form.Many2ManyList,
- }));
- },
- do_add_record: function () {
- var pop = new instance.web.form.SelectCreatePopup(this);
- pop.select_element(
- this.model,
- {
- title: _t("Add: ") + this.m2m_field.string,
- alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views["form"] : undefined,
- no_create: this.m2m_field.options.no_create,
- },
- new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
- this.m2m_field.build_context()
- );
- var self = this;
- pop.on("elements_selected", self, function(element_ids) {
- var reload = false;
- _(element_ids).each(function (id) {
- if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
- self.dataset.set_ids(self.dataset.ids.concat([id]));
- self.m2m_field.dataset_changed();
- reload = true;
- }
- });
- if (reload) {
- self.reload_content();
- }
- });
- },
- do_activate_record: function(index, id) {
- var self = this;
- var pop = new instance.web.form.FormOpenPopup(this);
- pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
- title: _t("Open: ") + this.m2m_field.string,
- alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views["form"] : undefined,
- readonly: this.getParent().get("effective_readonly")
- });
- pop.on('write_completed', self, self.reload_content);
- },
- do_button_action: function(name, id, callback) {
- var self = this;
- var _sup = _.bind(this._super, this);
- if (! this.m2m_field.options.reload_on_button) {
- return _sup(name, id, callback);
- } else {
- return this.m2m_field.view.save().then(function() {
- return _sup(name, id, function() {
- self.m2m_field.view.reload();
- });
- });
- }
- },
- is_action_enabled: function () { return true; },
-});
-instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
- _add_row_class: 'oe_form_field_many2many_list_row_add',
- is_readonly: function () {
- return this.view.m2m_field.get('effective_readonly');
- }
-});
-
-instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
- disable_utility_classes: true,
- init: function(field_manager, node) {
- this._super(field_manager, node);
- instance.web.form.CompletionFieldMixin.init.call(this);
- m2m_kanban_lazy_init();
- this.is_loaded = $.Deferred();
- this.initial_is_loaded = this.is_loaded;
-
- var self = this;
- this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
- this.dataset.m2m = this;
- this.dataset.on('unlink', self, function(ids) {
- self.dataset_changed();
- });
- },
- start: function() {
- this._super.apply(this, arguments);
-
- var self = this;
-
- self.load_view();
- self.on("change:effective_readonly", self, function() {
- self.is_loaded = self.is_loaded.then(function() {
- self.kanban_view.destroy();
- return $.when(self.load_view()).done(function() {
- self.render_value();
- });
- });
- });
- },
- // WARNING: duplicated in 4 other M2M widgets
- set_value: function(value_) {
- value_ = value_ || [];
- if (value_.length >= 1 && value_[0] instanceof Array) {
- // value_ is a list of m2m commands. We only process
- // LINK_TO and REPLACE_WITH in this context
- var val = [];
- _.each(value_, function (command) {
- if (command[0] === commands.LINK_TO) {
- val.push(command[1]); // (4, id[, _])
- } else if (command[0] === commands.REPLACE_WITH) {
- val = command[2]; // (6, _, ids)
- }
- });
- value_ = val;
- }
- this._super(value_);
- },
- get_value: function() {
- return [commands.replace_with(this.get('value'))];
- },
- load_view: function() {
- var self = this;
- this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
- 'create_text': _t("Add"),
- 'creatable': self.get("effective_readonly") ? false : true,
- 'quick_creatable': self.get("effective_readonly") ? false : true,
- 'read_only_mode': self.get("effective_readonly") ? true : false,
- 'confirm_on_delete': false,
- });
- var embedded = (this.field.views || {}).kanban;
- if (embedded) {
- this.kanban_view.set_embedded_view(embedded);
- }
- this.kanban_view.m2m = this;
- var loaded = $.Deferred();
- this.kanban_view.on("kanban_view_loaded",self,function() {
- self.initial_is_loaded.resolve();
- loaded.resolve();
- });
- this.kanban_view.on('switch_mode', this, this.open_popup);
- $.async_when().done(function () {
- self.kanban_view.appendTo(self.$el);
- });
- return loaded;
- },
- render_value: function() {
- var self = this;
- this.dataset.set_ids(this.get("value"));
- this.is_loaded = this.is_loaded.then(function() {
- return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
- });
- },
- dataset_changed: function() {
- this.set({'value': this.dataset.ids});
- },
- open_popup: function(type, unused) {
- if (type !== "form")
- return;
- var self = this;
- var pop;
- if (this.dataset.index === null) {
- pop = new instance.web.form.SelectCreatePopup(this);
- pop.select_element(
- this.field.relation,
- {
- title: _t("Add: ") + this.string
- },
- new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
- this.build_context()
- );
- pop.on("elements_selected", self, function(element_ids) {
- _.each(element_ids, function(one_id) {
- if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
- self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
- self.dataset_changed();
- self.render_value();
- }
- });
- });
- } else {
- var id = self.dataset.ids[self.dataset.index];
- pop = new instance.web.form.FormOpenPopup(this);
- pop.show_element(self.field.relation, id, self.build_context(), {
- title: _t("Open: ") + self.string,
- write_function: function(id, data, options) {
- return self.dataset.write(id, data, {}).done(function() {
- self.render_value();
- });
- },
- alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
- parent_view: self.view,
- child_name: self.name,
- readonly: self.get("effective_readonly")
- });
- }
- },
- add_id: function(id) {
- this.quick_create.add_id(id);
- },
-});
-
-function m2m_kanban_lazy_init() {
-if (instance.web.form.Many2ManyKanbanView)
- return;
-instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
- quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
- _is_quick_create_enabled: function() {
- return this._super() && ! this.group_by;
- },
-});
-instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
- template: 'Many2ManyKanban.quick_create',
-
- /**
- * close_btn: If true, the widget will display a "Close" button able to trigger
- * a "close" event.
- */
- init: function(parent, dataset, context, buttons) {
- this._super(parent);
- this.m2m = this.getParent().view.m2m;
- this.m2m.quick_create = this;
- this._dataset = dataset;
- this._buttons = buttons || false;
- this._context = context || {};
- },
- start: function () {
- var self = this;
- self.$text = this.$el.find('input').css("width", "200px");
- self.$text.textext({
- plugins : 'arrow autocomplete',
- autocomplete: {
- render: function(suggestion) {
- return $('<span class="text-label"/>').
- data('index', suggestion['index']).html(suggestion['label']);
- }
- },
- ext: {
- autocomplete: {
- selectFromDropdown: function() {
- $(this).trigger('hideDropdown');
- var index = Number(this.selectedSuggestionElement().children().children().data('index'));
- var data = self.search_result[index];
- if (data.id) {
- self.add_id(data.id);
- } else {
- data.action();
- }
- },
- },
- itemManager: {
- itemToString: function(item) {
- return item.name;
- },
- },
- },
- }).bind('getSuggestions', function(e, data) {
- var _this = this;
- var str = !!data ? data.query || '' : '';
- self.m2m.get_search_result(str).done(function(result) {
- self.search_result = result;
- $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
- return _.extend(el, {index:i});
- })});
- });
- });
- self.$text.focusout(function() {
- self.$text.val("");
- });
- },
- focus: function() {
- this.$text[0].focus();
- },
- add_id: function(id) {
- var self = this;
- self.$text.val("");
- self.trigger('added', id);
- this.m2m.dataset_changed();
- },
-});
-}
-
-/**
- * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
- */
-instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
- template: "AbstractFormPopup.render",
- /**
- * options:
- * -readonly: only applicable when not in creation mode, default to false
- * - alternative_form_view
- * - view_id
- * - write_function
- * - read_function
- * - create_function
- * - parent_view
- * - child_name
- * - form_view_options
- */
- init_popup: function(model, row_id, domain, context, options) {
- this.row_id = row_id;
- this.model = model;
- this.domain = domain || [];
- this.context = context || {};
- this.options = options;
- _.defaults(this.options, {});
- },
- init_dataset: function() {
- var self = this;
- this.created_elements = [];
- this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
- this.dataset.read_function = this.options.read_function;
- this.dataset.create_function = function(data, options, sup) {
- var fct = self.options.create_function || sup;
- return fct.call(this, data, options).done(function(r) {
- self.trigger('create_completed saved', r);
- self.created_elements.push(r);
- });
- };
- this.dataset.write_function = function(id, data, options, sup) {
- var fct = self.options.write_function || sup;
- return fct.call(this, id, data, options).done(function(r) {
- self.trigger('write_completed saved', r);
- });
- };
- this.dataset.parent_view = this.options.parent_view;
- this.dataset.child_name = this.options.child_name;
- },
- display_popup: function() {
- var self = this;
- this.renderElement();
- var dialog = new instance.web.Dialog(this, {
- dialogClass: 'oe_act_window',
- title: this.options.title || "",
- }, this.$el).open();
- dialog.on('closing', this, function (e){
- self.check_exit(true);
- });
- this.$buttonpane = dialog.$buttons;
- this.start();
- },
- setup_form_view: function() {
- var self = this;
- if (this.row_id) {
- this.dataset.ids = [this.row_id];
- this.dataset.index = 0;
- } else {
- this.dataset.index = null;
- }
- var options = _.clone(self.options.form_view_options) || {};
- if (this.row_id !== null) {
- options.initial_mode = this.options.readonly ? "view" : "edit";
- }
- _.extend(options, {
- $buttons: this.$buttonpane,
- });
- this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
- if (this.options.alternative_form_view) {
- this.view_form.set_embedded_view(this.options.alternative_form_view);
- }
- this.view_form.appendTo(this.$(".oe_popup_form").show());
- this.view_form.on("form_view_loaded", self, function() {
- var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
- self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
- multi_select: multi_select,
- readonly: self.row_id !== null && self.options.readonly,
- }));
- var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
- $snbutton.click(function() {
- $.when(self.view_form.save()).done(function() {
- self.view_form.reload_mutex.exec(function() {
- self.view_form.on_button_new();
- });
- });
- });
- var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
- $sbutton.click(function() {
- $.when(self.view_form.save()).done(function() {
- self.view_form.reload_mutex.exec(function() {
- self.check_exit();
- });
- });
- });
- var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
- $cbutton.click(function() {
- self.view_form.trigger('on_button_cancel');
- self.check_exit();
- });
- self.view_form.do_show();
- });
- },
- select_elements: function(element_ids) {
- this.trigger("elements_selected", element_ids);
- },
- check_exit: function(no_destroy) {
- if (this.created_elements.length > 0) {
- this.select_elements(this.created_elements);
- this.created_elements = [];
- }
- this.trigger('closed');
- this.destroy();
- },
- destroy: function () {
- this.trigger('closed');
- if (this.$el.is(":data(bs.modal)")) {
- this.$el.parents('.modal').modal('hide');
- }
- this._super();
- },
-});
-
-/**
- * Class to display a popup containing a form view.
- */
-instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
- show_element: function(model, row_id, context, options) {
- this.init_popup(model, row_id, [], context, options);
- _.defaults(this.options, {
- });
- this.display_popup();
- },
- start: function() {
- this._super();
- this.init_dataset();
- this.setup_form_view();
- },
-});
-
-/**
- * Class to display a popup to display a list to search a row. It also allows
- * to switch to a form view to create a new row.
- */
-instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
- /**
- * options:
- * - initial_ids
- * - initial_view: form or search (default search)
- * - disable_multiple_selection
- * - list_view_options
- */
- select_element: function(model, options, domain, context) {
- this.init_popup(model, null, domain, context, options);
- var self = this;
- _.defaults(this.options, {
- initial_view: "search",
- });
- this.initial_ids = this.options.initial_ids;
- this.display_popup();
- },
- start: function() {
- this.init_dataset();
- if (this.options.initial_view == "search") {
- var context = instance.web.pyeval.sync_eval_domains_and_contexts({
- domains: [],
- contexts: [this.context]
- }).context;
- var search_defaults = {};
- _.each(context, function (value_, key) {
- var match = /^search_default_(.*)$/.exec(key);
- if (match) {
- search_defaults[match[1]] = value_;
- }
- });
- this.setup_search_view(search_defaults);
- } else { // "form"
- this.new_object();
- }
- },
- setup_search_view: function(search_defaults) {
- var self = this;
- if (this.searchview) {
- this.searchview.destroy();
- }
- var $buttons = this.$('.o-search-options');
- this.searchview = new instance.web.SearchView(this,
- this.dataset, false, search_defaults, {$buttons: $buttons});
- this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
- if (self.initial_ids) {
- self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
- contexts.concat(self.context), groupbys);
- self.initial_ids = undefined;
- } else {
- self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
- }
- });
- this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
- self.searchview.toggle_visibility(true);
- self.view_list = new instance.web.form.SelectCreateListView(self,
- self.dataset, false,
- _.extend({'deletable': false,
- 'selectable': !self.options.disable_multiple_selection,
- 'import_enabled': false,
- '$buttons': self.$buttonpane,
- 'disable_editable_mode': true,
- '$pager': self.$('.oe_popup_list_pager'),
- }, self.options.list_view_options || {}));
- self.view_list.on('edit:before', self, function (e) {
- e.cancel = true;
- });
- self.view_list.popup = self;
- self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
- self.view_list.do_show();
- }).then(function() {
- if (self.options.initial_facet) {
- self.searchview.query.reset([self.options.initial_facet], {
- preventSearch: true,
- });
- }
- self.searchview.do_search();
- });
- self.view_list.on("list_view_loaded", self, function() {
- self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
- var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
- $cbutton.click(function() {
- self.destroy();
- });
- var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
- $sbutton.click(function() {
- self.select_elements(self.selected_ids);
- self.destroy();
- });
- $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
- $cbutton.click(function() {
- self.new_object();
- });
- });
- });
- },
- do_search: function(domains, contexts, groupbys) {
- var results = instance.web.pyeval.sync_eval_domains_and_contexts({
- domains: domains || [],
- contexts: contexts || [],
- group_by_seq: groupbys || []
- });
- this.view_list.do_search(results.domain, results.context, results.group_by);
- },
- on_click_element: function(ids) {
- var self = this;
- this.selected_ids = ids || [];
- if(this.selected_ids.length > 0) {
- self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
- } else {
- self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
- }
- },
- new_object: function() {
- if (this.searchview) {
- this.searchview.do_hide();
- }
- if (this.view_list) {
- this.view_list.do_hide();
- this.view_list.$el.parent().hide();
- }
- this.setup_form_view();
- },
-});
-
-instance.web.form.DomainEditorPopup = instance.web.form.SelectCreatePopup.extend({
- select_element: function (model, options, domain, context) {
- if (options.readonly) {
- return this._super(model, options, domain, context);
- }
- options.initial_facet = {
- category: _t("Custom Filter"),
- icon: 'fa-star',
- field: {
- get_context: function () { return context; },
- get_groupby: function () { return []; },
- get_domain: function () { return domain; },
- },
- values: [{label: _t("Selected domain"), value: null}],
- };
- this._super(model, options, [], context);
- },
- get_domain: function (selected_ids) {
- var group_domain = [],
- domain;
- if (this.$('input.oe_list_record_selector').prop('checked')) {
- if (this.view_list.grouped) {
- var group_domain = _.chain(_.values(this.view_list.groups.children))
- .filter(function (child) { return child.records.length; })
- .map(function (c) { return c.datagroup.domain;})
- .value();
- group_domain = _.flatten(group_domain, true);
- group_domain = _.times(group_domain.length - 1, _.constant('|')).concat(group_domain);
- }
- var search_data = this.searchview.build_search_data();
- domain = instance.web.pyeval.sync_eval_domains_and_contexts({
- domains: search_data.domains,
- contexts: search_data.contexts,
- group_by_seq: search_data.groupbys || []
- }).domain;
- }
- else {
- domain = [["id", "in", selected_ids]];
- }
- return this.dataset.domain.concat(group_domain).concat(domain || []);
- },
-});
-
-instance.web.form.SelectCreateListView = instance.web.ListView.extend({
- do_add_record: function () {
- this.popup.new_object();
- },
- select_record: function(index) {
- this.popup.select_elements([this.dataset.ids[index]]);
- this.popup.destroy();
- },
- do_select: function(ids, records) {
- this._super(ids, records);
- this.popup.on_click_element(ids);
- }
-});
-
-instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: 'FieldReference',
- init: function(field_manager, node) {
- this._super(field_manager, node);
- this.reference_ready = true;
- },
- destroy_content: function() {
- if (this.fm) {
- this.fm.destroy();
- this.fm = undefined;
- }
- },
- initialize_content: function() {
- var self = this;
- var fm = new instance.web.form.DefaultFieldManager(this);
- this.fm = fm;
- fm.extend_field_desc({
- "selection": {
- selection: this.field_manager.get_field_desc(this.name).selection,
- type: "selection",
- },
- "m2o": {
- relation: null,
- type: "many2one",
- },
- });
- this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
- name: 'selection',
- modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
- }});
- this.selection.on("change:value", this, this.on_selection_changed);
- this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
- this.selection
- .on('focused', null, function () {self.trigger('focused');})
- .on('blurred', null, function () {self.trigger('blurred');});
-
- this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
- name: 'Referenced Document',
- modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
- }});
- this.m2o.on("change:value", this, this.data_changed);
- this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
- this.m2o
- .on('focused', null, function () {self.trigger('focused');})
- .on('blurred', null, function () {self.trigger('blurred');});
- },
- on_selection_changed: function() {
- if (this.reference_ready) {
- this.internal_set_value([this.selection.get_value(), false]);
- this.render_value();
- }
- },
- data_changed: function() {
- if (this.reference_ready) {
- this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
- }
- },
- set_value: function(val) {
- if (val) {
- val = val.split(',');
- val[0] = val[0] || false;
- val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
- }
- this._super(val || [false, false]);
- },
- get_value: function() {
- return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
- },
- render_value: function() {
- this.reference_ready = false;
- if (!this.get("effective_readonly")) {
- this.selection.set_value(this.get('value')[0]);
- }
- this.m2o.field.relation = this.get('value')[0];
- this.m2o.set_value(this.get('value')[1]);
- this.m2o.$el.toggle(!!this.get('value')[0]);
- this.reference_ready = true;
- },
-});
-
-instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- init: function(field_manager, node) {
- var self = this;
- this._super(field_manager, node);
- this.binary_value = false;
- this.useFileAPI = !!window.FileReader;
- this.max_upload_size = 25 * 1024 * 1024; // 25Mo
- if (!this.useFileAPI) {
- this.fileupload_id = _.uniqueId('oe_fileupload');
- $(window).on(this.fileupload_id, function() {
- var args = [].slice.call(arguments).slice(1);
- self.on_file_uploaded.apply(self, args);
- });
- }
- },
- stop: function() {
- if (!this.useFileAPI) {
- $(window).off(this.fileupload_id);
- }
- this._super.apply(this, arguments);
- },
- initialize_content: function() {
- var self= this;
- this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
- this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
- this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
- this.$el.find('.oe_form_binary_file_edit').click(function(event){
- self.$el.find('input.oe_form_binary_file').click();
- });
- },
- on_file_change: function(e) {
- var self = this;
- var file_node = e.target;
- if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
- if (this.useFileAPI) {
- var file = file_node.files[0];
- if (file.size > this.max_upload_size) {
- var msg = _t("The selected file exceed the maximum file size of %s.");
- instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
- return false;
- }
- var filereader = new FileReader();
- filereader.readAsDataURL(file);
- filereader.onloadend = function(upload) {
- var data = upload.target.result;
- data = data.split(',')[1];
- self.on_file_uploaded(file.size, file.name, file.type, data);
- };
- } else {
- this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
- this.$el.find('form.oe_form_binary_form').submit();
- }
- this.$el.find('.oe_form_binary_progress').show();
- this.$el.find('.oe_form_binary').hide();
- }
- },
- on_file_uploaded: function(size, name, content_type, file_base64) {
- if (size === false) {
- this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
- // TODO: use openerp web crashmanager
- console.warn("Error while uploading file : ", name);
- } else {
- this.filename = name;
- this.on_file_uploaded_and_valid.apply(this, arguments);
- }
- this.$el.find('.oe_form_binary_progress').hide();
- this.$el.find('.oe_form_binary').show();
- },
- on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
- },
- on_save_as: function(ev) {
- var value = this.get('value');
- if (!value) {
- this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
- ev.stopPropagation();
- } else {
- instance.web.blockUI();
- var c = instance.webclient.crashmanager;
- var filename_fieldname = this.node.attrs.filename;
- var filename_field = this.view.fields && this.view.fields[filename_fieldname];
- this.session.get_file({
- url: '/web/binary/saveas_ajax',
- data: {data: JSON.stringify({
- model: this.view.dataset.model,
- id: (this.view.datarecord.id || ''),
- field: this.name,
- filename_field: (filename_fieldname || ''),
- data: instance.web.form.is_bin_size(value) ? null : value,
- filename: filename_field ? filename_field.get('value') : null,
- context: this.view.dataset.get_context()
- })},
- complete: instance.web.unblockUI,
- error: c.rpc_error.bind(c)
- });
- ev.stopPropagation();
- return false;
- }
- },
- set_filename: function(value) {
- var filename = this.node.attrs.filename;
- if (filename) {
- var tmp = {};
- tmp[filename] = value;
- this.field_manager.set_values(tmp);
- }
- },
- on_clear: function() {
- if (this.get('value') !== false) {
- this.binary_value = false;
- this.internal_set_value(false);
- }
- return false;
- }
-});
-
-instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
- template: 'FieldBinaryFile',
- initialize_content: function() {
- this._super();
- if (this.get("effective_readonly")) {
- var self = this;
- this.$el.find('a').click(function(ev) {
- if (self.get('value')) {
- self.on_save_as(ev);
- }
- return false;
- });
- }
- },
- render_value: function() {
- var show_value;
- if (!this.get("effective_readonly")) {
- if (this.node.attrs.filename) {
- show_value = this.view.datarecord[this.node.attrs.filename] || '';
- } else {
- show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
- }
- this.$el.find('input').eq(0).val(show_value);
- } else {
- this.$el.find('a').toggle(!!this.get('value'));
- if (this.get('value')) {
- show_value = _t("Download");
- if (this.view)
- show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
- this.$el.find('a').text(show_value);
- }
- }
- },
- on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
- this.binary_value = true;
- this.internal_set_value(file_base64);
- var show_value = name + " (" + instance.web.human_size(size) + ")";
- this.$el.find('input').eq(0).val(show_value);
- this.set_filename(name);
- },
- on_clear: function() {
- this._super.apply(this, arguments);
- this.$el.find('input').eq(0).val('');
- this.set_filename('');
- }
-});
-
-instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
- template: 'FieldBinaryImage',
- placeholder: "/web/static/src/img/placeholder.png",
- render_value: function() {
- var self = this;
- var url;
- if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
- url = 'data:image/png;base64,' + this.get('value');
- } else if (this.get('value')) {
- var id = JSON.stringify(this.view.datarecord.id || null);
- var field = this.name;
- if (this.options.preview_image)
- field = this.options.preview_image;
- url = this.session.url('/web/binary/image', {
- model: this.view.dataset.model,
- id: id,
- field: field,
- t: (new Date().getTime()),
- });
- } else {
- url = this.placeholder;
- }
- var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
- $($img).click(function(e) {
- if(self.view.get("actual_mode") == "view") {
- var $button = $(".oe_form_button_edit");
- $button.openerpBounce();
- e.stopPropagation();
- }
- });
- this.$el.find('> img').remove();
- this.$el.prepend($img);
- $img.load(function() {
- if (! self.options.size)
- return;
- $img.css("max-width", "" + self.options.size[0] + "px");
- $img.css("max-height", "" + self.options.size[1] + "px");
- });
- $img.on('error', function() {
- $img.attr('src', self.placeholder);
- instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
- });
- },
- on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
- this.internal_set_value(file_base64);
- this.binary_value = true;
- this.render_value();
- this.set_filename(name);
- },
- on_clear: function() {
- this._super.apply(this, arguments);
- this.render_value();
- this.set_filename('');
- },
- set_value: function(value_){
- var changed = value_ !== this.get_value();
- this._super.apply(this, arguments);
- // By default, on binary images read, the server returns the binary size
- // This is possible that two images have the exact same size
- // Therefore we trigger the change in case the image value hasn't changed
- // So the image is re-rendered correctly
- if (!changed){
- this.trigger("change:value", this, {
- oldValue: value_,
- newValue: value_
- });
- }
- }
-});
-
-/**
- * Widget for (many2many field) to upload one or more file in same time and display in list.
- * The user can delete his files.
- * Options on attribute ; "blockui" {Boolean} block the UI or not
- * during the file is uploading
- */
-instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- template: "FieldBinaryFileUploader",
- init: function(field_manager, node) {
- this._super(field_manager, node);
- this.field_manager = field_manager;
- this.node = node;
- if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
- throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
- }
- this.data = {};
- this.set_value([]);
- this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
- this.fileupload_id = _.uniqueId('oe_fileupload_temp');
- $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
- },
- initialize_content: function() {
- this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
- },
- // WARNING: duplicated in 4 other M2M widgets
- set_value: function(value_) {
- value_ = value_ || [];
- if (value_.length >= 1 && value_[0] instanceof Array) {
- // value_ is a list of m2m commands. We only process
- // LINK_TO and REPLACE_WITH in this context
- var val = [];
- _.each(value_, function (command) {
- if (command[0] === commands.LINK_TO) {
- val.push(command[1]); // (4, id[, _])
- } else if (command[0] === commands.REPLACE_WITH) {
- val = command[2]; // (6, _, ids)
- }
- });
- value_ = val;
- }
- this._super(value_);
- },
- get_value: function() {
- var tmp = [commands.replace_with(this.get("value"))];
- return tmp;
- },
- get_file_url: function (attachment) {
- return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
- },
- read_name_values : function () {
- var self = this;
- // don't reset know values
- var ids = this.get('value');
- var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
- // send request for get_name
- if (_value.length) {
- return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
- _.each(datas, function (data) {
- data.no_unlink = true;
- data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
- self.data[data.id] = data;
- });
- return ids;
- });
- } else {
- return $.when(ids);
- }
- },
- render_value: function () {
- var self = this;
- this.read_name_values().then(function (ids) {
- var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
- render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
- self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
-
- // reinit input type file
- var $input = self.$('input.oe_form_binary_file');
- $input.after($input.clone(true)).remove();
- self.$(".oe_fileupload").show();
-
- });
- },
- on_file_change: function (event) {
- event.stopPropagation();
- var self = this;
- var $target = $(event.target);
- if ($target.val() !== '') {
- var filename = $target.val().replace(/.*[\\\/]/,'');
- // don't uplode more of one file in same time
- if (self.data[0] && self.data[0].upload ) {
- return false;
- }
- for (var id in this.get('value')) {
- // if the files exits, delete the file before upload (if it's a new file)
- if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
- self.ds_file.unlink([id]);
- }
- }
-
- // block UI or not
- if(this.node.attrs.blockui>0) {
- instance.web.blockUI();
- }
-
- // TODO : unactivate send on wizard and form
-
- // submit file
- this.$('form.oe_form_binary_form').submit();
- this.$(".oe_fileupload").hide();
- // add file on data result
- this.data[0] = {
- 'id': 0,
- 'name': filename,
- 'filename': filename,
- 'url': '',
- 'upload': true
- };
- }
- },
- on_file_loaded: function (event, result) {
- var files = this.get('value');
-
- // unblock UI
- if(this.node.attrs.blockui>0) {
- instance.web.unblockUI();
- }
-
- if (result.error || !result.id ) {
- this.do_warn( _t('Uploading Error'), result.error);
- delete this.data[0];
- } else {
- if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
- delete this.data[0];
- this.data[result.id] = {
- 'id': result.id,
- 'name': result.name,
- 'filename': result.filename,
- 'url': this.get_file_url(result)
- };
- } else {
- this.data[result.id] = {
- 'id': result.id,
- 'name': result.name,
- 'filename': result.filename,
- 'url': this.get_file_url(result)
- };
- }
- var values = _.clone(this.get('value'));
- values.push(result.id);
- this.set({'value': values});
- }
- this.render_value();
- },
- on_file_delete: function (event) {
- event.stopPropagation();
- var file_id=$(event.target).data("id");
- if (file_id) {
- var files = _.filter(this.get('value'), function (id) {return id != file_id;});
- if(!this.data[file_id].no_unlink) {
- this.ds_file.unlink([file_id]);
- }
- this.set({'value': files});
- }
- },
-});
-
-instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
- template: "FieldStatus",
- init: function(field_manager, node) {
- this._super(field_manager, node);
- this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
- this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
- this.set({value: false});
- this.selection = {'unfolded': [], 'folded': []};
- this.set("selection", {'unfolded': [], 'folded': []});
- this.selection_dm = new instance.web.DropMisordered();
- this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
- },
- start: function() {
- this.field_manager.on("view_content_has_changed", this, this.calc_domain);
- this.calc_domain();
- this.on("change:value", this, this.get_selection);
- this.on("change:evaluated_selection_domain", this, this.get_selection);
- this.on("change:selection", this, function() {
- this.selection = this.get("selection");
- this.render_value();
- });
- this.get_selection();
- if (this.options.clickable) {
- this.$el.on('click','li[data-id]',this.on_click_stage);
- }
- if (this.$el.parent().is('header')) {
- this.$el.after('<div class="oe_clear"/>');
- }
- this._super();
- },
- set_value: function(value_) {
- if (value_ instanceof Array) {
- value_ = value_[0];
- }
- this._super(value_);
- },
- render_value: function() {
- var self = this;
- var content = QWeb.render("FieldStatus.content", {
- 'widget': self,
- 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
- });
- self.$el.html(content);
- },
- calc_domain: function() {
- var d = instance.web.pyeval.eval('domain', this.build_domain());
- var domain = []; //if there is no domain defined, fetch all the records
-
- if (d.length) {
- domain = ['|',['id', '=', this.get('value')]].concat(d);
- }
-
- if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
- this.set("evaluated_selection_domain", domain);
- }
- },
- /** Get the selection and render it
- * selection: [[identifier, value_to_display], ...]
- * For selection fields: this is directly given by this.field.selection
- * For many2one fields: perform a search on the relation of the many2one field
- */
- get_selection: function() {
- var self = this;
- var selection_unfolded = [];
- var selection_folded = [];
- var fold_field = this.options.fold_field;
-
- var calculation = _.bind(function() {
- if (this.field.type == "many2one") {
- return self.get_distant_fields().then(function (fields) {
- return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
- .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
- var ids = _.pluck(records, 'id');
- return self.dataset.name_get(ids).then(function (records_name) {
- _.each(records, function (record) {
- var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
- if (fold_field && record[fold_field] && record.id != self.get('value')) {
- selection_folded.push([record.id, name]);
- } else {
- selection_unfolded.push([record.id, name]);
- }
- });
- });
- });
- });
- } else {
- // For field type selection filter values according to
- // statusbar_visible attribute of the field. For example:
- // statusbar_visible="draft,open".
- var select = this.field.selection;
- for(var i=0; i < select.length; i++) {
- var key = select[i][0];
- if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
- selection_unfolded.push(select[i]);
- }
- }
- return $.when();
- }
- }, this);
- this.selection_dm.add(calculation()).then(function () {
- var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
- if (! _.isEqual(selection, self.get("selection"))) {
- self.set("selection", selection);
- }
- });
- },
- /*
- * :deprecated: this feature will probably be removed with OpenERP v8
- */
- get_distant_fields: function() {
- var self = this;
- if (! this.options.fold_field) {
- this.distant_fields = {}
- }
- if (this.distant_fields) {
- return $.when(this.distant_fields);
- }
- return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
- self.distant_fields = fields;
- return fields;
- });
- },
- on_click_stage: function (ev) {
- var self = this;
- var $li = $(ev.currentTarget);
- var val;
- if (this.field.type == "many2one") {
- val = parseInt($li.data("id"), 10);
- }
- else {
- val = $li.data("id");
- }
- if (val != self.get('value')) {
- this.view.recursive_save().done(function() {
- var change = {};
- change[self.name] = val;
- self.view.dataset.write(self.view.datarecord.id, change).done(function() {
- self.view.reload();
- });
- });
- }
- },
-});
-
-instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
- template: "FieldMonetary",
- widget_class: 'oe_form_field_float oe_form_field_monetary',
- init: function() {
- this._super.apply(this, arguments);
- this.set({"currency": false});
- if (this.options.currency_field) {
- this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
- this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
- });
- }
- this.on("change:currency", this, this.get_currency_info);
- this.get_currency_info();
- this.ci_dm = new instance.web.DropMisordered();
- },
- start: function() {
- var tmp = this._super();
- this.on("change:currency_info", this, this.reinitialize);
- return tmp;
- },
- get_currency_info: function() {
- var self = this;
- if (this.get("currency") === false) {
- this.set({"currency_info": null});
- return;
- }
- return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
- .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
- self.set({"currency_info": res});
- });
- },
- parse_value: function(val, def) {
- return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
- },
- format_value: function(val, def) {
- return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
- },
-});
-
-/*
- This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
- record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
- will be added to the relation.
-*/
-instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- className: "oe_form_many2many_checkboxes",
- init: function() {
- this._super.apply(this, arguments);
- this.set("value", {});
- this.set("records", []);
- this.field_manager.on("view_content_has_changed", this, function() {
- var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
- if (! _.isEqual(domain, this.get("domain"))) {
- this.set("domain", domain);
- }
- });
- this.records_orderer = new instance.web.DropMisordered();
- },
- initialize_field: function() {
- instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
- this.on("change:domain", this, this.query_records);
- this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
- this.on("change:records", this, this.render_value);
- },
- query_records: function() {
- var self = this;
- var model = new openerp.Model(openerp.session, this.field.relation);
- this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
- return model.call("name_get", [record_ids] , {"context": self.build_context()});
- })).then(function(res) {
- self.set("records", res);
- });
- },
- render_value: function() {
- this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
- var inputs = this.$("input");
- inputs.change(_.bind(this.from_dom, this));
- if (this.get("effective_readonly"))
- inputs.attr("disabled", "true");
- },
- from_dom: function() {
- var new_value = {};
- this.$("input").each(function() {
- var elem = $(this);
- new_value[elem.data("record-id")] = elem.is(':checked') ? true : undefined;
- });
- if (! _.isEqual(new_value, this.get("value")))
- this.internal_set_value(new_value);
- },
- // WARNING: (mostly) duplicated in 4 other M2M widgets
- set_value: function(value_) {
- value_ = value_ || [];
- if (value_.length >= 1 && value_[0] instanceof Array) {
- // value_ is a list of m2m commands. We only process
- // LINK_TO and REPLACE_WITH in this context
- var val = [];
- _.each(value_, function (command) {
- if (command[0] === commands.LINK_TO) {
- val.push(command[1]); // (4, id[, _])
- } else if (command[0] === commands.REPLACE_WITH) {
- val = command[2]; // (6, _, ids)
- }
- });
- value_ = val;
- }
- var formatted = {};
- _.each(value_, function(el) {
- formatted[JSON.stringify(el)] = true;
- });
- this._super(formatted);
- },
- get_value: function() {
- var value = _.filter(_.keys(this.get("value")), function(el) {
- return this.get("value")[el];
- }, this);
- value = _.map(value, function(el) {
- return JSON.parse(el);
- });
- return [commands.replace_with(value)];
- },
-});
-
-/**
- This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
- "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
- action on the model of the relation and show only the linked records.
-
- Widget options:
-
- * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
- to display (or False to take the default one) and the second element is the type of the view. Defaults to
- [[false, "tree"], [false, "form"]] .
-*/
-instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
- className: "oe_form_x2many_counter",
- init: function() {
- this._super.apply(this, arguments);
- this.set("value", []);
- _.defaults(this.options, {
- "views": [[false, "tree"], [false, "form"]],
- });
- },
- render_value: function() {
- var text = _.str.sprintf("%d %s", this.val().length, this.string);
- this.$().html(QWeb.render("X2ManyCounter", {text: text}));
- this.$("a").click(_.bind(this.go_to, this));
- },
- go_to: function() {
- return this.view.recursive_save().then(_.bind(function() {
- var val = this.val();
- var context = {};
- if (this.field.type === "one2many") {
- context["default_" + this.field.relation_field] = this.view.datarecord.id;
- }
- var domain = [["id", "in", val]];
- return this.do_action({
- type: 'ir.actions.act_window',
- name: this.string,
- res_model: this.field.relation,
- views: this.options.views,
- target: 'current',
- context: context,
- domain: domain,
- });
- }, this));
- },
- val: function() {
- var value = this.get("value") || [];
- if (value.length >= 1 && value[0] instanceof Array) {
- value = value[0][2];
- }
- return value;
- }
-});
-
-/**
- This widget is intended to be used on stat button numeric fields. It will display
- the value many2many and one2many. It is a read-only field that will
- display a simple string "<value of field> <label of the field>"
-*/
-instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
- is_field_number: true,
- init: function() {
- this._super.apply(this, arguments);
- this.internal_set_value(0);
- },
- set_value: function(value_) {
- if (value_ === false || value_ === undefined) {
- value_ = 0;
- }
- this._super.apply(this, [value_]);
- },
- render_value: function() {
- var options = {
- value: this.get("value") || 0,
- };
- if (! this.node.attrs.nolabel) {
- if(this.options.label_field && this.view.datarecord[this.options.label_field]) {
- options.text = this.view.datarecord[this.options.label_field];
- }
- else {
- options.text = this.string;
- }
- }
- this.$el.html(QWeb.render("StatInfo", options));
- },
-
-});
-
-/**
- This widget is intended to be used on boolean fields. It toggles a button
- switching between a green bullet / gray bullet.
-*/
-instance.web.form.FieldToggleBoolean = instance.web.form.AbstractField.extend({
- render_value: function () {
- this.icon = this.get_value() ? 'gtk-yes.png' : 'gtk-normal.png';
- this.$el.html(QWeb.render("FieldToggleBoolean", {'widget': this}));
- this.$('.oe_toggle_button').on('click', this.set_toggle_button.bind(this));
- },
- set_toggle_button: function () {
- var self = this;
- var toggle_value = this.get_value() === false ? true: false;
- if (this.view.get('actual_mode') == 'view') {
- var rec_values = {};
- rec_values[self.node.attrs.name] = toggle_value;
- return this.view.dataset._model.call(
- 'write', [
- [this.view.datarecord.id],
- rec_values,
- self.view.dataset.get_context()
- ]).done(function () { self.reload_record(); });
- }
- else {
- this.set_value(toggle_value);
- }
- },
- reload_record: function () {
- this.view.reload();
- },
-});
-
-/**
- * Registry of form fields, called by :js:`instance.web.FormView`.
- *
- * All referenced classes must implement FieldInterface. Those represent the classes whose instances
- * will substitute to the <field> tags as defined in OpenERP's views.
- */
-instance.web.form.widgets = new instance.web.Registry({
- 'char' : 'instance.web.form.FieldChar',
- 'id' : 'instance.web.form.FieldID',
- 'email' : 'instance.web.form.FieldEmail',
- 'url' : 'instance.web.form.FieldUrl',
- 'text' : 'instance.web.form.FieldText',
- 'html' : 'instance.web.form.FieldTextHtml',
- 'char_domain': 'instance.web.form.FieldCharDomain',
- 'date' : 'instance.web.form.FieldDate',
- 'datetime' : 'instance.web.form.FieldDatetime',
- 'selection' : 'instance.web.form.FieldSelection',
- 'radio' : 'instance.web.form.FieldRadio',
- 'many2one' : 'instance.web.form.FieldMany2One',
- 'many2onebutton' : 'instance.web.form.Many2OneButton',
- 'many2many' : 'instance.web.form.FieldMany2Many',
- 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
- 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
- 'one2many' : 'instance.web.form.FieldOne2Many',
- 'one2many_list' : 'instance.web.form.FieldOne2Many',
- 'reference' : 'instance.web.form.FieldReference',
- 'boolean' : 'instance.web.form.FieldBoolean',
- 'toggle_button' : 'instance.web.form.FieldToggleBoolean',
- 'float' : 'instance.web.form.FieldFloat',
- 'percentpie': 'instance.web.form.FieldPercentPie',
- 'barchart': 'instance.web.form.FieldBarChart',
- 'integer': 'instance.web.form.FieldFloat',
- 'float_time': 'instance.web.form.FieldFloat',
- 'progressbar': 'instance.web.form.FieldProgressBar',
- 'image': 'instance.web.form.FieldBinaryImage',
- 'binary': 'instance.web.form.FieldBinaryFile',
- 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
- 'statusbar': 'instance.web.form.FieldStatus',
- 'monetary': 'instance.web.form.FieldMonetary',
- 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
- 'x2many_counter': 'instance.web.form.X2ManyCounter',
- 'priority':'instance.web.form.Priority',
- 'kanban_state_selection':'instance.web.form.KanbanSelection',
- 'statinfo': 'instance.web.form.StatInfo',
-});
-
-/**
- * Registry of widgets usable in the form view that can substitute to any possible
- * tags defined in OpenERP's form views.
- *
- * Every referenced class should extend FormWidget.
- */
-instance.web.form.tags = new instance.web.Registry({
- 'button' : 'instance.web.form.WidgetButton',
-});
-
-instance.web.form.custom_widgets = new instance.web.Registry({
-});
-
-})();
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
deleted file mode 100644
index f5bda4bce11..00000000000
--- a/addons/web/static/src/js/view_list_editable.js
+++ /dev/null
@@ -1,830 +0,0 @@
-/**
- * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
- * @namespace
- */
-(function() {
-
- var instance = openerp;
- openerp.web.list_editable = {};
- var _t = instance.web._t;
-
- // editability status of list rows
- instance.web.ListView.prototype.defaults.editable = null;
-
- // TODO: not sure second @lends on existing item is correct, to check
- instance.web.ListView.include(/** @lends instance.web.ListView# */{
- init: function () {
- var self = this;
- this._super.apply(this, arguments);
-
- this.saving_mutex = new $.Mutex();
-
- this._force_editability = null;
- this._context_editable = false;
- this.editor = this.make_editor();
- // Stores records of {field, cell}, allows for re-rendering fields
- // depending on cell state during and after resize events
- this.fields_for_resize = [];
- instance.web.bus.on('resize', this, this.resize_fields);
-
- $(this.groups).bind({
- 'edit': function (e, id, dataset) {
- self.do_edit(dataset.index, id, dataset);
- },
- 'saved': function () {
- if (self.groups.get_selection().length) {
- return;
- }
- self.configure_pager(self.dataset);
- self.compute_aggregates();
- }
- });
-
- this.records.bind('remove', function () {
- if (self.editor.is_editing()) {
- self.cancel_edition();
- }
- });
-
- this.on('edit:before', this, function (event) {
- if (!self.editable() || self.editor.is_editing()) {
- event.cancel = true;
- }
- });
- this.on('edit:after', this, function () {
- self.$el.add(self.$buttons).addClass('oe_editing');
- self.$('.ui-sortable').sortable('disable');
- });
- this.on('save:after cancel:after', this, function () {
- self.$('.ui-sortable').sortable('enable');
- self.$el.add(self.$buttons).removeClass('oe_editing');
- });
- },
- destroy: function () {
- instance.web.bus.off('resize', this, this.resize_fields);
- this._super();
- },
- do_hide: function () {
- if (this.editor.is_editing()) {
- this.cancel_edition(true);
- }
- this._super();
- },
- sort_by_column: function (e) {
- e.stopPropagation();
- if (!this.editor.is_editing()) {
- this._super.apply(this, arguments);
- }
- },
- /**
- * Handles the activation of a record in editable mode (making a record
- * editable), called *after* the record has become editable.
- *
- * The default behavior is to setup the listview's dataset to match
- * whatever dataset was provided by the editing List
- *
- * @param {Number} index index of the record in the dataset
- * @param {Object} id identifier of the record being edited
- * @param {instance.web.DataSet} dataset dataset in which the record is available
- */
- do_edit: function (index, id, dataset) {
- _.extend(this.dataset, dataset);
- },
- do_delete: function (ids) {
- var nonfalse = _.compact(ids);
- var _super = this._super.bind(this);
- var next = this.editor.is_editing()
- ? this.cancel_edition(true)
- : $.when();
- return next.then(function () {
- return _super(nonfalse);
- });
- },
- editable: function () {
- return !this.grouped
- && !this.options.disable_editable_mode
- && (this.fields_view.arch.attrs.editable
- || this._context_editable
- || this.options.editable);
- },
- /**
- * Replace do_search to handle editability process
- */
- do_search: function(domain, context, group_by) {
- var self=this, _super = self._super, args=arguments;
- var ready = this.editor.is_editing()
- ? this.cancel_edition(true)
- : $.when();
-
- return ready.then(function () {
- self._context_editable = !!context.set_editable;
- return _super.apply(self, args);
- });
- },
- /**
- * Replace do_add_record to handle editability (and adding new record
- * as an editable row at the top or bottom of the list)
- */
- do_add_record: function () {
- var self = this;
- if (this.editable()) {
- this.$el.find('table:first').show();
- this.$el.find('.oe_view_nocontent').remove();
- this.start_edition();
- } else {
- this._super();
- }
- },
- load_list: function (data, grouped) {
- var self = this;
- // tree/@editable takes priority on everything else if present.
- var result = this._super(data, grouped);
-
- // In case current editor was started previously, also has to run
- // when toggling from editable to non-editable in case form widgets
- // have setup global behaviors expecting themselves to exist
- // somehow.
- this.editor.destroy();
- // Editor is not restartable due to formview not being restartable
- this.editor = this.make_editor();
-
- if (this.editable()) {
- this.$el.addClass('oe_list_editable');
- // FIXME: any hook available to ensure this is only done once?
- this.$buttons
- .off('click', '.oe_list_save')
- .on('click', '.oe_list_save', this.proxy('save_edition'))
- .off('click', '.oe_list_discard')
- .on('click', '.oe_list_discard', function (e) {
- e.preventDefault();
- self.cancel_edition();
- });
- var editor_ready = this.editor.prependTo(this.$el)
- .done(this.proxy('setup_events'));
-
- return $.when(result, editor_ready);
- } else {
- this.$el.removeClass('oe_list_editable');
- }
-
- return result;
- },
- /**
- * Builds a new editor object
- *
- * @return {instance.web.list.Editor}
- */
- make_editor: function () {
- return new instance.web.list.Editor(this);
- },
- do_button_action: function (name, id, callback) {
- var self = this, args = arguments;
- this.ensure_saved().done(function (done) {
- if (!id && done.created) {
- id = done.record.get('id');
- }
- self.handle_button(name, id, callback);
- });
- },
- /**
- * Ensures the editable list is saved (saves any pending edition if
- * needed, or tries to)
- *
- * Returns a deferred to the end of the saving.
- *
- * @returns {$.Deferred}
- */
- ensure_saved: function () {
- return this.save_edition();
- },
- /**
- * Builds a record with the provided id (``false`` for a creation),
- * setting all columns with ``false`` value so code which relies on
- * having an actual value behaves correctly
- *
- * @param {*} id
- * @return {instance.web.list.Record}
- */
- make_empty_record: function (id) {
- var attrs = {id: id};
- _(this.columns).chain()
- .filter(function (x) { return x.tag === 'field';})
- .pluck('name')
- .each(function (field) { attrs[field] = false; });
- return new instance.web.list.Record(attrs);
- },
- /**
- * Set up the edition of a record of the list view "inline"
- *
- * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record
- * @param {Object} [options]
- * @param {String} [options.focus_field] field to focus at start of edition
- * @return {jQuery.Deferred}
- */
- start_edition: function (record, options) {
- var self = this;
- var item = false;
- if (record) {
- item = record.attributes;
- this.dataset.select_id(record.get('id'));
- } else {
- record = this.make_empty_record(false);
- this.records.add(record, {
- at: this.prepends_on_create() ? 0 : null});
- }
- return this.ensure_saved().then(function(){
- return $.when.apply(null, self.editor.form.render_value_defs);
- }).then(function () {
- var $recordRow = self.groups.get_row_for(record);
- var cells = self.get_cells_for($recordRow);
- var fields = {};
- self.fields_for_resize.splice(0, self.fields_for_resize.length);
- return self.with_event('edit', {
- record: record.attributes,
- cancel: false
- }, function () {
- return self.editor.edit(item, function (field_name, field) {
- var cell = cells[field_name];
- if (!cell) {
- return;
- }
-
- // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
- field.$el.attr('data-fieldname', field_name);
- fields[field_name] = field;
- self.fields_for_resize.push({field: field, cell: cell});
- }, options).then(function () {
- $recordRow.addClass('oe_edition');
- self.resize_fields();
- var focus_field = options && options.focus_field ? options.focus_field : undefined;
- if (!focus_field || !fields[focus_field]){
- focus_field = _.find(self.editor.form.fields_order, function(field){ return fields[field] && fields[field].$el.is(':visible:has(input)'); });
- }
- if (focus_field && fields[focus_field]) fields[focus_field].$el.find('input').select();
- return record.attributes;
- });
- }).fail(function () {
- // if the start_edition event is cancelled and it was a
- // creation, remove the newly-created empty record
- if (!record.get('id')) {
- self.records.remove(record);
- }
- });
- });
- },
- get_cells_for: function ($row) {
- var cells = {};
- $row.children('td').each(function (index, el) {
- cells[el.getAttribute('data-field')] = el;
- });
- return cells;
- },
- /**
- * If currently editing a row, resizes all registered form fields based
- * on the corresponding row cell
- */
- resize_fields: function () {
- if (!this.editor.is_editing()) { return; }
- for(var i=0, len=this.fields_for_resize.length; i<len; ++i) {
- var item = this.fields_for_resize[i];
- this.resize_field(item.field, item.cell);
- }
- },
- /**
- * Resizes a field's root element based on the corresponding cell of
- * a listview row
- *
- * @param {instance.web.form.AbstractField} field
- * @param {jQuery} cell
- */
- resize_field: function (field, cell) {
- var $cell = $(cell);
-
- field.set_dimensions($cell.outerHeight(), $cell.outerWidth());
- field.$el.position({
- my: 'left top',
- at: 'left top',
- of: $cell
- });
- if (field.get('effective_readonly')) {
- field.$el.addClass('oe_readonly');
- }
- if(field.widget == "handle")
- field.$el.addClass('oe_list_field_handle');
- },
- /**
- * @return {jQuery.Deferred}
- */
- save_edition: function () {
- var self = this;
- return self.saving_mutex.exec(function() {
- if (!self.editor.is_editing()) {
- return $.when();
- }
- return self.with_event('save', {
- editor: self.editor,
- form: self.editor.form,
- cancel: false
- }, function () {
- return self.editor.save().then(function (attrs) {
- var created = false;
- var record = self.records.get(attrs.id);
- if (!record) {
- // new record
- created = true;
- record = self.records.find(function (r) {
- return !r.get('id');
- }).set('id', attrs.id);
- }
- // onwrite callback could be altering & reloading the
- // record which has *just* been saved, so first perform all
- // onwrites then do a final reload of the record
- return self.handle_onwrite(record)
- .then(function () {
- return self.reload_record(record); })
- .then(function () {
- return { created: created, record: record }; });
- });
- });
- });
- },
- /**
- * @param {Boolean} [force=false] discards the data even if the form has been edited
- * @return {jQuery.Deferred}
- */
- cancel_edition: function (force) {
- var self = this;
- return this.with_event('cancel', {
- editor: this.editor,
- form: this.editor.form,
- cancel: false
- }, function () {
- return this.editor.cancel(force).then(function (attrs) {
- if (attrs.id) {
- var record = self.records.get(attrs.id);
- if (!record) {
- // Record removed by third party during edition
- return;
- }
- return self.reload_record(record, {do_not_evict: true});
- }
- var to_delete = self.records.find(function (r) {
- return !r.get('id');
- });
- if (to_delete) {
- self.records.remove(to_delete);
- }
- });
- });
- },
- /**
- * Executes an action on the view's editor bracketed by a cancellable
- * event of the name provided.
- *
- * The event name provided will be post-fixed with ``:before`` and
- * ``:after``, the ``event`` parameter will be passed alongside the
- * ``:before`` variant and if the parameter's ``cancel`` key is set to
- * ``true`` the action *will not be called* and the method will return
- * a rejection
- *
- * @param {String} event_name name of the event
- * @param {Object} event event object, provided to ``:before`` sub-event
- * @param {Function} action callable, called with the view's editor as its context
- * @param {Array} [args] supplementary arguments provided to the action
- * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function
- * @return {jQuery.Deferred}
- */
- with_event: function (event_name, event, action) {
- var self = this;
- event = event || {};
- this.trigger(event_name + ':before', event);
- if (event.cancel) {
- return $.Deferred().reject({
- message: _.str.sprintf("Event %s:before cancelled",
- event_name)});
- }
- return $.when(action.call(this)).done(function () {
- self.trigger.apply(self, [event_name + ':after']
- .concat(_.toArray(arguments)));
- });
- },
- edition_view: function (editor) {
- var view = $.extend(true, {}, this.fields_view);
- view.arch.tag = 'form';
- _.extend(view.arch.attrs, {
- 'class': 'oe_form_container',
- version: '7.0'
- });
- _(view.arch.children).chain()
- .zip(_(this.columns).filter(function (c) {
- return !(c instanceof instance.web.list.MetaColumn);}))
- .each(function (ar) {
- var widget = ar[0], column = ar[1];
- var modifiers = _.extend({}, column.modifiers);
- widget.attrs.nolabel = true;
- if (modifiers['tree_invisible'] || widget.tag === 'button') {
- modifiers.invisible = true;
- }
- widget.attrs.modifiers = JSON.stringify(modifiers);
- });
- return view;
- },
- handle_onwrite: function (source_record) {
- var self = this;
- var on_write_callback = self.fields_view.arch.attrs.on_write;
- if (!on_write_callback) { return $.when(); }
- return this.dataset.call(on_write_callback, [source_record.get('id')])
- .then(function (ids) {
- return $.when.apply(
- null, _(ids).map(
- _.bind(self.handle_onwrite_record, self, source_record)));
- });
- },
- handle_onwrite_record: function (source_record, id) {
- var record = this.records.get(id);
- if (!record) {
- // insert after the source record
- var index = this.records.indexOf(source_record) + 1;
- record = this.make_empty_record(id);
- this.records.add(record, {at: index});
- }
- return this.reload_record(record);
- },
- prepends_on_create: function () {
- return this.editable() === 'top';
- },
- setup_events: function () {
- var self = this;
- _.each(this.editor.form.fields, function(field, field_name) {
- field.on("change:effective_readonly", self, function(){
- var item = _(self.fields_for_resize).find(function (item) {
- return item.field === field;
- });
- if (item) {
- setTimeout(function() {
- self.resize_field(item.field, item.cell);
- }, 0);
- }
-
- });
- });
-
- this.editor.$el.on('keyup keypress keydown', function (e) {
- if (!self.editor.is_editing()) { return true; }
- var key = _($.ui.keyCode).chain()
- .map(function (v, k) { return {name: k, code: v}; })
- .find(function (o) { return o.code === e.which; })
- .value();
- if (!key) { return true; }
- var method = e.type + '_' + key.name;
- if (!(method in self)) { return true; }
- return self[method](e);
- });
- },
- /**
- * Saves the current record, and goes to the next one (creation or
- * edition)
- *
- * @private
- * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit
- * @param {Object} [options]
- * @param {String} [options.focus_field]
- * @return {*}
- */
- _next: function (next_record, options) {
- next_record = next_record || 'succ';
- var self = this;
- return this.save_edition().then(function (saveInfo) {
- if (!saveInfo) { return null; }
- if (saveInfo.created) {
- return self.start_edition();
- }
- var record = self.records[next_record](
- saveInfo.record, {wraparound: true});
- return self.start_edition(record, options);
- });
- },
- keypress_ENTER: function () {
- return this._next();
- },
- keydown_ESCAPE: function (e) {
- return false;
- },
- keyup_ESCAPE: function (e) {
- return this.cancel_edition();
- },
- /**
- * Gets the selection range (start, end) for the provided element,
- * returns ``null`` if it can't get a range.
- *
- * @private
- */
- _text_selection_range: function (el) {
- var selectionStart;
- try {
- selectionStart = el.selectionStart;
- } catch (e) {
- // radio or checkbox throw on selectionStart access
- return null;
- }
- if (selectionStart !== undefined) {
- return {
- start: selectionStart,
- end: el.selectionEnd
- };
- } else if (document.body.createTextRange) {
- throw new Error("Implement text range handling for MSIE");
- }
- // Element without selection ranges (select, div/@contenteditable)
- return null;
- },
- _text_cursor: function (el) {
- var selection = this._text_selection_range(el);
- if (!selection) {
- return null;
- }
- if (selection.start !== selection.end) {
- return {position: null, collapsed: false};
- }
- return {position: selection.start, collapsed: true};
- },
- /**
- * Checks if the cursor is at the start of the provided el
- *
- * @param {HTMLInputElement | HTMLTextAreaElement}
- * @returns {Boolean}
- * @private
- */
- _at_start: function (cursor, el) {
- return cursor.collapsed && (cursor.position === 0);
- },
- /**
- * Checks if the cursor is at the end of the provided el
- *
- * @param {HTMLInputElement | HTMLTextAreaElement}
- * @returns {Boolean}
- * @private
- */
- _at_end: function (cursor, el) {
- return cursor.collapsed && (cursor.position === el.value.length);
- },
- /**
- * @param DOMEvent event
- * @param {String} record_direction direction to move into to get the next record (pred | succ)
- * @param {Function} is_valid_move whether the edition should be moved to the next record
- * @private
- */
- _key_move_record: function (event, record_direction, is_valid_move) {
- if (!this.editor.is_editing('edit')) { return $.when(); }
- var cursor = this._text_cursor(event.target);
- // if text-based input (has a cursor)
- // and selecting (not collapsed) or not at a field boundary
- // don't move to the next record
- if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); }
-
- event.preventDefault();
- var source_field = $(event.target).closest('[data-fieldname]')
- .attr('data-fieldname');
- return this._next(record_direction, {focus_field: source_field});
-
- },
- keydown_UP: function (e) {
- var self = this;
- return this._key_move_record(e, 'pred', function (el, cursor) {
- return self._at_start(cursor, el);
- });
- },
- keydown_DOWN: function (e) {
- var self = this;
- return this._key_move_record(e, 'succ', function (el, cursor) {
- return self._at_end(cursor, el);
- });
- },
-
- keydown_LEFT: function (e) {
- // If the cursor is at the beginning of the field
- var source_field = $(e.target).closest('[data-fieldname]')
- .attr('data-fieldname');
- var cursor = this._text_cursor(e.target);
- if (cursor && !this._at_start(cursor, e.target)) { return $.when(); }
-
- var fields_order = this.editor.form.fields_order;
- var field_index = _(fields_order).indexOf(source_field);
-
- // Look for the closest visible form field to the left
- var fields = this.editor.form.fields;
- var field;
- do {
- if (--field_index < 0) { return $.when(); }
-
- field = fields[fields_order[field_index]];
- } while (!field.$el.is(':visible'));
-
- // and focus it
- field.focus();
- return $.when();
- },
- keydown_RIGHT: function (e) {
- // same as above, but with cursor at the end of the field and
- // looking for new fields at the right
- var source_field = $(e.target).closest('[data-fieldname]')
- .attr('data-fieldname');
- var cursor = this._text_cursor(e.target);
- if (cursor && !this._at_end(cursor, e.target)) { return $.when(); }
-
- var fields_order = this.editor.form.fields_order;
- var field_index = _(fields_order).indexOf(source_field);
-
- var fields = this.editor.form.fields;
- var field;
- do {
- if (++field_index >= fields_order.length) { return $.when(); }
-
- field = fields[fields_order[field_index]];
- } while (!field.$el.is(':visible'));
-
- field.focus();
- return $.when();
- },
- keydown_TAB: function (e) {
- var form = this.editor.form;
- var last_field = _(form.fields_order).chain()
- .map(function (name) { return form.fields[name]; })
- .filter(function (field) { return field.$el.is(':visible') && !field.get('effective_readonly'); })
- .last()
- .value();
- // tabbed from last field in form
- if (last_field && last_field.$el.has(e.target).length) {
- e.preventDefault();
- return this._next();
- }
- return $.when();
- }
- });
-
- instance.web.list.Editor = instance.web.Widget.extend({
- /**
- * @constructs instance.web.list.Editor
- * @extends instance.web.Widget
- *
- * Adapter between listview and formview for editable-listview purposes
- *
- * @param {instance.web.Widget} parent
- * @param {Object} options
- * @param {instance.web.FormView} [options.formView=instance.web.FormView]
- * @param {Object} [options.delegate]
- */
- init: function (parent, options) {
- this._super(parent);
- this.options = options || {};
- _.defaults(this.options, {
- formView: instance.web.FormView,
- delegate: this.getParent()
- });
- this.delegate = this.options.delegate;
-
- this.record = null;
-
- this.form = new (this.options.formView)(
- this, this.delegate.dataset, false, {
- initial_mode: 'edit',
- disable_autofocus: true,
- $buttons: $(),
- $pager: $()
- });
- },
- start: function () {
- var self = this;
- var _super = this._super();
- this.form.embedded_view = this._validate_view(
- this.delegate.edition_view(this));
- var form_ready = this.form.appendTo(this.$el).done(
- self.form.proxy('do_hide'));
- return $.when(_super, form_ready);
- },
- _validate_view: function (edition_view) {
- if (!edition_view) {
- throw new Error("editor delegate's #edition_view must return "
- + "a view descriptor");
- }
- var arch = edition_view.arch;
- if (!(arch && arch.children instanceof Array)) {
- throw new Error("Editor delegate's #edition_view must have a" +
- " non-empty arch");
- }
- if (arch.tag !== "form") {
- throw new Error("Editor delegate's #edition_view must have a" +
- " 'form' root node");
- }
- if (!(arch.attrs && arch.attrs.version === "7.0")) {
- throw new Error("Editor delegate's #edition_view must be a" +
- " version 7 view");
- }
- if (!/\boe_form_container\b/.test(arch.attrs['class'])) {
- throw new Error("Editor delegate's #edition_view must have the" +
- " class 'oe_form_container' on its root" +
- " element");
- }
-
- return edition_view;
- },
-
- /**
- *
- * @param {String} [state] either ``new`` or ``edit``
- * @return {Boolean}
- */
- is_editing: function (state) {
- if (!this.record) {
- return false;
- }
- switch(state) {
- case null: case undefined:
- return true;
- case 'new': return !this.record.id;
- case 'edit': return !!this.record.id;
- }
- throw new Error("is_editing's state filter must be either `new` or" +
- " `edit` if provided");
- },
- edit: function (record, configureField, options) {
- // TODO: specify sequence of edit calls
- var self = this;
- var form = self.form;
- var loaded = record
- ? form.trigger('load_record', _.extend({}, record))
- : form.load_defaults();
- return $.when(loaded).then(function () {
- return form.do_show({reload: false});
- }).then(function () {
- self.record = form.datarecord;
- _(form.fields).each(function (field, name) {
- configureField(name, field);
- });
- return form;
- });
- },
- save: function () {
- var self = this;
- return this.form
- .save(this.delegate.prepends_on_create())
- .then(function (result) {
- var created = result.created && !self.record.id;
- if (created) {
- self.record.id = result.result;
- }
- return self.cancel();
- });
- },
- cancel: function (force) {
- if (!(force || this.form.can_be_discarded())) {
- return $.Deferred().reject({
- message: _t("The form's data can not be discarded")}).promise();
- }
- var record = this.record;
- this.record = null;
- this.form.do_hide();
- return $.when(record);
- },
- });
-
- instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
- passthrough_events: instance.web.ListView.Groups.prototype.passthrough_events + " edit saved",
- get_row_for: function (record) {
- return _(this.children).chain()
- .invoke('get_row_for', record)
- .compact()
- .first()
- .value();
- }
- });
-
- instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{
- row_clicked: function (event) {
- if (!this.view.editable() || ! this.view.is_action_enabled('edit')) {
- return this._super.apply(this, arguments);
- }
- var record_id = $(event.currentTarget).data('id');
- return this.view.start_edition(
- record_id ? this.records.get(record_id) : null, {
- focus_field: $(event.target).not(".oe_readonly").data('field')
- });
- },
- /**
- * If a row mapping to the record (@data-id matching the record's id or
- * no @data-id if the record has no id), returns it. Otherwise returns
- * ``null``.
- *
- * @param {Record} record the record to get a row for
- * @return {jQuery|null}
- */
- get_row_for: function (record) {
- var id;
- var $row = this.$current.children('[data-id=' + record.get('id') + ']');
- if ($row.length) {
- return $row;
- }
- return null;
- }
- });
-})();
diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js
new file mode 100644
index 00000000000..dfef449fe9e
--- /dev/null
+++ b/addons/web/static/src/js/view_manager.js
@@ -0,0 +1,514 @@
+odoo.define('web.ViewManager', ['web.core', 'web.data', 'web.Dialog', 'web.formats', 'web.framework', 'web.Model', 'web.pyeval', 'web.utils', 'web.SearchView', 'web.session', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var formats = require('web.formats');
+var framework = require('web.framework');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var utils = require('web.utils');
+var SearchView = require('web.SearchView');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+var ViewManager = Widget.extend({
+ template: "ViewManager",
+ /**
+ * @param {Object} [dataset] null object (... historical reasons)
+ * @param {Array} [views] List of [view_id, view_type]
+ * @param {Object} [flags] various boolean describing UI state
+ */
+ init: function(parent, dataset, views, flags, action) {
+ if (action) {
+ flags = action.flags || {};
+ if (!('auto_search' in flags)) {
+ flags.auto_search = action.auto_search !== false;
+ }
+ if (action.res_model === 'board.board' && action.view_mode === 'form') {
+ action.target = 'inline';
+ // Special case for Dashboards
+ _.extend(flags, {
+ views_switcher : false,
+ display_title : false,
+ search_view : false,
+ pager : false,
+ sidebar : false,
+ action_buttons : false
+ });
+ }
+ this.action = action;
+ this.action_manager = parent;
+ this.debug = session.debug;
+ dataset = new data.DataSetSearch(this, action.res_model, action.context, action.domain);
+ if (action.res_id) {
+ dataset.ids.push(action.res_id);
+ dataset.index = 0;
+ }
+ views = action.views;
+ }
+ var self = this;
+ this._super(parent);
+
+ this.flags = flags || {};
+ this.dataset = dataset;
+ this.view_order = [];
+ this.url_states = {};
+ this.views = {};
+ this.view_stack = []; // used for breadcrumbs
+ this.active_view = null;
+ this.searchview = null;
+ this.active_search = null;
+ this.registry = core.view_registry;
+ this.title = this.action && this.action.name;
+
+ _.each(views, function (view) {
+ var view_type = view[1] || view.view_type,
+ View = core.view_registry.get(view_type, true),
+ view_label = View ? View.prototype.display_name: (void 'nope'),
+ view_descr = {
+ controller: null,
+ options: view.options || {},
+ view_id: view[0] || view.view_id,
+ type: view_type,
+ label: view_label,
+ embedded_view: view.embedded_view,
+ title: self.action && self.action.name,
+ button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'),
+ };
+ self.view_order.push(view_descr);
+ self.views[view_type] = view_descr;
+ });
+ this.multiple_views = (self.view_order.length > 1);
+ },
+ /**
+ * @returns {jQuery.Deferred} initial view loading promise
+ */
+ start: function() {
+ var self = this;
+ var default_view = this.flags.default_view || this.view_order[0].type,
+ default_options = this.flags[default_view] && this.flags[default_view].options;
+
+ if (this.flags.headless) {
+ this.$('.oe-view-manager-header').hide();
+ }
+ this._super();
+ var $sidebar = this.flags.sidebar ? this.$('.oe-view-manager-sidebar') : undefined,
+ $pager = this.$('.oe-view-manager-pager');
+
+ this.$breadcrumbs = this.$('.oe-view-title');
+ this.$switch_buttons = this.$('.oe-view-manager-switch button');
+ this.$header = this.$('.oe-view-manager-header');
+ this.$header_col = this.$header.find('.oe-header-title');
+ this.$search_col = this.$header.find('.oe-view-manager-search-view');
+ this.$switch_buttons.click(function (event) {
+ var view_type = $(this).data('view-type');
+ if ((view_type === 'form') && (self.active_view.type === 'form')) {
+ self._display_view(view_type);
+ } else {
+ self.switch_mode(view_type);
+ }
+ });
+ var views_ids = {};
+ _.each(this.views, function (view) {
+ views_ids[view.type] = view.view_id;
+ view.options = _.extend({
+ $buttons: self.$('.oe-' + view.type + '-buttons'),
+ $sidebar : $sidebar,
+ $pager : $pager,
+ action : self.action,
+ action_views_ids : views_ids,
+ }, self.flags, self.flags[view.type], view.options);
+ view.$container = self.$(".oe-view-manager-view-" + view.type);
+
+ // show options.$buttons as views will put their $buttons inside it
+ // and call show/hide on them
+ view.options.$buttons.show();
+ self.$('.oe-vm-switch-' + view.type).tooltip();
+ });
+ this.$('.oe_debug_view').click(this.on_debug_changed);
+ this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current'));
+
+ this.search_view_loaded = this.setup_search_view();
+ var main_view_loaded = this.switch_mode(default_view, null, default_options);
+
+ return $.when(main_view_loaded, this.search_view_loaded);
+ },
+
+ switch_mode: function(view_type, no_store, view_options) {
+ var self = this,
+ view = this.views[view_type];
+
+ if (!view) {
+ return $.Deferred().reject();
+ }
+ if ((view_type !== 'form') && (view_type !== 'diagram')) {
+ this.view_stack = [];
+ }
+ this.view_stack.push(view);
+
+ // Hide active view (at first rendering, there is no view to hide)
+ if (this.active_view && this.active_view !== view) {
+ if (this.active_view.controller) this.active_view.controller.do_hide();
+ if (this.active_view.$container) this.active_view.$container.hide();
+ }
+ this.active_view = view;
+
+ if (!view.created) {
+ view.created = this.create_view.bind(this)(view, view_options);
+ }
+ this.active_search = $.Deferred();
+
+ if (this.searchview &&
+ this.flags.auto_search &&
+ view.controller.searchable !== false) {
+ $.when(this.search_view_loaded, view.created).done(this.searchview.do_search);
+ } else {
+ this.active_search.resolve();
+ }
+
+ self.update_header();
+ return $.when(view.created, this.active_search).done(function () {
+ self.active_view = view;
+ self._display_view(view_options);
+ self.trigger('switch_mode', view_type, no_store, view_options);
+ if (self.debug) {
+ self.$('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
+ view: self.active_view.controller,
+ view_manager: self,
+ uid: session.uid,
+ }));
+ }
+ });
+ },
+ update_header: function () {
+ this.$switch_buttons.removeClass('active');
+ this.$('.oe-vm-switch-' + this.active_view.type).addClass('active');
+ },
+ _display_view: function (view_options) {
+ var self = this;
+ this.active_view.$container.show();
+ $.when(this.active_view.controller.do_show(view_options)).done(function () {
+ if (self.searchview) {
+ var is_hidden = self.active_view.controller.searchable === false;
+ self.searchview.toggle_visibility(!is_hidden);
+ self.$header_col.toggleClass('col-md-6', !is_hidden).toggleClass('col-md-12', is_hidden);
+ self.$search_col.toggle(!is_hidden);
+ }
+ self.display_breadcrumbs();
+ });
+ },
+ display_breadcrumbs: function () {
+ var self = this;
+ if (!this.action_manager) return;
+ var breadcrumbs = this.action_manager.get_breadcrumbs();
+ if (!breadcrumbs.length) return;
+
+ var $breadcrumbs = breadcrumbs.map(function (bc, index) {
+ return make_breadcrumb(bc, index === breadcrumbs.length - 1);
+ });
+
+ this.$breadcrumbs
+ .empty()
+ .append($breadcrumbs);
+
+ function make_breadcrumb (bc, is_last) {
+ var $bc = $('<li>')
+ .append(is_last ? bc.title : $('<a>').text(bc.title))
+ .toggleClass('active', is_last);
+ if (!is_last) {
+ $bc.click(function () {
+ self.action_manager.select_widget(bc.widget, bc.index);
+ });
+ }
+ return $bc;
+ }
+ },
+ create_view: function(view, view_options) {
+ var self = this,
+ View = this.registry.get(view.type),
+ options = _.clone(view.options),
+ view_loaded = $.Deferred();
+
+ if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline'))
+ || (view_options && view_options.mode === 'edit'))) {
+ options.initial_mode = 'edit';
+ }
+ var controller = new View(this, this.dataset, view.view_id, options),
+ $container = view.$container;
+
+ $container.hide();
+ view.controller = controller;
+ view.$container = $container;
+
+ if (view.embedded_view) {
+ controller.set_embedded_view(view.embedded_view);
+ }
+ controller.on('switch_mode', this, this.switch_mode.bind(this));
+ controller.on('history_back', this, function () {
+ if (self.action_manager) self.action_manager.trigger('history_back');
+ });
+ controller.on("change:title", this, function() {
+ self.display_breadcrumbs();
+ });
+ controller.on('view_loaded', this, function () {
+ view_loaded.resolve();
+ });
+ this.$('.oe-view-manager-pager > span').hide();
+ return $.when(controller.appendTo($container), view_loaded)
+ .done(function () {
+ self.trigger("controller_inited", view.type, controller);
+ });
+ },
+ select_view: function (index) {
+ var view_type = this.view_stack[index].type;
+ this.view_stack.splice(index);
+ return this.switch_mode(view_type);
+ },
+ /**
+ * @returns {Number|Boolean} the view id of the given type, false if not found
+ */
+ get_view_id: function(view_type) {
+ return this.views[view_type] && this.views[view_type].view_id || false;
+ },
+ /**
+ * Sets up the current viewmanager's search view.
+ *
+ * @param {Number|false} view_id the view to use or false for a default one
+ * @returns {jQuery.Deferred} search view startup deferred
+ */
+ setup_search_view: function() {
+ if (this.searchview) {
+ this.searchview.destroy();
+ }
+
+ var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
+
+ var search_defaults = {};
+
+ var context = this.action ? this.action.context : [];
+ _.each(context, function (value, key) {
+ var match = /^search_default_(.*)$/.exec(key);
+ if (match) {
+ search_defaults[match[1]] = value;
+ }
+ });
+
+
+ var options = {
+ hidden: this.flags.search_view === false,
+ disable_custom_filters: this.flags.search_disable_custom_filters,
+ $buttons: this.$('.oe-search-options'),
+ action: this.action,
+ };
+ this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
+
+ this.searchview.on('search_data', this, this.search.bind(this));
+ return this.searchview.appendTo(this.$(".oe-view-manager-search-view:first"));
+ },
+ search: function(domains, contexts, groupbys) {
+ var self = this,
+ controller = this.active_view.controller,
+ action_context = this.action.context || {},
+ view_context = controller.get_context();
+ pyeval.eval_domains_and_contexts({
+ domains: [this.action.domain || []].concat(domains || []),
+ contexts: [action_context, view_context].concat(contexts || []),
+ group_by_seq: groupbys || []
+ }).done(function (results) {
+ if (results.error) {
+ self.active_search.resolve();
+ throw new Error(
+ _.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
+ JSON.stringify(results.error)));
+ }
+ self.dataset._model = new Model(
+ self.dataset.model, results.context, results.domain);
+ var groupby = results.group_by.length
+ ? results.group_by
+ : action_context.group_by;
+ if (_.isString(groupby)) {
+ groupby = [groupby];
+ }
+ if (!controller.grouped && !_.isEmpty(groupby)){
+ self.dataset.set_sort([]);
+ }
+ $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
+ self.active_search.resolve();
+ });
+ });
+ },
+ do_push_state: function(state) {
+ if (this.action_manager) {
+ state.view_type = this.active_view.type;
+ this.action_manager.do_push_state(state);
+ }
+ },
+ do_load_state: function(state, warm) {
+ if (state.view_type && state.view_type !== this.active_view.type) {
+ // warning: this code relies on the fact that switch_mode has an immediate side
+ // effect (setting the 'active_view' to its new value) AND an async effect (the
+ // view is created/loaded). So, the next statement (do_load_state) is executed
+ // on the new view, after it was initialized, but before it is fully loaded and
+ // in particular, before the do_show method is called.
+ this.switch_mode(state.view_type, true);
+ }
+ this.active_view.controller.do_load_state(state, warm);
+ },
+ on_debug_changed: function (evt) {
+ var self = this,
+ params = $(evt.target).data(),
+ val = params.action,
+ current_view = this.active_view.controller;
+ switch (val) {
+ case 'fvg':
+ var dialog = new Dialog(this, { title: _t("Fields View Get") }).open();
+ $('<pre>').text(utils.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el);
+ break;
+ case 'tests':
+ this.do_action({
+ name: _t("JS Tests"),
+ target: 'new',
+ type : 'ir.actions.act_url',
+ url: '/web/tests?mod=*'
+ });
+ break;
+ case 'get_metadata':
+ var ids = current_view.get_selected_ids();
+ if (ids.length === 1) {
+ this.dataset.call('get_metadata', [ids]).done(function(result) {
+ var dialog = new Dialog(this, {
+ title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model),
+ size: 'medium',
+ buttons: {
+ Ok: function() { this.parents('.modal').modal('hide');}
+ },
+ }, QWeb.render('ViewManagerDebugViewLog', {
+ perm : result[0],
+ format : formats.format_value
+ }));
+ dialog.open();
+ });
+ }
+ break;
+ case 'toggle_layout_outline':
+ current_view.rendering_engine.toggle_layout_debugging();
+ break;
+ case 'set_defaults':
+ current_view.open_defaults_dialog();
+ break;
+ case 'translate':
+ this.do_action({
+ name: _t("Technical Translation"),
+ res_model : 'ir.translation',
+ domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
+ views: [[false, 'list'], [false, 'form']],
+ type : 'ir.actions.act_window',
+ view_type : "list",
+ view_mode : "list"
+ });
+ break;
+ case 'fields':
+ this.dataset.call('fields_get', [false, {}]).done(function (fields) {
+ var $root = $('<dl>');
+ _(fields).each(function (attributes, name) {
+ $root.append($('<dt>').append($('<h4>').text(name)));
+ var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root));
+ _(attributes).each(function (def, name) {
+ if (def instanceof Object) {
+ def = JSON.stringify(def);
+ }
+ $attrs
+ .append($('<dt>').text(name))
+ .append($('<dd style="white-space: pre-wrap;">').text(def));
+ });
+ });
+ new Dialog(self, {
+ title: _.str.sprintf(_t("Model %s fields"),
+ self.dataset.model),
+ buttons: {
+ Ok: function() { this.parents('.modal').modal('hide');}
+ },
+ }, $root).open();
+ });
+ break;
+ case 'edit_workflow':
+ return this.do_action({
+ res_model : 'workflow',
+ name: _t('Edit Workflow'),
+ domain : [['osv', '=', this.dataset.model]],
+ views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
+ type : 'ir.actions.act_window',
+ view_type : 'list',
+ view_mode : 'list'
+ });
+ case 'edit':
+ this.do_edit_resource(params.model, params.id, evt.target.text);
+ break;
+ case 'manage_filters':
+ this.do_action({
+ res_model: 'ir.filters',
+ name: _t('Manage Filters'),
+ views: [[false, 'list'], [false, 'form']],
+ type: 'ir.actions.act_window',
+ context: {
+ search_default_my_filters: true,
+ search_default_model_id: this.dataset.model
+ }
+ });
+ break;
+ case 'print_workflow':
+ if (current_view.get_selected_ids && current_view.get_selected_ids().length == 1) {
+ framework.blockUI();
+ var action = {
+ context: { active_ids: current_view.get_selected_ids() },
+ report_name: "workflow.instance.graph",
+ datas: {
+ model: this.dataset.model,
+ id: current_view.get_selected_ids()[0],
+ nested: true,
+ }
+ };
+ this.session.get_file({
+ url: '/web/report',
+ data: {action: JSON.stringify(action)},
+ complete: framework.unblockUI
+ });
+ } else {
+ self.do_warn("Warning", "No record selected.");
+ }
+ break;
+ case 'leave_debug':
+ window.location.search="?";
+ break;
+ default:
+ if (val) {
+ console.warn("No debug handler for ", val);
+ }
+ }
+ },
+ do_edit_resource: function(model, id, name) {
+ this.do_action({
+ res_model : model,
+ res_id : id,
+ name: name,
+ type : 'ir.actions.act_window',
+ view_type : 'form',
+ view_mode : 'form',
+ views : [[false, 'form']],
+ target : 'new',
+ flags : {
+ action_buttons : true,
+ headless: true,
+ }
+ });
+ },
+});
+
+return ViewManager;
+
+});
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
deleted file mode 100644
index e58753e1efa..00000000000
--- a/addons/web/static/src/js/views.js
+++ /dev/null
@@ -1,1593 +0,0 @@
-/*---------------------------------------------------------
- * OpenERP web library
- *---------------------------------------------------------*/
-
-(function() {
-"use strict";
-
-var instance = openerp;
-openerp.web.views = {};
-var QWeb = instance.web.qweb,
- _t = instance.web._t;
-
-instance.web.ActionManager = instance.web.Widget.extend({
- template: "ActionManager",
- init: function(parent) {
- this._super(parent);
- this.inner_action = null;
- this.inner_widget = null;
- this.webclient = parent;
- this.dialog = null;
- this.dialog_widget = null;
- this.widgets = [];
- this.on('history_back', this, this.proxy('history_back'));
- },
- dialog_stop: function (reason) {
- if (this.dialog) {
- this.dialog.destroy(reason);
- }
- this.dialog = null;
- },
- /**
- * Add a new widget to the action manager
- *
- * widget: typically, widgets added are instance.web.ViewManager. The action manager
- * uses this list of widget to handle the breadcrumbs.
- * action: new action
- * options.on_reverse_breadcrumb: will be called when breadcrumb is selected
- * options.clear_breadcrumbs: boolean, if true, current widgets are destroyed
- * options.replace_breadcrumb: boolean, if true, replace current breadcrumb
- */
- push_widget: function(widget, action, options) {
- var self = this,
- to_destroy,
- old_widget = this.inner_widget;
- options = options || {};
-
- if (options.clear_breadcrumbs) {
- to_destroy = this.widgets;
- this.widgets = [];
- } else if (options.replace_breadcrumb) {
- to_destroy = _.last(this.widgets);
- this.widgets = _.initial(this.widgets);
- }
- if (widget instanceof instance.web.Widget) {
- var title = widget.get('title') || action.display_name || action.name;
- widget.set('title', title);
- this.widgets.push(widget);
- } else {
- this.widgets.push({
- view_stack: [{
- controller: {get: function () {return action.display_name || action.name; }},
- }],
- destroy: function () {},
- });
- }
- _.last(this.widgets).__on_reverse_breadcrumb = options.on_reverse_breadcrumb;
- this.inner_action = action;
- this.inner_widget = widget;
- return $.when(this.inner_widget.appendTo(this.$el)).done(function () {
- if ((action.target !== 'inline') && (!action.flags.headless) && widget.$header) {
- widget.$header.show();
- }
- if (old_widget) {
- old_widget.$el.hide();
- }
- if (options.clear_breadcrumbs) {
- self.clear_widgets(to_destroy);
- }
- });
- },
- get_breadcrumbs: function () {
- return _.flatten(_.map(this.widgets, function (widget) {
- if (widget instanceof instance.web.ViewManager) {
- return widget.view_stack.map(function (view, index) {
- return {
- title: view.controller.get('title') || widget.title,
- index: index,
- widget: widget,
- };
- });
- } else {
- return {title: widget.get('title'), widget: widget };
- }
- }), true);
- },
- get_title: function () {
- if (this.widgets.length === 1) {
- // horrible hack to display the action title instead of "New" for the actions
- // that use a form view to edit something that do not correspond to a real model
- // for example, point of sale "Your Session" or most settings form,
- var widget = this.widgets[0];
- if (widget instanceof instance.web.ViewManager && widget.view_stack.length === 1) {
- return widget.title;
- }
- }
- return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
- },
- get_widgets: function () {
- return this.widgets.slice(0);
- },
- history_back: function() {
- var widget = _.last(this.widgets);
- if (widget instanceof instance.web.ViewManager) {
- var nbr_views = widget.view_stack.length;
- if (nbr_views > 1) {
- return this.select_widget(widget, nbr_views - 2);
- }
- }
- if (this.widgets.length > 1) {
- widget = this.widgets[this.widgets.length - 2];
- var index = widget.view_stack && widget.view_stack.length - 1;
- return this.select_widget(widget, index);
- }
- return $.Deferred().reject();
- },
- select_widget: function(widget, index) {
- var self = this;
- if (this.webclient.has_uncommitted_changes()) {
- return $.Deferred().reject();
- }
- var widget_index = this.widgets.indexOf(widget),
- def = $.when(widget.select_view && widget.select_view(index));
-
- return def.done(function () {
- if (widget.__on_reverse_breadcrumb) {
- widget.__on_reverse_breadcrumb();
- }
- _.each(self.widgets.splice(widget_index + 1), function (w) {
- w.destroy();
- });
- self.inner_widget = _.last(self.widgets);
- if (self.inner_widget.display_breadcrumbs) {
- self.inner_widget.display_breadcrumbs();
- }
- if (self.inner_widget.do_show) {
- self.inner_widget.do_show();
- }
- });
- },
- clear_widgets: function(widgets) {
- _.invoke(widgets || this.widgets, 'destroy');
- if (!widgets) {
- this.widgets = [];
- this.inner_widget = null;
- }
- },
- do_push_state: function(state) {
- if (!this.webclient || !this.webclient.do_push_state || this.dialog) {
- return;
- }
- state = state || {};
- if (this.inner_action) {
- if (this.inner_action._push_me === false) {
- // this action has been explicitly marked as not pushable
- return;
- }
- state.title = this.get_title();
- if(this.inner_action.type == 'ir.actions.act_window') {
- state.model = this.inner_action.res_model;
- }
- if (this.inner_action.menu_id) {
- state.menu_id = this.inner_action.menu_id;
- }
- if (this.inner_action.id) {
- state.action = this.inner_action.id;
- } else if (this.inner_action.type == 'ir.actions.client') {
- state.action = this.inner_action.tag;
- var params = {};
- _.each(this.inner_action.params, function(v, k) {
- if(_.isString(v) || _.isNumber(v)) {
- params[k] = v;
- }
- });
- state = _.extend(params || {}, state);
- }
- if (this.inner_action.context) {
- var active_id = this.inner_action.context.active_id;
- if (active_id) {
- state.active_id = active_id;
- }
- var active_ids = this.inner_action.context.active_ids;
- if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
- // We don't push active_ids if it's a single element array containing the active_id
- // This makes the url shorter in most cases.
- state.active_ids = this.inner_action.context.active_ids.join(',');
- }
- }
- }
- this.webclient.do_push_state(state);
- },
- do_load_state: function(state, warm) {
- var self = this,
- action_loaded;
- if (state.action) {
- if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) {
- var action_client = {
- type: "ir.actions.client",
- tag: state.action,
- params: state,
- _push_me: state._push_me,
- };
- if (warm) {
- this.null_action();
- }
- action_loaded = this.do_action(action_client);
- } else {
- var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
- if (run_action) {
- var add_context = {};
- if (state.active_id) {
- add_context.active_id = state.active_id;
- }
- if (state.active_ids) {
- // The jQuery BBQ plugin does some parsing on values that are valid integers.
- // It means that if there's only one item, it will do parseInt() on it,
- // otherwise it will keep the comma seperated list as string.
- add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
- return parseInt(id, 10) || id;
- });
- } else if (state.active_id) {
- add_context.active_ids = [state.active_id];
- }
- add_context.params = state;
- if (warm) {
- this.null_action();
- }
- action_loaded = this.do_action(state.action, { additional_context: add_context });
- $.when(action_loaded || null).done(function() {
- instance.webclient.menu.is_bound.done(function() {
- if (self.inner_action && self.inner_action.id) {
- instance.webclient.menu.open_action(self.inner_action.id);
- }
- });
- });
- }
- }
- } else if (state.model && state.id) {
- // TODO handle context & domain ?
- if (warm) {
- this.null_action();
- }
- var action = {
- res_model: state.model,
- res_id: state.id,
- type: 'ir.actions.act_window',
- views: [[false, 'form']]
- };
- action_loaded = this.do_action(action);
- } else if (state.sa) {
- // load session action
- if (warm) {
- this.null_action();
- }
- action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).then(function(action) {
- if (action) {
- return self.do_action(action);
- }
- });
- }
-
- $.when(action_loaded || null).done(function() {
- if (self.inner_widget && self.inner_widget.do_load_state) {
- self.inner_widget.do_load_state(state, warm);
- }
- });
- },
- /**
- * Execute an OpenERP action
- *
- * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
- * @param {Object} [options]
- * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
- * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action
- * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
- * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
- * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
- * @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
- * @param {Object} [options.additional_context] Additional context to be merged with the action's context.
- * @return {jQuery.Deferred} Action loaded
- */
- do_action: function(action, options) {
- options = _.defaults(options || {}, {
- clear_breadcrumbs: false,
- on_reverse_breadcrumb: function() {},
- hide_breadcrumb: false,
- on_close: function() {},
- action_menu_id: null,
- additional_context: {},
- });
-
- if (action === false) {
- action = { type: 'ir.actions.act_window_close' };
- } else if (_.isString(action) && instance.web.client_actions.contains(action)) {
- var action_client = { type: "ir.actions.client", tag: action, params: {} };
- return this.do_action(action_client, options);
- } else if (_.isNumber(action) || _.isString(action)) {
- var self = this;
- var additional_context = {
- active_id : options.additional_context.active_id,
- active_ids : options.additional_context.active_ids,
- active_model : options.additional_context.active_model
- };
- return self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) {
- return self.do_action(result, options);
- });
- }
-
- instance.web.bus.trigger('action', action);
-
- // Ensure context & domain are evaluated and can be manipulated/used
- var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
- action.context = instance.web.pyeval.eval('context', ncontext);
- if (action.context.active_id || action.context.active_ids) {
- // Here we assume that when an `active_id` or `active_ids` is used
- // in the context, we are in a `related` action, so we disable the
- // searchview's default custom filters.
- action.context.search_disable_custom_filters = true;
- }
- if (action.domain) {
- action.domain = instance.web.pyeval.eval(
- 'domain', action.domain, action.context || {});
- }
-
- if (!action.type) {
- console.error("No type for action", action);
- return $.Deferred().reject();
- }
- var type = action.type.replace(/\./g,'_');
- var popup = action.target === 'new';
- var inline = action.target === 'inline' || action.target === 'inlineview';
- var form = _.str.startsWith(action.view_mode, 'form');
- action.flags = _.defaults(action.flags || {}, {
- views_switcher : !popup && !inline,
- search_view : !popup && !inline,
- action_buttons : !popup && !inline,
- sidebar : !popup && !inline,
- pager : (!popup || !form) && !inline,
- display_title : !popup,
- headless: (popup || inline) && form,
- search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
- });
- action.menu_id = options.action_menu_id;
- action.context.params = _.extend({ 'action' : action.id }, action.context.params);
- if (!(type in this)) {
- console.error("Action manager can't handle action of type " + action.type, action);
- return $.Deferred().reject();
- }
- return this[type](action, options);
- },
- null_action: function() {
- this.dialog_stop();
- this.clear_widgets();
- },
- /**
- *
- * @param {Object} executor
- * @param {Object} executor.action original action
- * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
- * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
- * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
- * @param {Object} options
- * @return {*}
- */
- ir_actions_common: function(executor, options) {
- var widget;
- if (executor.action.target === 'new') {
- var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null;
- if (pre_dialog){
- // prevent previous dialog to consider itself closed,
- // right now, as we're opening a new one (prevents
- // reload of original form view)
- pre_dialog.off('closing', null, pre_dialog.on_close);
- }
- if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
- this.dialog_widget.destroy();
- }
- // explicitly passing a closing action to dialog_stop() prevents
- // it from reloading the original form view
- this.dialog_stop(executor.action);
- this.dialog = new instance.web.Dialog(this, {
- title: executor.action.name,
- dialogClass: executor.klass,
- });
-
- // chain on_close triggers with previous dialog, if any
- this.dialog.on_close = function(){
- options.on_close.apply(null, arguments);
- if (pre_dialog && pre_dialog.on_close){
- // no parameter passed to on_close as this will
- // only be called when the last dialog is truly
- // closing, and *should* trigger a reload of the
- // underlying form view (see comments above)
- pre_dialog.on_close();
- }
- };
- this.dialog.on("closing", null, this.dialog.on_close);
- widget = executor.widget();
- if (widget instanceof instance.web.ViewManager) {
- _.extend(widget.flags, {
- $buttons: this.dialog.$buttons,
- footer_to_buttons: true,
- });
- if (widget.action.view_mode === 'form') {
- widget.flags.headless = true;
- }
- }
- this.dialog_widget = widget;
- this.dialog_widget.setParent(this.dialog);
- var initialized = this.dialog_widget.appendTo(this.dialog.$el);
- this.dialog.open();
- return $.when(initialized);
- }
- if (this.inner_widget && this.webclient.has_uncommitted_changes()) {
- return $.Deferred().reject();
- }
- widget = executor.widget();
- this.dialog_stop(executor.action);
- return this.push_widget(widget, executor.action, options);
- },
- ir_actions_act_window: function (action, options) {
- var self = this;
-
- return this.ir_actions_common({
- widget: function () {
- return new instance.web.ViewManager(self, null, null, null, action);
- },
- action: action,
- klass: 'oe_act_window',
- }, options);
- },
- ir_actions_client: function (action, options) {
- var self = this;
- var ClientWidget = instance.web.client_actions.get_object(action.tag);
- if (!ClientWidget) {
- return self.do_warn("Action Error", "Could not find client action '" + action.tag + "'.");
- }
-
- if (!(ClientWidget.prototype instanceof instance.web.Widget)) {
- var next;
- if ((next = ClientWidget(this, action))) {
- return this.do_action(next, options);
- }
- return $.when();
- }
-
- return this.ir_actions_common({
- widget: function () { return new ClientWidget(self, action); },
- action: action,
- klass: 'oe_act_client',
- }, options).then(function () {
- if (action.tag !== 'reload') {self.do_push_state({});}
- });
- },
- ir_actions_act_window_close: function (action, options) {
- if (!this.dialog) {
- options.on_close();
- }
- this.dialog_stop();
- return $.when();
- },
- ir_actions_server: function (action, options) {
- var self = this;
- this.rpc('/web/action/run', {
- action_id: action.id,
- context: action.context || {}
- }).done(function (action) {
- self.do_action(action, options);
- });
- },
- ir_actions_report_xml: function(action, options) {
- var self = this;
- instance.web.blockUI();
- action = _.clone(action);
- var eval_contexts = ([instance.session.user_context] || []).concat([action.context]);
- action.context = instance.web.pyeval.eval('contexts',eval_contexts);
-
- // iOS devices doesn't allow iframe use the way we do it,
- // opening a new window seems the best way to workaround
- if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
- var params = {
- action: JSON.stringify(action),
- token: new Date().getTime()
- };
- var url = self.session.url('/web/report', params);
- instance.web.unblockUI();
- $('<a href="'+url+'" target="_blank"></a>')[0].click();
- return;
- }
- var c = instance.webclient.crashmanager;
- return $.Deferred(function (d) {
- self.session.get_file({
- url: '/web/report',
- data: {action: JSON.stringify(action)},
- complete: instance.web.unblockUI,
- success: function(){
- if (!self.dialog) {
- options.on_close();
- }
- self.dialog_stop();
- d.resolve();
- },
- error: function () {
- c.rpc_error.apply(c, arguments);
- d.reject();
- }
- });
- });
- },
- ir_actions_act_url: function (action) {
- if (action.target === 'self') {
- instance.web.redirect(action.url);
- } else {
- window.open(action.url, '_blank');
- }
- return $.when();
- },
-});
-
-instance.web.ViewManager = instance.web.Widget.extend({
- template: "ViewManager",
- /**
- * @param {Object} [dataset] null object (... historical reasons)
- * @param {Array} [views] List of [view_id, view_type]
- * @param {Object} [flags] various boolean describing UI state
- */
- init: function(parent, dataset, views, flags, action) {
- if (action) {
- flags = action.flags || {};
- if (!('auto_search' in flags)) {
- flags.auto_search = action.auto_search !== false;
- }
- if (action.res_model === 'board.board' && action.view_mode === 'form') {
- action.target = 'inline';
- // Special case for Dashboards
- _.extend(flags, {
- views_switcher : false,
- display_title : false,
- search_view : false,
- pager : false,
- sidebar : false,
- action_buttons : false
- });
- }
- this.action = action;
- this.action_manager = parent;
- dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
- if (action.res_id) {
- dataset.ids.push(action.res_id);
- dataset.index = 0;
- }
- views = action.views;
- }
- var self = this;
- this._super(parent);
-
- this.flags = flags || {};
- this.dataset = dataset;
- this.view_order = [];
- this.url_states = {};
- this.views = {};
- this.view_stack = []; // used for breadcrumbs
- this.active_view = null;
- this.searchview = null;
- this.active_search = null;
- this.registry = instance.web.views;
- this.title = this.action && this.action.name;
-
- _.each(views, function (view) {
- var view_type = view[1] || view.view_type,
- View = instance.web.views.get_object(view_type, true),
- view_label = View ? View.prototype.display_name: (void 'nope'),
- view_descr = {
- controller: null,
- options: view.options || {},
- view_id: view[0] || view.view_id,
- type: view_type,
- label: view_label,
- embedded_view: view.embedded_view,
- title: self.action && self.action.name,
- button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'),
- };
- self.view_order.push(view_descr);
- self.views[view_type] = view_descr;
- });
- this.multiple_views = (self.view_order.length > 1);
- },
- /**
- * @returns {jQuery.Deferred} initial view loading promise
- */
- start: function() {
- var self = this;
- var default_view = this.flags.default_view || this.view_order[0].type,
- default_options = this.flags[default_view] && this.flags[default_view].options;
-
- if (this.flags.headless) {
- this.$('.oe-view-manager-header').hide();
- }
- this._super();
- var $sidebar = this.flags.sidebar ? this.$('.oe-view-manager-sidebar') : undefined,
- $pager = this.$('.oe-view-manager-pager');
-
- this.$breadcrumbs = this.$('.oe-view-title');
- this.$switch_buttons = this.$('.oe-view-manager-switch button');
- this.$header = this.$('.oe-view-manager-header');
- this.$header_col = this.$header.find('.oe-header-title');
- this.$search_col = this.$header.find('.oe-view-manager-search-view');
- this.$switch_buttons.click(function (event) {
- var view_type = $(this).data('view-type');
- if ((view_type === 'form') && (self.active_view.type === 'form')) {
- self._display_view(view_type);
- } else {
- self.switch_mode(view_type);
- }
- });
- var views_ids = {};
- _.each(this.views, function (view) {
- views_ids[view.type] = view.view_id;
- view.options = _.extend({
- $buttons: self.$('.oe-' + view.type + '-buttons'),
- $sidebar : $sidebar,
- $pager : $pager,
- action : self.action,
- action_views_ids : views_ids,
- }, self.flags, self.flags[view.type], view.options);
- view.$container = self.$(".oe-view-manager-view-" + view.type);
- // show options.$buttons as views will put their $buttons inside it
- // and call show/hide on them
- view.options.$buttons.show();
- self.$('.oe-vm-switch-' + view.type).tooltip();
- });
- this.$('.oe_debug_view').click(this.on_debug_changed);
- this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current'));
-
- this.search_view_loaded = this.setup_search_view();
- var main_view_loaded = this.switch_mode(default_view, null, default_options);
-
- return $.when(main_view_loaded, this.search_view_loaded);
- },
-
- switch_mode: function(view_type, no_store, view_options) {
- var self = this,
- view = this.views[view_type];
- if (!view) {
- return $.Deferred().reject();
- }
- if ((view_type !== 'form') && (view_type !== 'diagram')) {
- this.view_stack = [];
- }
-
- this.view_stack.push(view);
-
- // Hide active view (at first rendering, there is no view to hide)
- if (this.active_view && this.active_view !== view) {
- if (this.active_view.controller) this.active_view.controller.do_hide();
- if (this.active_view.$container) this.active_view.$container.hide();
- }
- this.active_view = view;
-
- if (!view.created) {
- view.created = this.create_view.bind(this)(view, view_options);
- }
- this.active_search = $.Deferred();
-
- if (this.searchview &&
- this.flags.auto_search &&
- view.controller.searchable !== false) {
- $.when(this.search_view_loaded, view.created).done(this.searchview.do_search);
- } else {
- this.active_search.resolve();
- }
-
- self.update_header();
- return $.when(view.created, this.active_search).done(function () {
- self.active_view = view;
- self._display_view(view_options);
- self.trigger('switch_mode', view_type, no_store, view_options);
- if (self.session.debug) {
- self.$('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
- view: self.active_view.controller,
- view_manager: self,
- }));
- }
- });
- },
- update_header: function () {
- this.$switch_buttons.removeClass('active');
- this.$('.oe-vm-switch-' + this.active_view.type).addClass('active');
- },
- _display_view: function (view_options) {
- var self = this;
- this.active_view.$container.show();
- $.when(this.active_view.controller.do_show(view_options)).done(function () {
- if (self.searchview) {
- var is_hidden = self.active_view.controller.searchable === false;
- self.searchview.toggle_visibility(!is_hidden);
- self.$header_col.toggleClass('col-md-6', !is_hidden).toggleClass('col-md-12', is_hidden);
- self.$search_col.toggle(!is_hidden);
- }
- self.display_breadcrumbs();
- });
- },
- display_breadcrumbs: function () {
- var self = this;
- if (!this.action_manager) return;
- var breadcrumbs = this.action_manager.get_breadcrumbs();
- if (!breadcrumbs.length) return;
-
- var $breadcrumbs = breadcrumbs.map(function (bc, index) {
- return make_breadcrumb(bc, index === breadcrumbs.length - 1);
- });
-
- this.$breadcrumbs
- .empty()
- .append($breadcrumbs);
-
- function make_breadcrumb (bc, is_last) {
- var $bc = $('<li>')
- .append(is_last ? bc.title : $('<a>').text(bc.title))
- .toggleClass('active', is_last);
- if (!is_last) {
- $bc.click(function () {
- self.action_manager.select_widget(bc.widget, bc.index);
- });
- }
- return $bc;
- }
- },
- create_view: function(view, view_options) {
- var self = this,
- View = this.registry.get_object(view.type),
- options = _.clone(view.options),
- view_loaded = $.Deferred();
-
- if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline'))
- || (view_options && view_options.mode === 'edit'))) {
- options.initial_mode = 'edit';
- }
- var controller = new View(this, this.dataset, view.view_id, options),
- $container = view.$container;
-
- $container.hide();
- view.controller = controller;
- view.$container = $container;
-
- if (view.embedded_view) {
- controller.set_embedded_view(view.embedded_view);
- }
- controller.on('switch_mode', this, this.switch_mode.bind(this));
- controller.on('history_back', this, function () {
- if (self.action_manager) self.action_manager.trigger('history_back');
- });
- controller.on("change:title", this, function() {
- self.display_breadcrumbs();
- });
- controller.on('view_loaded', this, function () {
- view_loaded.resolve();
- });
- this.$('.oe-view-manager-pager > span').hide();
- return $.when(controller.appendTo($container), view_loaded)
- .done(function () {
- self.trigger("controller_inited", view.type, controller);
- });
- },
- select_view: function (index) {
- var view_type = this.view_stack[index].type;
- this.view_stack.splice(index);
- return this.switch_mode(view_type);
- },
- /**
- * @returns {Number|Boolean} the view id of the given type, false if not found
- */
- get_view_id: function(view_type) {
- return this.views[view_type] && this.views[view_type].view_id || false;
- },
- /**
- * Sets up the current viewmanager's search view.
- *
- * @param {Number|false} view_id the view to use or false for a default one
- * @returns {jQuery.Deferred} search view startup deferred
- */
- setup_search_view: function() {
- if (this.searchview) {
- this.searchview.destroy();
- }
-
- var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
-
- var search_defaults = {};
-
- var context = this.action ? this.action.context : [];
- _.each(context, function (value, key) {
- var match = /^search_default_(.*)$/.exec(key);
- if (match) {
- search_defaults[match[1]] = value;
- }
- });
-
-
- var options = {
- hidden: this.flags.search_view === false,
- disable_custom_filters: this.flags.search_disable_custom_filters,
- $buttons: this.$('.oe-search-options'),
- action: this.action,
- };
- var SearchView = instance.web.SearchView;
- this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
-
- this.searchview.on('search_data', this, this.search.bind(this));
- return this.searchview.appendTo(this.$(".oe-view-manager-search-view:first"));
- },
- search: function(domains, contexts, groupbys) {
- var self = this,
- controller = this.active_view.controller,
- action_context = this.action.context || {},
- view_context = controller.get_context();
- instance.web.pyeval.eval_domains_and_contexts({
- domains: [this.action.domain || []].concat(domains || []),
- contexts: [action_context, view_context].concat(contexts || []),
- group_by_seq: groupbys || []
- }).done(function (results) {
- if (results.error) {
- self.active_search.resolve();
- throw new Error(
- _.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
- JSON.stringify(results.error)));
- }
- self.dataset._model = new instance.web.Model(
- self.dataset.model, results.context, results.domain);
- var groupby = results.group_by.length
- ? results.group_by
- : action_context.group_by;
- if (_.isString(groupby)) {
- groupby = [groupby];
- }
- if (!controller.grouped && !_.isEmpty(groupby)){
- self.dataset.set_sort([]);
- }
- $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
- self.active_search.resolve();
- });
- });
- },
- do_push_state: function(state) {
- if (this.action_manager) {
- state.view_type = this.active_view.type;
- this.action_manager.do_push_state(state);
- }
- },
- do_load_state: function(state, warm) {
- if (state.view_type && state.view_type !== this.active_view.type) {
- // warning: this code relies on the fact that switch_mode has an immediate side
- // effect (setting the 'active_view' to its new value) AND an async effect (the
- // view is created/loaded). So, the next statement (do_load_state) is executed
- // on the new view, after it was initialized, but before it is fully loaded and
- // in particular, before the do_show method is called.
- this.switch_mode(state.view_type, true);
- }
- this.active_view.controller.do_load_state(state, warm);
- },
- on_debug_changed: function (evt) {
- var self = this,
- params = $(evt.target).data(),
- val = params.action,
- current_view = this.active_view.controller;
- switch (val) {
- case 'fvg':
- var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get") }).open();
- $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el);
- break;
- case 'tests':
- this.do_action({
- name: _t("JS Tests"),
- target: 'new',
- type : 'ir.actions.act_url',
- url: '/web/tests?mod=*'
- });
- break;
- case 'get_metadata':
- var ids = current_view.get_selected_ids();
- if (ids.length === 1) {
- this.dataset.call('get_metadata', [ids]).done(function(result) {
- var dialog = new instance.web.Dialog(this, {
- title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model),
- size: 'medium',
- buttons: {
- Ok: function() { this.parents('.modal').modal('hide');}
- },
- }, QWeb.render('ViewManagerDebugViewLog', {
- perm : result[0],
- format : instance.web.format_value
- })).open();
- });
- }
- break;
- case 'toggle_layout_outline':
- current_view.rendering_engine.toggle_layout_debugging();
- break;
- case 'set_defaults':
- current_view.open_defaults_dialog();
- break;
- case 'translate':
- this.do_action({
- name: _t("Technical Translation"),
- res_model : 'ir.translation',
- domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
- views: [[false, 'list'], [false, 'form']],
- type : 'ir.actions.act_window',
- view_type : "list",
- view_mode : "list"
- });
- break;
- case 'fields':
- this.dataset.call('fields_get', [false, {}]).done(function (fields) {
- var $root = $('<dl>');
- _(fields).each(function (attributes, name) {
- $root.append($('<dt>').append($('<h4>').text(name)));
- var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root));
- _(attributes).each(function (def, name) {
- if (def instanceof Object) {
- def = JSON.stringify(def);
- }
- $attrs
- .append($('<dt>').text(name))
- .append($('<dd style="white-space: pre-wrap;">').text(def));
- });
- });
- new instance.web.Dialog(self, {
- title: _.str.sprintf(_t("Model %s fields"),
- self.dataset.model),
- buttons: {
- Ok: function() { this.parents('.modal').modal('hide');}
- },
- }, $root).open();
- });
- break;
- case 'edit_workflow':
- return this.do_action({
- res_model : 'workflow',
- name: _t('Edit Workflow'),
- domain : [['osv', '=', this.dataset.model]],
- views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
- type : 'ir.actions.act_window',
- view_type : 'list',
- view_mode : 'list'
- });
- case 'edit':
- this.do_edit_resource(params.model, params.id, evt.target.text);
- break;
- case 'manage_filters':
- this.do_action({
- res_model: 'ir.filters',
- name: _t('Manage Filters'),
- views: [[false, 'list'], [false, 'form']],
- type: 'ir.actions.act_window',
- context: {
- search_default_my_filters: true,
- search_default_model_id: this.dataset.model
- }
- });
- break;
- case 'print_workflow':
- if (current_view.get_selected_ids && current_view.get_selected_ids().length == 1) {
- instance.web.blockUI();
- var action = {
- context: { active_ids: current_view.get_selected_ids() },
- report_name: "workflow.instance.graph",
- datas: {
- model: this.dataset.model,
- id: current_view.get_selected_ids()[0],
- nested: true,
- }
- };
- this.session.get_file({
- url: '/web/report',
- data: {action: JSON.stringify(action)},
- complete: instance.web.unblockUI
- });
- } else {
- self.do_warn("Warning", "No record selected.");
- }
- break;
- case 'leave_debug':
- window.location.search="?";
- break;
- default:
- if (val) {
- console.warn("No debug handler for ", val);
- }
- }
- },
- do_edit_resource: function(model, id, name) {
- this.do_action({
- res_model : model,
- res_id : id,
- name: name,
- type : 'ir.actions.act_window',
- view_type : 'form',
- view_mode : 'form',
- views : [[false, 'form']],
- target : 'new',
- flags : {
- action_buttons : true,
- headless: true,
- }
- });
- },
-});
-
-instance.web.Sidebar = instance.web.Widget.extend({
- init: function(parent) {
- var self = this;
- this._super(parent);
- var view = this.getParent();
- this.sections = [
- { 'name' : 'print', 'label' : _t('Print'), },
- { 'name' : 'other', 'label' : _t('More'), }
- ];
- this.items = {
- 'print' : [],
- 'other' : []
- };
- this.fileupload_id = _.uniqueId('oe_fileupload');
- $(window).on(this.fileupload_id, function() {
- var args = [].slice.call(arguments).slice(1);
- self.do_attachement_update(self.dataset, self.model_id,args);
- instance.web.unblockUI();
- });
- },
- start: function() {
- var self = this;
- this._super(this);
- this.redraw();
- this.$el.on('click','.dropdown-menu li a', function(event) {
- var section = $(this).data('section');
- var index = $(this).data('index');
- var item = self.items[section][index];
- if (item.callback) {
- item.callback.apply(self, [item]);
- } else if (item.action) {
- self.on_item_action_clicked(item);
- } else if (item.url) {
- return true;
- }
- event.preventDefault();
- });
- },
- redraw: function() {
- var self = this;
- self.$el.html(QWeb.render('Sidebar', {widget: self}));
-
- // Hides Sidebar sections when item list is empty
- this.$('.oe_form_dropdown_section').each(function() {
- $(this).toggle(!!$(this).find('li').length);
- });
- self.$("[title]").tooltip({
- delay: { show: 500, hide: 0}
- });
- },
- /**
- * For each item added to the section:
- *
- * ``label``
- * will be used as the item's name in the sidebar, can be html
- *
- * ``action``
- * descriptor for the action which will be executed, ``action`` and
- * ``callback`` should be exclusive
- *
- * ``callback``
- * function to call when the item is clicked in the sidebar, called
- * with the item descriptor as its first argument (so information
- * can be stored as additional keys on the object passed to
- * ``add_items``)
- *
- * ``classname`` (optional)
- * ``@class`` set on the sidebar serialization of the item
- *
- * ``title`` (optional)
- * will be set as the item's ``@title`` (tooltip)
- *
- * @param {String} section_code
- * @param {Array<{label, action | callback[, classname][, title]}>} items
- */
- add_items: function(section_code, items) {
- var self = this;
- if (items) {
- this.items[section_code].unshift.apply(this.items[section_code],items);
- this.redraw();
- }
- },
- add_toolbar: function(toolbar) {
- var self = this;
- _.each(['print','action','relate'], function(type) {
- var items = toolbar[type];
- if (items) {
- for (var i = 0; i < items.length; i++) {
- items[i] = {
- label: items[i]['name'],
- action: items[i],
- classname: 'oe_sidebar_' + type
- };
- }
- self.add_items(type=='print' ? 'print' : 'other', items);
- }
- });
- },
- on_item_action_clicked: function(item) {
- var self = this;
- self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) {
- var ids = self.getParent().get_selected_ids();
- var domain;
- if (self.getParent().get_active_domain) {
- domain = self.getParent().get_active_domain();
- }
- else {
- domain = $.Deferred().resolve(undefined);
- }
- if (ids.length === 0) {
- new instance.web.Dialog(this, { title: _t("Warning"), size: 'medium',}, $("<div />").text(_t("You must choose at least one record."))).open();
- return false;
- }
- var dataset = self.getParent().dataset;
- var active_ids_context = {
- active_id: ids[0],
- active_ids: ids,
- active_model: dataset.model,
- };
-
- $.when(domain).done(function (domain) {
- if (domain !== undefined) {
- active_ids_context.active_domain = domain;
- }
- var c = instance.web.pyeval.eval('context',
- new instance.web.CompoundContext(
- sidebar_eval_context, active_ids_context));
-
- self.rpc("/web/action/load", {
- action_id: item.action.id,
- context: new instance.web.CompoundContext(
- dataset.get_context(), active_ids_context).eval()
- }).done(function(result) {
- result.context = new instance.web.CompoundContext(
- result.context || {}, active_ids_context)
- .set_eval_context(c);
- result.flags = result.flags || {};
- result.flags.new_window = true;
- self.do_action(result, {
- on_close: function() {
- // reload view
- self.getParent().reload();
- },
- });
- });
- });
- });
- },
- do_attachement_update: function(dataset, model_id, args) {
- var self = this;
- this.dataset = dataset;
- this.model_id = model_id;
- if (args && args[0].error) {
- this.do_warn(_t('Uploading Error'), args[0].error);
- }
- if (!model_id) {
- this.on_attachments_loaded([]);
- } else {
- var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
- var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
- ds.read_slice(['name', 'url', 'type', 'create_uid', 'create_date', 'write_uid', 'write_date'], {}).done(this.on_attachments_loaded);
- }
- },
- on_attachments_loaded: function(attachments) {
- var self = this;
- var items = [];
- var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
- _.each(attachments,function(a) {
- a.label = a.name;
- if(a.type === "binary") {
- a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime());
- }
- });
- self.items.files = attachments;
- self.redraw();
- this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
- this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
- },
- on_attachment_changed: function(e) {
- var $e = $(e.target);
- if ($e.val() !== '') {
- this.$el.find('form.oe_form_binary_form').submit();
- $e.parent().find('input[type=file]').prop('disabled', true);
- $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
- this.$('.oe_sidebar_add_attachment a').text(_t('Uploading...'));
- instance.web.blockUI();
- }
- },
- on_attachment_delete: function(e) {
- e.preventDefault();
- e.stopPropagation();
- var self = this;
- var $e = $(e.currentTarget);
- if (confirm(_t("Do you really want to delete this attachment ?"))) {
- (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).done(function() {
- self.do_attachement_update(self.dataset, self.model_id);
- });
- }
- }
-});
-
-instance.web.View = instance.web.Widget.extend({
- // name displayed in view switchers
- display_name: '',
- /**
- * Define a view type for each view to allow automatic call to fields_view_get.
- */
- view_type: undefined,
- init: function(parent, dataset, view_id, options) {
- this._super(parent);
- this.ViewManager = parent;
- this.dataset = dataset;
- this.view_id = view_id;
- this.$buttons = options && options.$buttons;
- this.set_default_options(options);
- },
- start: function () {
- return this.load_view();
- },
- load_view: function(context) {
- var self = this;
- var view_loaded_def;
- if (this.embedded_view) {
- view_loaded_def = $.Deferred();
- $.async_when().done(function() {
- view_loaded_def.resolve(self.embedded_view);
- });
- } else {
- if (! this.view_type)
- console.warn("view_type is not defined", this);
- view_loaded_def = instance.web.fields_view_get({
- "model": this.dataset._model,
- "view_id": this.view_id,
- "view_type": this.view_type,
- "toolbar": !!this.options.$sidebar,
- "context": this.dataset.get_context(),
- });
- }
- return this.alive(view_loaded_def).then(function(r) {
- self.fields_view = r;
- // add css classes that reflect the (absence of) access rights
- self.$el.addClass('oe_view')
- .toggleClass('oe_cannot_create', !self.is_action_enabled('create'))
- .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit'))
- .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete'));
- return $.when(self.view_loading(r)).then(function() {
- self.trigger('view_loaded', r);
- });
- });
- },
- view_loading: function(r) {
- },
- set_default_options: function(options) {
- this.options = options || {};
- _.defaults(this.options, {
- // All possible views options should be defaulted here
- $sidebar: null,
- sidebar_id: null,
- action: null,
- action_views_ids: {}
- });
- },
- /**
- * Fetches and executes the action identified by ``action_data``.
- *
- * @param {Object} action_data the action descriptor data
- * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
- * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
- * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
- * @param {Object} [action_data.context=null] additional action context, to add to the current context
- * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
- * @param {Object} [record_id] the identifier of the object on which the action is to be applied
- * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
- */
- do_execute_action: function (action_data, dataset, record_id, on_closed) {
- var self = this;
- var result_handler = function () {
- if (on_closed) { on_closed.apply(null, arguments); }
- if (self.getParent() && self.getParent().on_action_executed) {
- return self.getParent().on_action_executed.apply(null, arguments);
- }
- };
- var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
-
- // response handler
- var handler = function (action) {
- if (action && action.constructor == Object) {
- // filter out context keys that are specific to the current action.
- // Wrong default_* and search_default_* values will no give the expected result
- // Wrong group_by values will simply fail and forbid rendering of the destination view
- var ncontext = new instance.web.CompoundContext(
- _.object(_.reject(_.pairs(dataset.get_context().eval()), function(pair) {
- return pair[0].match('^(?:(?:default_|search_default_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
- }))
- );
- ncontext.add(action_data.context || {});
- ncontext.add({active_model: dataset.model});
- if (record_id) {
- ncontext.add({
- active_id: record_id,
- active_ids: [record_id],
- });
- }
- ncontext.add(action.context || {});
- action.context = ncontext;
- return self.do_action(action, {
- on_close: result_handler,
- });
- } else {
- self.do_action({"type":"ir.actions.act_window_close"});
- return result_handler();
- }
- };
-
- if (action_data.special === 'cancel') {
- return handler({"type":"ir.actions.act_window_close"});
- } else if (action_data.type=="object") {
- var args = [[record_id]];
- if (action_data.args) {
- try {
- // Warning: quotes and double quotes problem due to json and xml clash
- // Maybe we should force escaping in xml or do a better parse of the args array
- var additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
- args = args.concat(additional_args);
- } catch(e) {
- console.error("Could not JSON.parse arguments", action_data.args);
- }
- }
- args.push(context);
- return dataset.call_button(action_data.name, args).then(handler).then(function () {
- if (instance.webclient) {
- instance.webclient.menu.do_reload_needaction();
- }
- });
- } else if (action_data.type=="action") {
- return this.rpc('/web/action/load', {
- action_id: action_data.name,
- context: _.extend(instance.web.pyeval.eval('context', context), {'active_model': dataset.model, 'active_ids': dataset.ids, 'active_id': record_id}),
- do_not_eval: true
- }).then(handler);
- } else {
- return dataset.exec_workflow(record_id, action_data.name).then(handler);
- }
- },
- /**
- * Directly set a view to use instead of calling fields_view_get. This method must
- * be called before start(). When an embedded view is set, underlying implementations
- * of instance.web.View must use the provided view instead of any other one.
- *
- * @param embedded_view A view.
- */
- set_embedded_view: function(embedded_view) {
- this.embedded_view = embedded_view;
- },
- do_show: function () {
- var self = this;
- if (this.$buttons) {
- this.$buttons.show();
- }
- this.$el.show();
- setTimeout(function () {
- self.$el.parent().addClass('in');
- }, 0);
-
- instance.web.bus.trigger('view_shown', this);
- },
- do_hide: function () {
- if (this.$buttons) {
- this.$buttons.hide();
- }
- this.$el.parent().removeClass('in');
- this.$el.hide();
- },
- is_active: function () {
- return this.ViewManager.active_view.controller === this;
- }, /**
- * Wraps fn to only call it if the current view is the active one. If the
- * current view is not active, doesn't call fn.
- *
- * fn can not return anything, as a non-call to fn can't return anything
- * either
- *
- * @param {Function} fn function to wrap in the active guard
- */
- guard_active: function (fn) {
- var self = this;
- return function () {
- if (self.is_active()) {
- fn.apply(self, arguments);
- }
- };
- },
- do_push_state: function(state) {
- if (this.getParent() && this.getParent().do_push_state) {
- this.getParent().do_push_state(state);
- }
- },
- do_load_state: function (state, warm) {
-
- },
- /**
- * Switches to a specific view type
- */
- do_switch_view: function() {
- this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments)));
- },
- do_search: function(domain, context, group_by) {
- },
- on_sidebar_export: function() {
- new instance.web.DataExport(this, this.dataset).open();
- },
- sidebar_eval_context: function () {
- return $.when({});
- },
- /**
- * Asks the view to reload itself, if the reloading is asynchronous should
- * return a {$.Deferred} indicating when the reloading is done.
- */
- reload: function () {
- return $.when();
- },
- /**
- * Return whether the user can perform the action ('create', 'edit', 'delete') in this view.
- * An action is disabled by setting the corresponding attribute in the view's main element,
- * like: <form string="" create="false" edit="false" delete="false">
- */
- is_action_enabled: function(action) {
- var attrs = this.fields_view.arch.attrs;
- return (action in attrs) ? JSON.parse(attrs[action]) : true;
- },
- get_context: function () {
- return {};
- },
-});
-
-/**
- * Performs a fields_view_get and apply postprocessing.
- * return a {$.Deferred} resolved with the fvg
- *
- * @param {Object} args
- * @param {String|Object} args.model instance.web.Model instance or string repr of the model
- * @param {Object} [args.context] context if args.model is a string
- * @param {Number} [args.view_id] id of the view to be loaded, default view if null
- * @param {String} [args.view_type] type of view to be loaded if view_id is null
- * @param {Boolean} [args.toolbar=false] get the toolbar definition
- */
-instance.web.fields_view_get = function(args) {
- function postprocess(fvg) {
- var doc = $.parseXML(fvg.arch).documentElement;
- fvg.arch = instance.web.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
- if ('id' in fvg.fields) {
- // Special case for id's
- var id_field = fvg.fields.id;
- id_field.original_type = id_field.type;
- id_field.type = 'id';
- }
- _.each(fvg.fields, function(field) {
- _.each(field.views || {}, function(view) {
- postprocess(view);
- });
- });
- return fvg;
- }
- args = _.defaults(args, {
- toolbar: false,
- });
- var model = args.model;
- if (typeof model === 'string') {
- model = new instance.web.Model(args.model, args.context);
- }
- return args.model.call('fields_view_get', {
- view_id: args.view_id,
- view_type: args.view_type,
- context: args.context,
- toolbar: args.toolbar
- }).then(function(fvg) {
- return postprocess(fvg);
- });
-};
-
-instance.web.xml_to_json = function(node, strip_whitespace) {
- switch (node.nodeType) {
- case 9:
- return instance.web.xml_to_json(node.documentElement, strip_whitespace);
- case 3:
- case 4:
- return (strip_whitespace && node.data.trim() === '') ? undefined : node.data;
- case 1:
- var attrs = $(node).getAttributes();
- _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
- if (attrs[key]) {
- try {
- attrs[key] = JSON.parse(attrs[key]);
- } catch(e) { }
- }
- });
- return {
- tag: node.tagName.toLowerCase(),
- attrs: attrs,
- children: _.compact(_.map(node.childNodes, function(node) {
- return instance.web.xml_to_json(node, strip_whitespace);
- })),
- };
- }
-};
-
-instance.web.json_node_to_xml = function(node, human_readable, indent) {
- // For debugging purpose, this function will convert a json node back to xml
- indent = indent || 0;
- var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
- r = sindent + '<' + node.tag,
- cr = human_readable ? '\n' : '';
-
- if (typeof(node) === 'string') {
- return sindent + node;
- } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
- throw new Error(
- _.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
- JSON.stringify(node)));
- }
- for (var attr in node.attrs) {
- var vattr = node.attrs[attr];
- if (typeof(vattr) !== 'string') {
- // domains, ...
- vattr = JSON.stringify(vattr);
- }
- vattr = vattr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
- if (human_readable) {
- vattr = vattr.replace(/&quot;/g, "'");
- }
- r += ' ' + attr + '="' + vattr + '"';
- }
- if (node.children && node.children.length) {
- r += '>' + cr;
- var childs = [];
- for (var i = 0, ii = node.children.length; i < ii; i++) {
- childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1));
- }
- r += childs.join(cr);
- r += cr + sindent + '</' + node.tag + '>';
- return r;
- } else {
- return r + '/>';
- }
-};
-instance.web.xml_to_str = function(node) {
- var str = "";
- if (window.XMLSerializer) {
- str = (new XMLSerializer()).serializeToString(node);
- } else if (window.ActiveXObject) {
- str = node.xml;
- } else {
- throw new Error(_t("Could not serialize XML"));
- }
- // Browsers won't deal with self closing tags except void elements:
- // http://www.w3.org/TR/html-markup/syntax.html
- var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
-
- // The following regex is a bit naive but it's ok for the xmlserializer output
- str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
- if (void_elements.indexOf(tag) < 0) {
- return "<" + tag + attrs + "></" + tag + ">";
- } else {
- return match;
- }
- });
- return str;
-};
-
-/**
- * Registry for all the main views
- */
-instance.web.views = new instance.web.Registry();
-
-})();
diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js
new file mode 100644
index 00000000000..c1f679fa4f3
--- /dev/null
+++ b/addons/web/static/src/js/views/form_common.js
@@ -0,0 +1,1135 @@
+odoo.define('web.form_common', ['web.core', 'web.data', 'web.Dialog', 'web.ListView', 'web.pyeval', 'web.SearchView', 'web.session', 'web.utils', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var ListView = require('web.ListView');
+var pyeval = require('web.pyeval');
+var SearchView = require('web.SearchView');
+var session = require('web.session');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+/**
+ * Interface implemented by the form view or any other object
+ * able to provide the features necessary for the fields to work.
+ *
+ * Properties:
+ * - display_invalid_fields : if true, all fields where is_valid() return true should
+ * be displayed as invalid.
+ * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
+ * Events:
+ * - view_content_has_changed : when the values of the fields have changed. When
+ * this event is triggered all fields should reprocess their modifiers.
+ * - field_changed:<field_name> : when the value of a field change, an event is triggered
+ * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
+ * This event is not related to the on_change mechanism of OpenERP and is always called
+ * when the value of a field is setted or changed. This event is only triggered when the
+ * value of the field is syntactically valid, but it can be triggered when the value
+ * is sematically invalid (ie, when a required field is false). It is possible that an event
+ * about a precise field is never triggered even if that field exists in the view, in that
+ * case the value of the field is assumed to be false.
+ */
+var FieldManagerMixin = {
+ /**
+ * Must return the asked field as in fields_get.
+ */
+ get_field_desc: function(field_name) {},
+ /**
+ * Returns the current value of a field present in the view. See the get_value() method
+ * method in FieldInterface for further information.
+ */
+ get_field_value: function(field_name) {},
+ /**
+ Gives new values for the fields contained in the view. The new values could not be setted
+ right after the call to this method. Setting new values can trigger on_changes.
+
+ @param {Object} values A dictonary with key = field name and value = new value.
+ @return {$.Deferred} Is resolved after all the values are setted.
+ */
+ set_values: function(values) {},
+ /**
+ Computes an OpenERP domain.
+
+ @param {Array} expression An OpenERP domain.
+ @return {boolean} The computed value of the domain.
+ */
+ compute_domain: function(expression) {},
+ /**
+ Builds an evaluation context for the resolution of the fields' contexts. Please note
+ the field are only supposed to use this context to evualuate their own, they should not
+ extend it.
+
+ @return {CompoundContext} An OpenERP context.
+ */
+ build_eval_context: function() {},
+};
+
+/**
+ Welcome.
+
+ If you read this documentation, it probably means that you were asked to use a form view widget outside of
+ a form view. Before going further, you must understand that those fields were never really created for
+ that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
+ you to hack the system with more style.
+*/
+var DefaultFieldManager = Widget.extend({
+ init: function(parent, eval_context) {
+ this._super(parent);
+ this.field_descs = {};
+ this.eval_context = eval_context || {};
+ this.set({
+ display_invalid_fields: false,
+ actual_mode: 'create',
+ });
+ },
+ get_field_desc: function(field_name) {
+ if (this.field_descs[field_name] === undefined) {
+ this.field_descs[field_name] = {
+ string: field_name,
+ };
+ }
+ return this.field_descs[field_name];
+ },
+ extend_field_desc: function(fields) {
+ var self = this;
+ _.each(fields, function(v, k) {
+ _.extend(self.get_field_desc(k), v);
+ });
+ },
+ get_field_value: function(field_name) {
+ return false;
+ },
+ set_values: function(values) {
+ // nothing
+ },
+ compute_domain: function(expression) {
+ return data.compute_domain(expression, {});
+ },
+ build_eval_context: function() {
+ return new data.CompoundContext(this.eval_context);
+ },
+});
+
+
+/**
+ * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
+ * switch.
+ */
+var ReinitializeWidgetMixin = {
+ /**
+ * Default implementation of, you should not override it, use initialize_field() instead.
+ */
+ start: function() {
+ this.initialize_field();
+ this._super();
+ },
+ initialize_field: function() {
+ this.on("change:effective_readonly", this, this.reinitialize);
+ this.initialize_content();
+ },
+ reinitialize: function() {
+ this.destroy_content();
+ this.renderElement();
+ this.initialize_content();
+ },
+ /**
+ * Called to destroy anything that could have been created previously, called before a
+ * re-initialization.
+ */
+ destroy_content: function() {},
+ /**
+ * Called to initialize the content.
+ */
+ initialize_content: function() {},
+};
+
+/**
+ * A mixin to apply on any field that has to completely re-render when its readonly state
+ * switch.
+ */
+var ReinitializeFieldMixin = _.extend({}, ReinitializeWidgetMixin, {
+ reinitialize: function() {
+ ReinitializeWidgetMixin.reinitialize.call(this);
+ var res = this.render_value();
+ if (this.view && this.view.render_value_defs){
+ this.view.render_value_defs.push(res);
+ }
+ },
+});
+
+/**
+ A mixin containing some useful methods to handle completion inputs.
+
+ The widget containing this option can have these arguments in its widget options:
+ - no_quick_create: if true, it will disable the quick create
+*/
+var CompletionFieldMixin = {
+ init: function() {
+ this.limit = 7;
+ this.orderer = new utils.DropMisordered();
+ this.can_create = this.node.attrs.can_create || true;
+ this.can_write = this.node.attrs.can_write || true;
+ },
+ /**
+ * Call this method to search using a string.
+ */
+ get_search_result: function(search_val) {
+ var self = this;
+
+ var dataset = new data.DataSet(this, this.field.relation, self.build_context());
+ this.last_query = search_val;
+ var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
+ if (!_(ids_blacklist).isEmpty()) {
+ exclusion_domain.push(['id', 'not in', ids_blacklist]);
+ }
+
+ return this.orderer.add(dataset.name_search(
+ search_val, new data.CompoundDomain(self.build_domain(), exclusion_domain),
+ 'ilike', this.limit + 1, self.build_context())).then(function(data) {
+ self.last_search = data;
+ // possible selections for the m2o
+ var values = _.map(data, function(x) {
+ x[1] = x[1].split("\n")[0];
+ return {
+ label: _.str.escapeHTML(x[1]),
+ value: x[1],
+ name: x[1],
+ id: x[0],
+ };
+ });
+
+ // search more... if more results that max
+ if (values.length > self.limit) {
+ values = values.slice(0, self.limit);
+ values.push({
+ label: _t("Search More..."),
+ action: function() {
+ dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
+ self._search_create_popup("search", data);
+ });
+ },
+ classname: 'oe_m2o_dropdown_option'
+ });
+ }
+ // quick create
+ var raw_result = _(data.result).map(function(x) {return x[1];});
+ if (search_val.length > 0 && !_.include(raw_result, search_val) &&
+ ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
+ self.can_create && values.push({
+ label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
+ $('<span />').text(search_val).html()),
+ action: function() {
+ self._quick_create(search_val);
+ },
+ classname: 'oe_m2o_dropdown_option'
+ });
+ }
+ // create...
+ if (!(self.options && (self.options.no_create || self.options.no_create_edit)) && self.can_create){
+ values.push({
+ label: _t("Create and Edit..."),
+ action: function() {
+ self._search_create_popup("form", undefined, self._create_context(search_val));
+ },
+ classname: 'oe_m2o_dropdown_option'
+ });
+ }
+ else if (values.length === 0) {
+ values.push({
+ label: _t("No results to show..."),
+ action: function() {},
+ classname: 'oe_m2o_dropdown_option'
+ });
+ }
+
+ return values;
+ });
+ },
+ get_search_blacklist: function() {
+ return [];
+ },
+ _quick_create: function(name) {
+ var self = this;
+ var slow_create = function () {
+ self._search_create_popup("form", undefined, self._create_context(name));
+ };
+ if (self.options.quick_create === undefined || self.options.quick_create) {
+ new data.DataSet(this, this.field.relation, self.build_context())
+ .name_create(name).done(function(data) {
+ if (!self.get('effective_readonly'))
+ self.add_id(data[0]);
+ }).fail(function(error, event) {
+ event.preventDefault();
+ slow_create();
+ });
+ } else
+ slow_create();
+ },
+ // all search/create popup handling
+ _search_create_popup: function(view, ids, context) {
+ var self = this;
+ var pop = new SelectCreatePopup(this);
+ pop.select_element(
+ self.field.relation,
+ {
+ title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
+ initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
+ initial_view: view,
+ disable_multiple_selection: true
+ },
+ self.build_domain(),
+ new data.CompoundContext(self.build_context(), context || {})
+ );
+ pop.on("elements_selected", self, function(element_ids) {
+ self.add_id(element_ids[0]);
+ self.focus();
+ });
+ },
+ /**
+ * To implement.
+ */
+ add_id: function(id) {},
+ _create_context: function(name) {
+ var tmp = {};
+ var field = (this.options || {}).create_name_field;
+ if (field === undefined)
+ field = "name";
+ if (field !== false && name && (this.options || {}).quick_create !== false)
+ tmp["default_" + field] = name;
+ return tmp;
+ },
+};
+
+
+
+/**
+ * Must be applied over an class already possessing the PropertiesMixin.
+ *
+ * Apply the result of the "invisible" domain to this.$el.
+ */
+var InvisibilityChangerMixin = {
+ init: function(field_manager, invisible_domain) {
+ var self = this;
+ this._ic_field_manager = field_manager;
+ this._ic_invisible_modifier = invisible_domain;
+ this._ic_field_manager.on("view_content_has_changed", this, function() {
+ var result = self._ic_invisible_modifier === undefined ? false :
+ self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
+ self.set({"invisible": result});
+ });
+ this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
+ var check = function() {
+ if (self.get("invisible") || self.get('force_invisible')) {
+ self.set({"effective_invisible": true});
+ } else {
+ self.set({"effective_invisible": false});
+ }
+ };
+ this.on('change:invisible', this, check);
+ this.on('change:force_invisible', this, check);
+ check.call(this);
+ },
+ start: function() {
+ this.on("change:effective_invisible", this, this._check_visibility);
+ this._check_visibility();
+ },
+ _check_visibility: function() {
+ this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
+ },
+};
+
+var InvisibilityChanger = core.Class.extend(core.mixins.PropertiesMixin, InvisibilityChangerMixin, {
+ init: function(parent, field_manager, invisible_domain, $el) {
+ this.setParent(parent);
+ core.mixins.PropertiesMixin.init.call(this);
+ InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
+ this.$el = $el;
+ this.start();
+ },
+});
+
+// Specialization of InvisibilityChanger for the `notebook` form element to handle more
+// elegantly its special cases (i.e. activate the closest visible sibling)
+var NotebookInvisibilityChanger = InvisibilityChanger.extend({
+ // Override start so that it does not call _check_visibility since it will be
+ // called again when view content will be loaded (event view_content_has_changed)
+ start: function() {
+ this.on("change:effective_invisible", this, this._check_visibility);
+ },
+ _check_visibility: function() {
+ this._super();
+ if (this.get("effective_invisible") === true) {
+ // Switch to invisible
+ // Remove this element as active and set a visible sibling active (if there is one)
+ if (this.$el.hasClass('active')) {
+ this.$el.removeClass('active');
+ var visible_siblings = this.$el.siblings(':not(.oe_form_invisible)');
+ if (visible_siblings.length) {
+ $(visible_siblings[0]).addClass('active');
+ }
+ }
+ } else {
+ // Switch to visible
+ // If there is no visible active sibling, set this element as active,
+ // otherwise if that sibling hasn't autofocus and if we are in edit mode,
+ // remove that sibling as active and set this element as active
+ var visible_active_sibling = this.$el.siblings(':not(.oe_form_invisible).active');
+ if (!(visible_active_sibling.length)) {
+ this.$el.addClass('active');
+ } else if (!$(visible_active_sibling[0]).data('autofocus') && this._ic_field_manager.get('actual_mode') === "edit") {
+ this.$el.addClass('active');
+ $(visible_active_sibling[0]).removeClass('active');
+ }
+ }
+ },
+});
+
+
+/**
+ Base class for all fields, custom widgets and buttons to be displayed in the form view.
+
+ Properties:
+ - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
+ the values of the "readonly" property and the "mode" property on the field manager.
+*/
+var FormWidget = Widget.extend(InvisibilityChangerMixin, {
+ /**
+ * @constructs instance.web.form.FormWidget
+ * @extends instance.web.Widget
+ *
+ * @param field_manager
+ * @param node
+ */
+ init: function(field_manager, node) {
+ this._super(field_manager);
+ this.field_manager = field_manager;
+ this.view = this.field_manager;
+ this.node = node;
+ this.session = session;
+ this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
+ InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
+
+ this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
+
+ this.set({
+ required: false,
+ readonly: false,
+ });
+ // some events to make the property "effective_readonly" sync automatically with "readonly" and
+ // "mode" on field_manager
+ var self = this;
+ var test_effective_readonly = function() {
+ self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
+ };
+ this.on("change:readonly", this, test_effective_readonly);
+ this.field_manager.on("change:actual_mode", this, test_effective_readonly);
+ test_effective_readonly.call(this);
+ },
+ renderElement: function() {
+ this.process_modifiers();
+ this._super();
+ this.$el.addClass(this.node.attrs["class"] || "");
+ },
+ destroy: function() {
+ $.fn.tooltip('destroy');
+ this._super.apply(this, arguments);
+ },
+ /**
+ * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
+ *
+ * This method is an utility method that is meant to be called by child classes.
+ *
+ * @param {jQuery} $e jQuery object of elements to bind focus/blur on
+ */
+ setupFocus: function ($e) {
+ var self = this;
+ $e.on({
+ focus: function () { self.trigger('focused'); },
+ blur: function () { self.trigger('blurred'); }
+ });
+ },
+ process_modifiers: function() {
+ var to_set = {};
+ for (var a in this.modifiers) {
+ if (!this.modifiers.hasOwnProperty(a)) { continue; }
+ if (!_.include(["invisible"], a)) {
+ var val = this.field_manager.compute_domain(this.modifiers[a]);
+ to_set[a] = val;
+ }
+ }
+ this.set(to_set);
+ },
+ do_attach_tooltip: function(widget, trigger, options) {
+ widget = widget || this;
+ trigger = trigger || this.$el;
+ options = _.extend({
+ delay: { show: 500, hide: 0 },
+ title: function() {
+ var template = widget.template + '.tooltip';
+ if (!QWeb.has_template(template)) {
+ template = 'WidgetLabel.tooltip';
+ }
+ return QWeb.render(template, {
+ debug: session.debug,
+ widget: widget
+ });
+ },
+ }, options || {});
+ //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
+ //as empty
+ if (session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
+ $(trigger).tooltip(options);
+ }
+ },
+ /**
+ * Builds a new context usable for operations related to fields by merging
+ * the fields'context with the action's context.
+ */
+ build_context: function() {
+ // only use the model's context if there is not context on the node
+ var v_context = this.node.attrs.context;
+ if (! v_context) {
+ v_context = (this.field || {}).context || {};
+ }
+
+ if (v_context.__ref || true) { //TODO: remove true
+ var fields_values = this.field_manager.build_eval_context();
+ v_context = new data.CompoundContext(v_context).set_eval_context(fields_values);
+ }
+ return v_context;
+ },
+ build_domain: function() {
+ var f_domain = this.field.domain || [];
+ var n_domain = this.node.attrs.domain || null;
+ // if there is a domain on the node, overrides the model's domain
+ var final_domain = n_domain !== null ? n_domain : f_domain;
+ if (!(final_domain instanceof Array) || true) { //TODO: remove true
+ var fields_values = this.field_manager.build_eval_context();
+ final_domain = new data.CompoundDomain(final_domain).set_eval_context(fields_values);
+ }
+ return final_domain;
+ }
+});
+
+/*
+# Values: (0, 0, { fields }) create
+# (1, ID, { fields }) update
+# (2, ID) remove (delete)
+# (3, ID) unlink one (target id or target of relation)
+# (4, ID) link
+# (5) unlink all (only valid for one2many)
+*/
+var commands = {
+ // (0, _, {values})
+ CREATE: 0,
+ 'create': function (values) {
+ return [commands.CREATE, false, values];
+ },
+ // (1, id, {values})
+ UPDATE: 1,
+ 'update': function (id, values) {
+ return [commands.UPDATE, id, values];
+ },
+ // (2, id[, _])
+ DELETE: 2,
+ 'delete': function (id) {
+ return [commands.DELETE, id, false];
+ },
+ // (3, id[, _]) removes relation, but not linked record itself
+ FORGET: 3,
+ 'forget': function (id) {
+ return [commands.FORGET, id, false];
+ },
+ // (4, id[, _])
+ LINK_TO: 4,
+ 'link_to': function (id) {
+ return [commands.LINK_TO, id, false];
+ },
+ // (5[, _[, _]])
+ DELETE_ALL: 5,
+ 'delete_all': function () {
+ return [5, false, false];
+ },
+ // (6, _, ids) replaces all linked records with provided ids
+ REPLACE_WITH: 6,
+ 'replace_with': function (ids) {
+ return [6, false, ids];
+ }
+};
+
+/**
+ * Interface to be implemented by fields.
+ *
+ * Events:
+ * - changed_value: triggered when the value of the field has changed. This can be due
+ * to a user interaction or a call to set_value().
+ *
+ */
+var FieldInterface = {
+ /**
+ * Constructor takes 2 arguments:
+ * - field_manager: Implements FieldManagerMixin
+ * - node: the "<field>" node in json form
+ */
+ init: function(field_manager, node) {},
+ /**
+ * Called by the form view to indicate the value of the field.
+ *
+ * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
+ * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
+ * before the widget is inserted into the DOM.
+ *
+ * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
+ * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
+ * also be able to handle any other format commonly used in the _defaults key on the models in the addons
+ * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
+ * no information is ever given to know which format is used.
+ */
+ set_value: function(value_) {},
+ /**
+ * Get the current value of the widget.
+ *
+ * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
+ * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
+ * For example if the field is marked as "required", a call to get_value() can return false.
+ *
+ * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
+ * return a default value according to the type of field.
+ *
+ * This method is always assumed to perform synchronously, it can not return a promise.
+ *
+ * If there was no user interaction to modify the value of the field, it is always assumed that
+ * get_value() return the same semantic value than the one passed in the last call to set_value(),
+ * although the syntax can be different. This can be the case for type of fields that have a different
+ * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
+ */
+ get_value: function() {},
+ /**
+ * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
+ * view.
+ */
+ set_input_id: function(id) {},
+ /**
+ * Returns true if is_syntax_valid() returns true and the value is semantically
+ * valid too according to the semantic restrictions applied to the field.
+ */
+ is_valid: function() {},
+ /**
+ * Returns true if the field holds a value which is syntactically correct, ignoring
+ * the potential semantic restrictions applied to the field.
+ */
+ is_syntax_valid: function() {},
+ /**
+ * Must set the focus on the field. Return false if field is not focusable.
+ */
+ focus: function() {},
+ /**
+ * Called when the translate button is clicked.
+ */
+ on_translate: function() {},
+ /**
+ This method is called by the form view before reading on_change values and before saving. It tells
+ the field to save its value before reading it using get_value(). Must return a promise.
+ */
+ commit_value: function() {},
+};
+
+/**
+ * Abstract class for classes implementing FieldInterface.
+ *
+ * Properties:
+ * - value: useful property to hold the value of the field. By default, set_value() and get_value()
+ * set and retrieve the value property. Changing the value property also triggers automatically
+ * a 'changed_value' event that inform the view to trigger on_changes.
+ *
+ */
+var AbstractField = FormWidget.extend(FieldInterface, {
+ /**
+ * @constructs instance.web.form.AbstractField
+ * @extends instance.web.form.FormWidget
+ *
+ * @param field_manager
+ * @param node
+ */
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.name = this.node.attrs.name;
+ this.field = this.field_manager.get_field_desc(this.name);
+ this.widget = this.node.attrs.widget;
+ this.string = this.node.attrs.string || this.field.string || this.name;
+ this.options = pyeval.py_eval(this.node.attrs.options || '{}');
+ this.set({'value': false});
+
+ this.on("change:value", this, function() {
+ this.trigger('changed_value');
+ this._check_css_flags();
+ });
+ },
+ renderElement: function() {
+ var self = this;
+ this._super();
+ if (this.field.translate && this.view) {
+ this.$el.addClass('oe_form_field_translatable');
+ this.$el.find('.oe_field_translate').click(this.on_translate);
+ }
+ this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
+ this.do_attach_tooltip(this, this.$label[0] || this.$el);
+ if (session.debug) {
+ this.$label.off('dblclick').on('dblclick', function() {
+ console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
+ window.w = self;
+ console.log("window.w =", window.w);
+ });
+ }
+ if (!this.disable_utility_classes) {
+ this.off("change:required", this, this._set_required);
+ this.on("change:required", this, this._set_required);
+ this._set_required();
+ }
+ this._check_visibility();
+ this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
+ this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
+ this._check_css_flags();
+ },
+ start: function() {
+ this._super();
+ this.on("change:value", this, function() {
+ if (! this.no_rerender)
+ this.render_value();
+ });
+ this.render_value();
+ },
+ /**
+ * Private. Do not use.
+ */
+ _set_required: function() {
+ this.$el.toggleClass('oe_form_required', this.get("required"));
+ },
+ set_value: function(value_) {
+ this.set({'value': value_});
+ },
+ get_value: function() {
+ return this.get('value');
+ },
+ /**
+ Utility method that all implementations should use to change the
+ value without triggering a re-rendering.
+ */
+ internal_set_value: function(value_) {
+ var tmp = this.no_rerender;
+ this.no_rerender = true;
+ this.set({'value': value_});
+ this.no_rerender = tmp;
+ },
+ /**
+ This method is called each time the value is modified.
+ */
+ render_value: function() {},
+ is_valid: function() {
+ return this.is_syntax_valid() && !(this.get('required') && this.is_false());
+ },
+ is_syntax_valid: function() {
+ return true;
+ },
+ /**
+ * Method useful to implement to ease validity testing. Must return true if the current
+ * value is similar to false in OpenERP.
+ */
+ is_false: function() {
+ return this.get('value') === false;
+ },
+ _check_css_flags: function() {
+ if (this.field.translate) {
+ this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
+ }
+ if (!this.disable_utility_classes) {
+ if (this.field_manager.get('display_invalid_fields')) {
+ this.$el.toggleClass('oe_form_invalid', !this.is_valid());
+ }
+ }
+ },
+ focus: function() {
+ return false;
+ },
+ set_input_id: function(id) {
+ this.id_for_label = id;
+ },
+ on_translate: function() {
+ var self = this;
+ var trans = new data.DataSet(this, 'ir.translation');
+ return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
+ self.do_action(r);
+ });
+ },
+
+ set_dimensions: function (height, width) {
+ this.$el.css({
+ width: width,
+ minHeight: height
+ });
+ },
+ commit_value: function() {
+ return $.when();
+ },
+});
+
+/**
+ * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
+ */
+var AbstractFormPopup = Widget.extend({
+ template: "AbstractFormPopup.render",
+ /**
+ * options:
+ * -readonly: only applicable when not in creation mode, default to false
+ * - alternative_form_view
+ * - view_id
+ * - write_function
+ * - read_function
+ * - create_function
+ * - parent_view
+ * - child_name
+ * - form_view_options
+ */
+ init_popup: function(model, row_id, domain, context, options) {
+ this.row_id = row_id;
+ this.model = model;
+ this.domain = domain || [];
+ this.context = context || {};
+ this.options = options;
+ _.defaults(this.options, {});
+ },
+ init_dataset: function() {
+ var self = this;
+ this.created_elements = [];
+ this.dataset = new data.ProxyDataSet(this, this.model, this.context);
+ this.dataset.read_function = this.options.read_function;
+ this.dataset.create_function = function(data, options, sup) {
+ var fct = self.options.create_function || sup;
+ return fct.call(this, data, options).done(function(r) {
+ self.trigger('create_completed saved', r);
+ self.created_elements.push(r);
+ });
+ };
+ this.dataset.write_function = function(id, data, options, sup) {
+ var fct = self.options.write_function || sup;
+ return fct.call(this, id, data, options).done(function(r) {
+ self.trigger('write_completed saved', r);
+ });
+ };
+ this.dataset.parent_view = this.options.parent_view;
+ this.dataset.child_name = this.options.child_name;
+ },
+ display_popup: function() {
+ var self = this;
+ this.renderElement();
+ var dialog = new Dialog(this, {
+ dialogClass: 'oe_act_window',
+ title: this.options.title || "",
+ }, this.$el).open();
+ dialog.on('closing', this, function (e){
+ self.check_exit(true);
+ });
+ this.$buttonpane = dialog.$buttons;
+ this.start();
+ },
+ setup_form_view: function() {
+ var self = this;
+ if (this.row_id) {
+ this.dataset.ids = [this.row_id];
+ this.dataset.index = 0;
+ } else {
+ this.dataset.index = null;
+ }
+ var options = _.clone(self.options.form_view_options) || {};
+ if (this.row_id !== null) {
+ options.initial_mode = this.options.readonly ? "view" : "edit";
+ }
+ _.extend(options, {
+ $buttons: this.$buttonpane,
+ });
+ var FormView = core.view_registry.get('form');
+ this.view_form = new FormView(this, this.dataset, this.options.view_id || false, options);
+ if (this.options.alternative_form_view) {
+ this.view_form.set_embedded_view(this.options.alternative_form_view);
+ }
+ this.view_form.appendTo(this.$(".oe_popup_form").show());
+ this.view_form.on("form_view_loaded", self, function() {
+ var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
+ self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
+ multi_select: multi_select,
+ readonly: self.row_id !== null && self.options.readonly,
+ }));
+ var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
+ $snbutton.click(function() {
+ $.when(self.view_form.save()).done(function() {
+ self.view_form.reload_mutex.exec(function() {
+ self.view_form.on_button_new();
+ });
+ });
+ });
+ var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
+ $sbutton.click(function() {
+ $.when(self.view_form.save()).done(function() {
+ self.view_form.reload_mutex.exec(function() {
+ self.check_exit();
+ });
+ });
+ });
+ var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
+ $cbutton.click(function() {
+ self.view_form.trigger('on_button_cancel');
+ self.check_exit();
+ });
+ self.view_form.do_show();
+ });
+ },
+ select_elements: function(element_ids) {
+ this.trigger("elements_selected", element_ids);
+ },
+ check_exit: function(no_destroy) {
+ if (this.created_elements.length > 0) {
+ this.select_elements(this.created_elements);
+ this.created_elements = [];
+ }
+ this.trigger('closed');
+ this.destroy();
+ },
+ destroy: function () {
+ this.trigger('closed');
+ if (this.$el.is(":data(bs.modal)")) {
+ this.$el.parents('.modal').modal('hide');
+ }
+ this._super();
+ },
+});
+
+/**
+ * Class to display a popup containing a form view.
+ */
+var FormOpenPopup = AbstractFormPopup.extend({
+ show_element: function(model, row_id, context, options) {
+ this.init_popup(model, row_id, [], context, options);
+ _.defaults(this.options, {
+ });
+ this.display_popup();
+ },
+ start: function() {
+ this._super();
+ this.init_dataset();
+ this.setup_form_view();
+ },
+});
+
+var SelectCreateListView = ListView.extend({
+ do_add_record: function () {
+ this.popup.new_object();
+ },
+ select_record: function(index) {
+ this.popup.select_elements([this.dataset.ids[index]]);
+ this.popup.destroy();
+ },
+ do_select: function(ids, records) {
+ this._super(ids, records);
+ this.popup.on_click_element(ids);
+ }
+});
+
+/**
+ * Class to display a popup to display a list to search a row. It also allows
+ * to switch to a form view to create a new row.
+ */
+var SelectCreatePopup = AbstractFormPopup.extend({
+ /**
+ * options:
+ * - initial_ids
+ * - initial_view: form or search (default search)
+ * - disable_multiple_selection
+ * - list_view_options
+ */
+ select_element: function(model, options, domain, context) {
+ this.init_popup(model, null, domain, context, options);
+ _.defaults(this.options, {
+ initial_view: "search",
+ });
+ this.initial_ids = this.options.initial_ids;
+ this.display_popup();
+ },
+ start: function() {
+ this.init_dataset();
+ if (this.options.initial_view == "search") {
+ var context = pyeval.sync_eval_domains_and_contexts({
+ domains: [],
+ contexts: [this.context]
+ }).context;
+ var search_defaults = {};
+ _.each(context, function (value_, key) {
+ var match = /^search_default_(.*)$/.exec(key);
+ if (match) {
+ search_defaults[match[1]] = value_;
+ }
+ });
+ this.setup_search_view(search_defaults);
+ } else { // "form"
+ this.new_object();
+ }
+ },
+ setup_search_view: function(search_defaults) {
+ var self = this;
+ if (this.searchview) {
+ this.searchview.destroy();
+ }
+ var $buttons = this.$('.o-search-options');
+ this.searchview = new SearchView(this,
+ this.dataset, false, search_defaults, {$buttons: $buttons});
+ this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
+ if (self.initial_ids) {
+ self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
+ contexts.concat(self.context), groupbys);
+ self.initial_ids = undefined;
+ } else {
+ self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
+ }
+ });
+ this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
+ self.searchview.toggle_visibility(true);
+ self.view_list = new SelectCreateListView(self,
+ self.dataset, false,
+ _.extend({'deletable': false,
+ 'selectable': !self.options.disable_multiple_selection,
+ 'import_enabled': false,
+ '$buttons': self.$buttonpane,
+ 'disable_editable_mode': true,
+ '$pager': self.$('.oe_popup_list_pager'),
+ }, self.options.list_view_options || {}));
+ self.view_list.on('edit:before', self, function (e) {
+ e.cancel = true;
+ });
+ self.view_list.popup = self;
+ self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
+ self.view_list.do_show();
+ }).then(function() {
+ if (self.options.initial_facet) {
+ self.searchview.query.reset([self.options.initial_facet], {
+ preventSearch: true,
+ });
+ }
+ self.searchview.do_search();
+ });
+ self.view_list.on("list_view_loaded", self, function() {
+ self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
+ var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
+ $cbutton.click(function() {
+ self.destroy();
+ });
+ var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
+ $sbutton.click(function() {
+ self.select_elements(self.selected_ids);
+ self.destroy();
+ });
+ $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
+ $cbutton.click(function() {
+ self.new_object();
+ });
+ });
+ });
+ },
+ do_search: function(domains, contexts, groupbys) {
+ var results = pyeval.sync_eval_domains_and_contexts({
+ domains: domains || [],
+ contexts: contexts || [],
+ group_by_seq: groupbys || []
+ });
+ this.view_list.do_search(results.domain, results.context, results.group_by);
+ },
+ on_click_element: function(ids) {
+ var self = this;
+ this.selected_ids = ids || [];
+ if(this.selected_ids.length > 0) {
+ self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
+ } else {
+ self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
+ }
+ },
+ new_object: function() {
+ if (this.searchview) {
+ this.searchview.do_hide();
+ }
+ if (this.view_list) {
+ this.view_list.do_hide();
+ this.view_list.$el.parent().hide();
+ }
+ this.setup_form_view();
+ },
+});
+
+var DomainEditorPopup = SelectCreatePopup.extend({
+ select_element: function (model, options, domain, context) {
+ if (options.readonly) {
+ return this._super(model, options, domain, context);
+ }
+ options.initial_facet = {
+ category: _t("Custom Filter"),
+ icon: 'fa-star',
+ field: {
+ get_context: function () { return context; },
+ get_groupby: function () { return []; },
+ get_domain: function () { return domain; },
+ },
+ values: [{label: _t("Selected domain"), value: null}],
+ };
+ this._super(model, options, [], context);
+ },
+ get_domain: function (selected_ids) {
+ var group_domain = [],
+ domain;
+ if (this.$('input.oe_list_record_selector').prop('checked')) {
+ if (this.view_list.grouped) {
+ var group_domain = _.chain(_.values(this.view_list.groups.children))
+ .filter(function (child) { return child.records.length; })
+ .map(function (c) { return c.datagroup.domain;})
+ .value();
+ group_domain = _.flatten(group_domain, true);
+ group_domain = _.times(group_domain.length - 1, _.constant('|')).concat(group_domain);
+ }
+ var search_data = this.searchview.build_search_data();
+ domain = pyeval.sync_eval_domains_and_contexts({
+ domains: search_data.domains,
+ contexts: search_data.contexts,
+ group_by_seq: search_data.groupbys || []
+ }).domain;
+ }
+ else {
+ domain = [["id", "in", selected_ids]];
+ }
+ return this.dataset.domain.concat(group_domain).concat(domain || []);
+ },
+});
+
+return {
+ // mixins
+ FieldManagerMixin: FieldManagerMixin,
+ ReinitializeWidgetMixin: ReinitializeWidgetMixin,
+ ReinitializeFieldMixin: ReinitializeFieldMixin,
+ CompletionFieldMixin: CompletionFieldMixin,
+
+ // misc
+ FormWidget: FormWidget,
+ DefaultFieldManager: DefaultFieldManager,
+ InvisibilityChanger: InvisibilityChanger,
+ NotebookInvisibilityChanger: NotebookInvisibilityChanger,
+ commands: commands,
+ AbstractField: AbstractField,
+
+ // popup dialogs
+ AbstractFormPopup: AbstractFormPopup,
+ FormOpenPopup: FormOpenPopup,
+ SelectCreatePopup: SelectCreatePopup,
+ DomainEditorPopup: DomainEditorPopup,
+};
+
+}); \ No newline at end of file
diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js
new file mode 100644
index 00000000000..19ca1903048
--- /dev/null
+++ b/addons/web/static/src/js/views/form_relational_widgets.js
@@ -0,0 +1,2028 @@
+odoo.define('web.form_relational', ['web.core', 'web.data', 'web.Dialog', 'web.form_common', 'web.FormView', 'web.ListView', 'web.Model', 'web.session', 'web.utils', 'web.ViewManager'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var common = require('web.form_common');
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var Model = require('web.Model');
+var session = require('web.session');
+var utils = require('web.utils');
+var ViewManager = require('web.ViewManager');
+
+
+var _t = core._t;
+var QWeb = core.qweb;
+var commands = common.commands;
+var list_widget_registry = core.list_widget_registry;
+
+var M2ODialog = Dialog.extend({
+ template: "M2ODialog",
+ init: function(parent) {
+ this.name = parent.string;
+ this._super(parent, {
+ title: _.str.sprintf(_t("Create a %s"), parent.string),
+ size: 'medium',
+ });
+ },
+ start: function() {
+ var self = this;
+ var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
+ this.$("p").text( text );
+ this.$buttons.html(QWeb.render("M2ODialog.buttons"));
+ this.$("input").val(this.getParent().$input.val());
+ this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
+ if (self.$("input").val() !== ''){
+ self.getParent()._quick_create(self.$("input").val());
+ self.destroy();
+ } else{
+ e.preventDefault();
+ self.$("input").focus();
+ }
+ });
+ this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
+ self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
+ self.destroy();
+ });
+ this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
+ self.destroy();
+ });
+ },
+});
+
+var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, common.ReinitializeFieldMixin, {
+ template: "FieldMany2One",
+ events: {
+ 'keydown input': function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ e.stopPropagation();
+ }
+ },
+ },
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ common.CompletionFieldMixin.init.call(this);
+ this.set({'value': false});
+ this.display_value = {};
+ this.display_value_backup = {};
+ this.last_search = [];
+ this.floating = false;
+ this.current_display = null;
+ this.is_started = false;
+ this.ignore_focusout = false;
+ },
+ reinit_value: function(val) {
+ this.internal_set_value(val);
+ this.floating = false;
+ if (this.is_started && !this.no_rerender) {
+ this.render_value();
+ }
+ },
+ initialize_field: function() {
+ this.is_started = true;
+ core.bus.on('click', this, function() {
+ if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
+ this.$input.autocomplete("close");
+ }
+ });
+ common.ReinitializeFieldMixin.initialize_field.call(this);
+ },
+ initialize_content: function() {
+ if (!this.get("effective_readonly"))
+ this.render_editable();
+ },
+ destroy_content: function () {
+ if (this.$drop_down) {
+ this.$drop_down.off('click');
+ delete this.$drop_down;
+ }
+ if (this.$input) {
+ this.$input.closest(".modal .modal-content").off('scroll');
+ this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
+ 'focus focusout change keydown');
+ delete this.$input;
+ }
+ if (this.$follow_button) {
+ this.$follow_button.off('blur focus click');
+ delete this.$follow_button;
+ }
+ },
+ destroy: function () {
+ this.destroy_content();
+ return this._super();
+ },
+ init_error_displayer: function() {
+ // nothing
+ },
+ hide_error_displayer: function() {
+ // doesn't work
+ },
+ show_error_displayer: function() {
+ new M2ODialog(this).open();
+ },
+ render_editable: function() {
+ var self = this;
+ this.$input = this.$el.find("input");
+
+ this.init_error_displayer();
+
+ self.$input.on('focus', function() {
+ self.hide_error_displayer();
+ });
+
+ this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
+ this.$follow_button = $(".oe_m2o_cm_button", this.$el);
+
+ this.$follow_button.click(function(ev) {
+ ev.preventDefault();
+ if (!self.get('value')) {
+ self.focus();
+ return;
+ }
+ var pop = new common.FormOpenPopup(self);
+ var context = self.build_context().eval();
+ var model_obj = new Model(self.field.relation);
+ model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
+ pop.show_element(
+ self.field.relation,
+ self.get("value"),
+ self.build_context(),
+ {
+ title: _t("Open: ") + self.string,
+ view_id: view_id,
+ readonly: !self.can_write
+ }
+ );
+ pop.on('write_completed', self, function(){
+ self.display_value = {};
+ self.display_value_backup = {};
+ self.render_value();
+ self.focus();
+ self.trigger('changed_value');
+ });
+ });
+ });
+
+ // some behavior for input
+ var input_changed = function() {
+ if (self.current_display !== self.$input.val()) {
+ self.current_display = self.$input.val();
+ if (self.$input.val() === "") {
+ self.internal_set_value(false);
+ self.floating = false;
+ } else {
+ self.floating = true;
+ }
+ }
+ };
+ this.$input.keydown(input_changed);
+ this.$input.change(input_changed);
+ this.$drop_down.click(function() {
+ self.$input.focus();
+ if (self.$input.autocomplete("widget").is(":visible")) {
+ self.$input.autocomplete("close");
+ } else {
+ if (self.get("value") && ! self.floating) {
+ self.$input.autocomplete("search", "");
+ } else {
+ self.$input.autocomplete("search");
+ }
+ }
+ });
+
+ // Autocomplete close on dialog content scroll
+ var close_autocomplete = _.debounce(function() {
+ if (self.$input.autocomplete("widget").is(":visible")) {
+ self.$input.autocomplete("close");
+ }
+ }, 50);
+ this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
+
+ self.ed_def = $.Deferred();
+ self.uned_def = $.Deferred();
+ var ed_delay = 200;
+ var ed_duration = 15000;
+ var anyoneLoosesFocus = function (e) {
+ if (self.ignore_focusout) { return; }
+ var used = false;
+ if (self.floating) {
+ if (self.last_search.length > 0) {
+ if (self.last_search[0][0] != self.get("value")) {
+ self.display_value = {};
+ self.display_value_backup = {};
+ self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
+ self.reinit_value(self.last_search[0][0]);
+ self.last_search = [];
+ } else {
+ used = true;
+ self.render_value();
+ }
+ } else {
+ used = true;
+ }
+ self.floating = false;
+ }
+ if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
+ self.ed_def.reject();
+ self.uned_def.reject();
+ self.ed_def = $.Deferred();
+ self.ed_def.done(function() {
+ self.can_create && self.show_error_displayer();
+ ignore_blur = false;
+ self.trigger('focused');
+ });
+ ignore_blur = true;
+ setTimeout(function() {
+ self.ed_def.resolve();
+ self.uned_def.reject();
+ self.uned_def = $.Deferred();
+ self.uned_def.done(function() {
+ self.hide_error_displayer();
+ });
+ setTimeout(function() {self.uned_def.resolve();}, ed_duration);
+ }, ed_delay);
+ } else {
+ self.no_ed = false;
+ self.ed_def.reject();
+ }
+ };
+ var ignore_blur = false;
+ this.$input.on({
+ focusout: anyoneLoosesFocus,
+ focus: function () { self.trigger('focused'); },
+ autocompleteopen: function () { ignore_blur = true; },
+ autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
+ blur: function () {
+ // autocomplete open
+ if (ignore_blur) { $(this).focus(); return; }
+ if (_(self.getChildren()).any(function (child) {
+ return child instanceof common.AbstractFormPopup;
+ })) { return; }
+ self.trigger('blurred');
+ }
+ });
+
+ var isSelecting = false;
+ // autocomplete
+ this.$input.autocomplete({
+ source: function(req, resp) {
+ self.get_search_result(req.term).done(function(result) {
+ resp(result);
+ });
+ },
+ select: function(event, ui) {
+ isSelecting = true;
+ var item = ui.item;
+ if (item.id) {
+ self.display_value = {};
+ self.display_value_backup = {};
+ self.display_value["" + item.id] = item.name;
+ self.reinit_value(item.id);
+ } else if (item.action) {
+ item.action();
+ // Cancel widget blurring, to avoid form blur event
+ self.trigger('focused');
+ return false;
+ }
+ },
+ focus: function(e, ui) {
+ e.preventDefault();
+ },
+ html: true,
+ // disabled to solve a bug, but may cause others
+ //close: anyoneLoosesFocus,
+ minLength: 0,
+ delay: 200,
+ });
+ var appendTo = this.$input.parents('.oe-view-manager-content, .modal-dialog').last();
+ if (appendTo.length === 0) {
+ appendTo = '.oe_application > *';
+ }
+ this.$input.autocomplete({
+ appendTo: appendTo
+ });
+ // set position for list of suggestions box
+ this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
+ this.$input.autocomplete("widget").openerpClass();
+ // used to correct a bug when selecting an element by pushing 'enter' in an editable list
+ this.$input.keyup(function(e) {
+ if (e.which === 13) { // ENTER
+ if (isSelecting)
+ e.stopPropagation();
+ }
+ isSelecting = false;
+ });
+ this.setupFocus(this.$follow_button);
+ },
+ render_value: function(no_recurse) {
+ var self = this;
+ if (! this.get("value")) {
+ this.display_string("");
+ return;
+ }
+ var display = this.display_value["" + this.get("value")];
+ if (display) {
+ this.display_string(display);
+ return;
+ }
+ if (! no_recurse) {
+ var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context());
+ var def = this.alive(dataset.name_get([self.get("value")])).done(function(data) {
+ if (!data[0]) {
+ self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
+ return;
+ }
+ self.display_value["" + self.get("value")] = data[0][1];
+ self.render_value(true);
+ }).fail( function (data, event) {
+ // avoid displaying crash errors as many2One should be name_get compliant
+ event.preventDefault();
+ self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
+ self.render_value(true);
+ });
+ if (this.view && this.view.render_value_defs){
+ this.view.render_value_defs.push(def);
+ }
+ }
+ },
+ display_string: function(str) {
+ var self = this;
+ if (!this.get("effective_readonly")) {
+ this.$input.val(str.split("\n")[0]);
+ this.current_display = this.$input.val();
+ if (this.is_false()) {
+ this.$('.oe_m2o_cm_button').css({'display':'none'});
+ } else {
+ this.$('.oe_m2o_cm_button').css({'display':'inline'});
+ }
+ } else {
+ var lines = _.escape(str).split("\n");
+ var link = "";
+ var follow = "";
+ link = lines[0];
+ follow = _.rest(lines).join("<br />");
+ if (follow)
+ link += "<br />";
+ var $link = this.$el.find('.oe_form_uri')
+ .unbind('click')
+ .html(link);
+ if (! this.options.no_open)
+ $link.click(function () {
+ var context = self.build_context().eval();
+ var model_obj = new Model(self.field.relation);
+ model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
+ self.do_action(action);
+ });
+ return false;
+ });
+ $(".oe_form_m2o_follow", this.$el).html(follow);
+ }
+ },
+ set_value: function(value_) {
+ if (value_ instanceof Array) {
+ this.display_value = {};
+ this.display_value_backup = {};
+ if (! this.options.always_reload) {
+ this.display_value["" + value_[0]] = value_[1];
+ }
+ else {
+ this.display_value_backup["" + value_[0]] = value_[1];
+ }
+ value_ = value_[0];
+ }
+ value_ = value_ || false;
+ this.reinit_value(value_);
+ },
+ get_displayed: function() {
+ return this.display_value["" + this.get("value")];
+ },
+ add_id: function(id) {
+ this.display_value = {};
+ this.display_value_backup = {};
+ this.reinit_value(id);
+ },
+ is_false: function() {
+ return ! this.get("value");
+ },
+ focus: function () {
+ var input = !this.get('effective_readonly') && this.$input && this.$input[0];
+ return input ? input.focus() : false;
+ },
+ _quick_create: function() {
+ this.no_ed = true;
+ this.ed_def.reject();
+ return common.CompletionFieldMixin._quick_create.apply(this, arguments);
+ },
+ _search_create_popup: function() {
+ this.no_ed = true;
+ this.ed_def.reject();
+ this.ignore_focusout = true;
+ this.reinit_value(false);
+ var res = common.CompletionFieldMixin._search_create_popup.apply(this, arguments);
+ this.ignore_focusout = false;
+ this.no_ed = false;
+ return res;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ if (!this.get("effective_readonly") && this.$input)
+ this.$input.css('height', height);
+ }
+});
+
+var Many2OneButton = common.AbstractField.extend({
+ template: 'Many2OneButton',
+ init: function(field_manager, node) {
+ this._super.apply(this, arguments);
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.set_button();
+ },
+ set_button: function() {
+ var self = this;
+ if (this.$button) {
+ this.$button.remove();
+ }
+ this.string = '';
+ this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
+ this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
+ this.$button.addClass('oe_link').css({'padding':'4px'});
+ this.$el.append(this.$button);
+ this.$button.on('click', self.on_click);
+ },
+ on_click: function(ev) {
+ var self = this;
+ this.popup = new common.FormOpenPopup(this);
+ this.popup.show_element(
+ this.field.relation,
+ this.get('value'),
+ this.build_context(),
+ {title: this.string}
+ );
+ this.popup.on('create_completed', self, function(r) {
+ self.set_value(r);
+ });
+ },
+ set_value: function(value_) {
+ if (value_ instanceof Array) {
+ value_ = value_[0];
+ }
+ value_ = value_ || false;
+ this.set('value', value_);
+ this.set_button();
+ },
+});
+
+/**
+ * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
+ * the big ugly button in the header.
+ *
+ * Requires the implementation of a ``is_readonly`` method (usually a proxy to
+ * the corresponding field's readonly or effective_readonly property) to
+ * decide whether the special row should or should not be inserted.
+ *
+ * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
+ * set on the insertion row.
+ */
+var AddAnItemList = ListView.List.extend({
+ pad_table_to: function (count) {
+ if (!this.view.is_action_enabled('create') || this.is_readonly()) {
+ this._super(count);
+ return;
+ }
+
+ this._super(count > 0 ? count - 1 : 0);
+
+ var self = this;
+ var columns = _(this.columns).filter(function (column) {
+ return column.invisible !== '1';
+ }).length;
+ if (this.options.selectable) { columns++; }
+ if (this.options.deletable) { columns++; }
+
+ var $cell = $('<td>', {
+ colspan: columns,
+ 'class': this._add_row_class || ''
+ }).append(
+ $('<a>', {href: '#'}).text(_t("Add an item"))
+ .mousedown(function () {
+ // FIXME: needs to be an official API somehow
+ if (self.view.editor.is_editing()) {
+ self.view.__ignore_blur = true;
+ }
+ })
+ .click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ // FIXME: there should also be an API for that one
+ if (self.view.editor.form.__blur_timeout) {
+ clearTimeout(self.view.editor.form.__blur_timeout);
+ self.view.editor.form.__blur_timeout = false;
+ }
+ self.view.ensure_saved().done(function () {
+ self.view.do_add_record();
+ });
+ }));
+
+ var $padding = this.$current.find('tr:not([data-id]):first');
+ var $newrow = $('<tr>').append($cell);
+ if ($padding.length) {
+ $padding.before($newrow);
+ } else {
+ this.$current.append($newrow);
+ }
+ }
+});
+
+
+var FieldOne2Many = common.AbstractField.extend({
+ multi_selection: false,
+ disable_utility_classes: true,
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.is_loaded = $.Deferred();
+ this.initial_is_loaded = this.is_loaded;
+ this.form_last_update = $.Deferred();
+ this.init_form_last_update = this.form_last_update;
+ this.is_started = false;
+ this.dataset = new One2ManyDataSet(this, this.field.relation);
+ this.dataset.o2m = this;
+ this.dataset.parent_view = this.view;
+ this.dataset.child_name = this.name;
+ var self = this;
+ this.dataset.on('dataset_changed', this, function() {
+ self.trigger_on_change();
+ });
+ this.set_value([]);
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$el.addClass('oe_form_field oe_form_field_one2many');
+
+ var self = this;
+
+ self.load_views();
+ var destroy = function() {
+ self.is_loaded = self.is_loaded.then(function() {
+ self.viewmanager.destroy();
+ return $.when(self.load_views()).done(function() {
+ self.reload_current_view();
+ });
+ });
+ };
+ this.is_loaded.done(function() {
+ self.on("change:effective_readonly", self, destroy);
+ });
+ this.view.on("on_button_cancel", self, destroy);
+ this.is_started = true;
+ this.reload_current_view();
+ },
+ trigger_on_change: function() {
+ this.trigger('changed_value');
+ },
+ load_views: function() {
+ var self = this;
+
+ var modes = this.node.attrs.mode;
+ modes = !!modes ? modes.split(",") : ["tree"];
+ var views = [];
+ _.each(modes, function(mode) {
+ if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
+ throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
+ }
+ var view = {
+ view_id: false,
+ view_type: mode == "tree" ? "list" : mode,
+ options: {}
+ };
+ if (self.field.views && self.field.views[mode]) {
+ view.embedded_view = self.field.views[mode];
+ }
+ if(view.view_type === "list") {
+ _.extend(view.options, {
+ addable: null,
+ selectable: self.multi_selection,
+ sortable: true,
+ import_enabled: false,
+ deletable: true
+ });
+ if (self.get("effective_readonly")) {
+ _.extend(view.options, {
+ deletable: null,
+ reorderable: false,
+ });
+ }
+ } else if (view.view_type === "form") {
+ if (self.get("effective_readonly")) {
+ view.view_type = 'form';
+ }
+ _.extend(view.options, {
+ not_interactible_on_create: true,
+ });
+ } else if (view.view_type === "kanban") {
+ _.extend(view.options, {
+ confirm_on_delete: false,
+ });
+ if (self.get("effective_readonly")) {
+ _.extend(view.options, {
+ action_buttons: false,
+ quick_creatable: false,
+ creatable: false,
+ read_only_mode: true,
+ });
+ }
+ }
+ views.push(view);
+ });
+ this.views = views;
+
+ this.viewmanager = new One2ManyViewManager(this, this.dataset, views, {});
+ this.viewmanager.o2m = self;
+ var once = $.Deferred().done(function() {
+ self.init_form_last_update.resolve();
+ });
+ var def = $.Deferred().done(function() {
+ self.initial_is_loaded.resolve();
+ });
+ this.viewmanager.on("controller_inited", self, function(view_type, controller) {
+ controller.o2m = self;
+ if (view_type == "list") {
+ if (self.get("effective_readonly")) {
+ controller.on('edit:before', self, function (e) {
+ e.cancel = true;
+ });
+ _(controller.columns).find(function (column) {
+ if (!(column instanceof list_widget_registry.get('field.handle'))) {
+ return false;
+ }
+ column.modifiers.invisible = true;
+ return true;
+ });
+ }
+ } else if (view_type === "form") {
+ if (self.get("effective_readonly")) {
+ $(".oe_form_buttons", controller.$el).children().remove();
+ }
+ controller.on("load_record", self, function(){
+ once.resolve();
+ });
+ controller.on('pager_action_executed',self,self.save_any_view);
+ } else if (view_type == "graph") {
+ self.reload_current_view();
+ }
+ def.resolve();
+ });
+ this.viewmanager.on("switch_mode", self, function(n_mode) {
+ $.when(self.save_any_view()).done(function() {
+ if (n_mode === "list") {
+ $.async_when().done(function() {
+ self.reload_current_view();
+ });
+ }
+ });
+ });
+ $.async_when().done(function () {
+ self.viewmanager.appendTo(self.$el);
+ });
+ return def;
+ },
+ reload_current_view: function() {
+ var self = this;
+ self.is_loaded = self.is_loaded.then(function() {
+ var view = self.get_active_view();
+ if (view.type === "list") {
+ return view.controller.reload_content();
+ } else if (view.type === "form") {
+ if (self.dataset.index === null && self.dataset.ids.length >= 1) {
+ self.dataset.index = 0;
+ }
+ var act = function() {
+ return view.controller.do_show();
+ };
+ self.form_last_update = self.form_last_update.then(act, act);
+ return self.form_last_update;
+ } else if (view.controller.do_search) {
+ return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
+ }
+ }, undefined);
+ return self.is_loaded;
+ },
+ get_active_view: function () {
+ /**
+ * Returns the current active view if any.
+ */
+ return (this.viewmanager && this.viewmanager.active_view);
+ },
+ set_value: function(value_) {
+ value_ = value_ || [];
+ var self = this;
+ var view = this.get_active_view();
+ this.dataset.reset_ids([]);
+ var ids;
+ if(value_.length >= 1 && value_[0] instanceof Array) {
+ ids = [];
+ _.each(value_, function(command) {
+ var obj = {values: command[2]};
+ switch (command[0]) {
+ case commands.CREATE:
+ obj.id = _.uniqueId(self.dataset.virtual_id_prefix);
+ obj.defaults = {};
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
+ ids.push(obj.id);
+ return;
+ case commands.UPDATE:
+ obj.id = command[1];
+ self.dataset.to_write.push(obj);
+ self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
+ ids.push(obj.id);
+ return;
+ case commands.DELETE:
+ self.dataset.to_delete.push({id: command[1]});
+ return;
+ case commands.LINK_TO:
+ ids.push(command[1]);
+ return;
+ case commands.DELETE_ALL:
+ self.dataset.delete_all = true;
+ return;
+ }
+ });
+ this._super(ids);
+ this.dataset.set_ids(ids);
+ } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
+ ids = [];
+ this.dataset.delete_all = true;
+ _.each(value_, function(command) {
+ var obj = {values: command};
+ obj.id = _.uniqueId(self.dataset.virtual_id_prefix);
+ obj.defaults = {};
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ ids.push(obj.id);
+ });
+ this._super(ids);
+ this.dataset.set_ids(ids);
+ } else {
+ this._super(value_);
+ this.dataset.reset_ids(value_);
+ }
+ if (this.dataset.index === null && this.dataset.ids.length > 0) {
+ this.dataset.index = 0;
+ }
+ this.trigger_on_change();
+ if (this.is_started && !this.no_rerender) {
+ return self.reload_current_view();
+ } else {
+ return $.when();
+ }
+ },
+ get_value: function() {
+ var self = this;
+ if (!this.dataset)
+ return [];
+ var val = this.dataset.delete_all ? [commands.delete_all()] : [];
+ val = val.concat(_.map(this.dataset.ids, function(id) {
+ var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
+ if (alter_order) {
+ return commands.create(alter_order.values);
+ }
+ alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
+ if (alter_order) {
+ return commands.update(alter_order.id, alter_order.values);
+ }
+ return commands.link_to(id);
+ }));
+ return val.concat(_.map(
+ this.dataset.to_delete, function(x) {
+ return commands['delete'](x.id);}));
+ },
+ commit_value: function() {
+ return this.save_any_view();
+ },
+ save_any_view: function() {
+ var view = this.get_active_view();
+ if (view) {
+ if (this.viewmanager.active_view.type === "form") {
+ if (view.controller.is_initialized.state() !== 'resolved') {
+ return $.when(false);
+ }
+ return $.when(view.controller.save());
+ } else if (this.viewmanager.active_view.type === "list") {
+ return $.when(view.controller.ensure_saved());
+ }
+ }
+ return $.when(false);
+ },
+ is_syntax_valid: function() {
+ var view = this.get_active_view();
+ if (!view){
+ return true;
+ }
+ switch (this.viewmanager.active_view.type) {
+ case 'form':
+ return _(view.controller.fields).chain()
+ .invoke('is_valid')
+ .all(_.identity)
+ .value();
+ case 'list':
+ return view.controller.is_valid();
+ }
+ return true;
+ },
+});
+
+var One2ManyDataSet = data.BufferedDataSet.extend({
+ get_context: function() {
+ this.context = this.o2m.build_context();
+ return this.context;
+ }
+});
+
+var One2ManyListView = ListView.extend({
+ _template: 'One2Many.listview',
+ init: function (parent, dataset, view_id, options) {
+ this._super(parent, dataset, view_id, _.extend(options || {}, {
+ GroupsType: One2ManyGroups,
+ ListType: One2ManyList
+ }));
+ this.on('edit:after', this, this.proxy('_after_edit'));
+ this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
+
+ this.records
+ .bind('add', this.proxy("changed_records"))
+ .bind('remove', this.proxy("changed_records"));
+ this.on('save:after', this, this.proxy("changed_records"));
+ },
+ start: function () {
+ var ret = this._super();
+ this.$el
+ .off('mousedown.handleButtons')
+ .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
+ return ret;
+ },
+ changed_records: function () {
+ this.o2m.trigger_on_change();
+ },
+ is_valid: function () {
+ var self = this;
+ if (!this.fields_view || !this.editable()){
+ return true;
+ }
+ if (_.isEmpty(this.records.records)){
+ return true;
+ }
+ var current_values = {};
+ _.each(this.editor.form.fields, function(field){
+ field._inhibit_on_change_flag = true;
+ field.no_rerender = true;
+ current_values[field.name] = field.get('value');
+ });
+ var valid = _.every(this.records.records, function(record){
+ _.each(self.editor.form.fields, function(field){
+ field.set_value(record.attributes[field.name]);
+ });
+ return _.every(self.editor.form.fields, function(field){
+ field.process_modifiers();
+ field._check_css_flags();
+ return field.is_valid();
+ });
+ });
+ _.each(this.editor.form.fields, function(field){
+ field.set('value', current_values[field.name]);
+ field._inhibit_on_change_flag = false;
+ field.no_rerender = false;
+ });
+ return valid;
+ },
+ do_add_record: function () {
+ if (this.editable()) {
+ this._super.apply(this, arguments);
+ } else {
+ var self = this;
+ var pop = new common.SelectCreatePopup(this);
+ pop.select_element(
+ self.o2m.field.relation,
+ {
+ title: _t("Create: ") + self.o2m.string,
+ initial_view: "form",
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views.form : undefined,
+ create_function: function(data, options) {
+ return self.o2m.dataset.create(data, options).done(function(r) {
+ self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
+ self.o2m.dataset.trigger("dataset_changed", r);
+ });
+ },
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ },
+ parent_view: self.o2m.view,
+ child_name: self.o2m.name,
+ form_view_options: {'not_interactible_on_create':true}
+ },
+ self.o2m.build_domain(),
+ self.o2m.build_context()
+ );
+ pop.on("elements_selected", self, function() {
+ self.o2m.reload_current_view();
+ });
+ }
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new common.FormOpenPopup(self);
+ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
+ title: _t("Open: ") + self.o2m.string,
+ write_function: function(id, data) {
+ return self.o2m.dataset.write(id, data, {}).done(function() {
+ self.o2m.reload_current_view();
+ });
+ },
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views.form : undefined,
+ parent_view: self.o2m.view,
+ child_name: self.o2m.name,
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ },
+ form_view_options: {'not_interactible_on_create':true},
+ readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
+ });
+ },
+ do_button_action: function (name, id, callback) {
+ if (!_.isNumber(id)) {
+ core.bus.trigger('display_notification_warning',
+ _t("Action Button"),
+ _t("The o2m record must be saved before an action can be used"));
+ return;
+ }
+ var parent_form = this.o2m.view;
+ var self = this;
+ this.ensure_saved().then(function () {
+ if (parent_form)
+ return parent_form.save();
+ else
+ return $.when();
+ }).done(function () {
+ var ds = self.o2m.dataset;
+ var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
+ return value.length;
+ });
+ if (!self.o2m.options.reload_on_button && !cached_records) {
+ self.handle_button(name, id, callback);
+ }else {
+ self.handle_button(name, id, function(){
+ self.o2m.view.reload();
+ });
+ }
+ });
+ },
+
+ _after_edit: function () {
+ this.__ignore_blur = false;
+ this.editor.form.on('blurred', this, this._on_form_blur);
+
+ // The form's blur thing may be jiggered during the edition setup,
+ // potentially leading to the o2m instasaving the row. Cancel any
+ // blurring triggered the edition startup here
+ this.editor.form.widgetFocused();
+ },
+ _before_unedit: function () {
+ this.editor.form.off('blurred', this, this._on_form_blur);
+ },
+ _button_down: function () {
+ // If a button is clicked (usually some sort of action button), it's
+ // the button's responsibility to ensure the editable list is in the
+ // correct state -> ignore form blurring
+ this.__ignore_blur = true;
+ },
+ /**
+ * Handles blurring of the nested form (saves the currently edited row),
+ * unless the flag to ignore the event is set to ``true``
+ *
+ * Makes the internal form go away
+ */
+ _on_form_blur: function () {
+ if (this.__ignore_blur) {
+ this.__ignore_blur = false;
+ return;
+ }
+ // FIXME: why isn't there an API for this?
+ if (this.editor.form.$el.hasClass('oe_form_dirty')) {
+ this.ensure_saved();
+ return;
+ }
+ this.cancel_edition();
+ },
+ keypress_ENTER: function () {
+ // blurring caused by hitting the [Return] key, should skip the
+ // autosave-on-blur and let the handler for [Return] do its thing (save
+ // the current row *anyway*, then create a new one/edit the next one)
+ this.__ignore_blur = true;
+ this._super.apply(this, arguments);
+ },
+ do_delete: function (ids) {
+ var confirm = window.confirm;
+ window.confirm = function () { return true; };
+ try {
+ return this._super(ids);
+ } finally {
+ window.confirm = confirm;
+ }
+ },
+ reload_record: function (record, options) {
+ if (!options || !options.do_not_evict) {
+ // Evict record.id from cache to ensure it will be reloaded correctly
+ this.dataset.evict_record(record.get('id'));
+ }
+
+ return this._super(record);
+ }
+});
+
+var One2ManyGroups = ListView.Groups.extend({
+ setup_resequence_rows: function () {
+ if (!this.view.o2m.get('effective_readonly')) {
+ this._super.apply(this, arguments);
+ }
+ }
+});
+
+var One2ManyList = AddAnItemList.extend({
+ _add_row_class: 'oe_form_field_one2many_list_row_add',
+ is_readonly: function () {
+ return this.view.o2m.get('effective_readonly');
+ },
+});
+
+var One2ManyFormView = FormView.extend({
+ form_template: 'One2Many.formview',
+ load_form: function(data) {
+ this._super(data);
+ var self = this;
+ this.$buttons.find('button.oe_form_button_create').click(function() {
+ self.save().done(self.on_button_new);
+ });
+ },
+ do_notify_change: function() {
+ if (this.dataset.parent_view) {
+ this.dataset.parent_view.do_notify_change();
+ } else {
+ this._super.apply(this, arguments);
+ }
+ }
+});
+
+var One2ManyViewManager = ViewManager.extend({
+ template: 'One2Many.viewmanager',
+ init: function(parent, dataset, views, flags) {
+ this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
+ this.registry = core.view_registry.extend({
+ list: One2ManyListView,
+ form: One2ManyFormView,
+ });
+ this.__ignore_blur = false;
+ },
+ switch_mode: function(mode, unused) {
+ if (mode !== 'form') {
+ return this._super(mode, unused);
+ }
+ var self = this;
+ var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
+ var pop = new common.FormOpenPopup(this);
+ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
+ title: _t("Open: ") + self.o2m.string,
+ create_function: function(data, options) {
+ return self.o2m.dataset.create(data, options).done(function(r) {
+ self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
+ self.o2m.dataset.trigger("dataset_changed", r);
+ });
+ },
+ write_function: function(id, data, options) {
+ return self.o2m.dataset.write(id, data, {}).done(function() {
+ self.o2m.reload_current_view();
+ });
+ },
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views.form : undefined,
+ parent_view: self.o2m.view,
+ child_name: self.o2m.name,
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ },
+ form_view_options: {'not_interactible_on_create':true},
+ readonly: self.o2m.get("effective_readonly")
+ });
+ pop.on("elements_selected", self, function() {
+ self.o2m.reload_current_view();
+ });
+ },
+});
+
+var FieldMany2ManyTags = common.AbstractField.extend(common.CompletionFieldMixin, common.ReinitializeFieldMixin, {
+ template: "FieldMany2ManyTags",
+ tag_template: "FieldMany2ManyTag",
+ init: function() {
+ this._super.apply(this, arguments);
+ common.CompletionFieldMixin.init.call(this);
+ this.set({"value": []});
+ this._display_orderer = new utils.DropMisordered();
+ this._drop_shown = false;
+ },
+ initialize_texttext: function(){
+ var self = this;
+ return {
+ plugins : 'tags arrow autocomplete',
+ autocomplete: {
+ render: function(suggestion) {
+ return $('<span class="text-label"/>').
+ data('index', suggestion.index).html(suggestion.label);
+ }
+ },
+ ext: {
+ autocomplete: {
+ selectFromDropdown: function() {
+ this.trigger('hideDropdown');
+ var index = Number(this.selectedSuggestionElement().children().children().data('index'));
+ var data = self.search_result[index];
+ if (data.id) {
+ self.add_id(data.id);
+ } else {
+ self.ignore_blur = true;
+ data.action();
+ }
+ this.trigger('setSuggestions', {result : []});
+ },
+ },
+ tags: {
+ isTagAllowed: function(tag) {
+ return !!tag.name;
+
+ },
+ removeTag: function(tag) {
+ var id = tag.data("id");
+ self.set({"value": _.without(self.get("value"), id)});
+ },
+ renderTag: function(stuff) {
+ return $.fn.textext.TextExtTags.prototype.renderTag.
+ call(this, stuff).data("id", stuff.id);
+ },
+ },
+ itemManager: {
+ itemToString: function(item) {
+ return item.name;
+ },
+ },
+ core: {
+ onSetInputData: function(e, data) {
+ if (data === '') {
+ this._plugins.autocomplete._suggestions = null;
+ }
+ this.input().val(data);
+ },
+ },
+ },
+ };
+ },
+ initialize_content: function() {
+ if (this.get("effective_readonly"))
+ return;
+ var self = this;
+ self.ignore_blur = false;
+ self.$text = this.$("textarea");
+ self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
+ var _this = this;
+ var str = !!data ? data.query || '' : '';
+ self.get_search_result(str).done(function(result) {
+ self.search_result = result;
+ $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
+ return _.extend(el, {index:i});
+ })});
+ });
+ }).bind('hideDropdown', function() {
+ self._drop_shown = false;
+ }).bind('showDropdown', function() {
+ self._drop_shown = true;
+ });
+ self.tags = self.$text.textext()[0].tags();
+ self.$text
+ .focusin(function () {
+ self.trigger('focused');
+ self.ignore_blur = false;
+ })
+ .focusout(function() {
+ self.$text.trigger("setInputData", "");
+ if (!self.ignore_blur) {
+ self.trigger('blurred');
+ }
+ }).keydown(function(e) {
+ if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
+ self.$text.textext()[0].autocomplete().selectFromDropdown();
+ }
+ });
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ is_false: function() {
+ return _(this.get("value")).isEmpty();
+ },
+ get_value: function() {
+ var tmp = [commands.replace_with(this.get("value"))];
+ return tmp;
+ },
+ get_search_blacklist: function() {
+ return this.get("value");
+ },
+ map_tag: function(data){
+ return _.map(data, function(el) {return {name: el[1], id:el[0]};});
+ },
+ get_render_data: function(ids){
+ var self = this;
+ var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context());
+ return dataset.name_get(ids);
+ },
+ render_tag: function(data) {
+ var self = this;
+ if (! self.get("effective_readonly")) {
+ self.tags.containerElement().children().remove();
+ self.$('textarea').css("padding-left", "3px");
+ self.tags.addTags(self.map_tag(data));
+ } else {
+ self.$el.html(QWeb.render(self.tag_template, {elements: data}));
+ }
+ },
+ render_value: function() {
+ var self = this;
+ var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context());
+ var values = self.get("value");
+ var handle_names = function(data) {
+ if (self.isDestroyed())
+ return;
+ var indexed = {};
+ _.each(data, function(el) {
+ indexed[el[0]] = el;
+ });
+ data = _.map(values, function(el) { return indexed[el]; });
+ self.render_tag(data);
+ };
+ if (! values || values.length > 0) {
+ return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
+ } else {
+ handle_names([]);
+ }
+ },
+ add_id: function(id) {
+ this.set({'value': _.uniq(this.get('value').concat([id]))});
+ },
+ focus: function () {
+ var input = this.$text && this.$text[0];
+ return input ? input.focus() : false;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ this.$("textarea").css({
+ width: width,
+ minHeight: height
+ });
+ },
+ _search_create_popup: function() {
+ this.ignore_blur = true;
+ return common.CompletionFieldMixin._search_create_popup.apply(this, arguments);
+ },
+});
+
+/**
+ widget options:
+ - reload_on_button: Reload the whole form view if click on a button in a list view.
+ If you see this options, do not use it, it's basically a dirty hack to make one
+ precise o2m to behave the way we want.
+*/
+var FieldMany2Many = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ multi_selection: false,
+ disable_utility_classes: true,
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.is_loaded = $.Deferred();
+ this.dataset = new Many2ManyDataSet(this, this.field.relation);
+ this.dataset.m2m = this;
+ var self = this;
+ this.dataset.on('unlink', self, function(ids) {
+ self.dataset_changed();
+ });
+ this.set_value([]);
+ this.list_dm = new utils.DropMisordered();
+ this.render_value_dm = new utils.DropMisordered();
+ },
+ initialize_content: function() {
+ var self = this;
+
+ this.$el.addClass('oe_form_field oe_form_field_many2many');
+
+ this.list_view = new Many2ManyListView(this, this.dataset, false, {
+ 'addable': null,
+ 'deletable': this.get("effective_readonly") ? false : true,
+ 'selectable': this.multi_selection,
+ 'sortable': false,
+ 'reorderable': false,
+ 'import_enabled': false,
+ });
+ var embedded = (this.field.views || {}).tree;
+ if (embedded) {
+ this.list_view.set_embedded_view(embedded);
+ }
+ this.list_view.m2m_field = this;
+ var loaded = $.Deferred();
+ this.list_view.on("list_view_loaded", this, function() {
+ loaded.resolve();
+ });
+ this.list_view.appendTo(this.$el);
+
+ var old_def = self.is_loaded;
+ self.is_loaded = $.Deferred().done(function() {
+ old_def.resolve();
+ });
+ this.list_dm.add(loaded).then(function() {
+ self.is_loaded.resolve();
+ });
+ },
+ destroy_content: function() {
+ this.list_view.destroy();
+ this.list_view = undefined;
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ get_value: function() {
+ return [commands.replace_with(this.get('value'))];
+ },
+ is_false: function () {
+ return _(this.get("value")).isEmpty();
+ },
+ render_value: function() {
+ var self = this;
+ this.dataset.set_ids(this.get("value"));
+ this.render_value_dm.add(this.is_loaded).then(function() {
+ return self.list_view.reload_content();
+ });
+ },
+ dataset_changed: function() {
+ this.internal_set_value(this.dataset.ids);
+ },
+});
+
+var Many2ManyDataSet = data.DataSetStatic.extend({
+ get_context: function() {
+ this.context = this.m2m.build_context();
+ return this.context;
+ }
+});
+
+/**
+ * @class
+ * @extends instance.web.ListView
+ */
+var Many2ManyListView = ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
+ init: function (parent, dataset, view_id, options) {
+ this._super(parent, dataset, view_id, _.extend(options || {}, {
+ ListType: Many2ManyList,
+ }));
+ },
+ do_add_record: function () {
+ var pop = new common.SelectCreatePopup(this);
+ pop.select_element(
+ this.model,
+ {
+ title: _t("Add: ") + this.m2m_field.string,
+ alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views.form : undefined,
+ no_create: this.m2m_field.options.no_create,
+ },
+ new data.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
+ this.m2m_field.build_context()
+ );
+ var self = this;
+ pop.on("elements_selected", self, function(element_ids) {
+ var reload = false;
+ _(element_ids).each(function (id) {
+ if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
+ self.dataset.set_ids(self.dataset.ids.concat([id]));
+ self.m2m_field.dataset_changed();
+ reload = true;
+ }
+ });
+ if (reload) {
+ self.reload_content();
+ }
+ });
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new common.FormOpenPopup(this);
+ pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
+ title: _t("Open: ") + this.m2m_field.string,
+ alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views.form : undefined,
+ readonly: this.getParent().get("effective_readonly")
+ });
+ pop.on('write_completed', self, self.reload_content);
+ },
+ do_button_action: function(name, id, callback) {
+ var self = this;
+ var _sup = _.bind(this._super, this);
+ if (! this.m2m_field.options.reload_on_button) {
+ return _sup(name, id, callback);
+ } else {
+ return this.m2m_field.view.save().then(function() {
+ return _sup(name, id, function() {
+ self.m2m_field.view.reload();
+ });
+ });
+ }
+ },
+ is_action_enabled: function () { return true; },
+});
+var Many2ManyList = AddAnItemList.extend({
+ _add_row_class: 'oe_form_field_many2many_list_row_add',
+ is_readonly: function () {
+ return this.view.m2m_field.get('effective_readonly');
+ }
+});
+
+var FieldMany2ManyKanban = common.AbstractField.extend(common.CompletionFieldMixin, {
+ disable_utility_classes: true,
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ common.CompletionFieldMixin.init.call(this);
+ this.is_loaded = $.Deferred();
+ this.initial_is_loaded = this.is_loaded;
+
+ var self = this;
+ this.dataset = new Many2ManyDataSet(this, this.field.relation);
+ this.dataset.m2m = this;
+ this.dataset.on('unlink', self, function(ids) {
+ self.dataset_changed();
+ });
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+
+ var self = this;
+
+ self.load_view();
+ self.on("change:effective_readonly", self, function() {
+ self.is_loaded = self.is_loaded.then(function() {
+ self.kanban_view.destroy();
+ return $.when(self.load_view()).done(function() {
+ self.render_value();
+ });
+ });
+ });
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ get_value: function() {
+ return [commands.replace_with(this.get('value'))];
+ },
+ load_view: function() {
+ var self = this;
+ var Many2ManyKanbanView = core.view_registry.get('many2many_kanban');
+ this.kanban_view = new Many2ManyKanbanView(this, this.dataset, false, {
+ 'create_text': _t("Add"),
+ 'creatable': self.get("effective_readonly") ? false : true,
+ 'quick_creatable': self.get("effective_readonly") ? false : true,
+ 'read_only_mode': self.get("effective_readonly") ? true : false,
+ 'confirm_on_delete': false,
+ });
+ var embedded = (this.field.views || {}).kanban;
+ if (embedded) {
+ this.kanban_view.set_embedded_view(embedded);
+ }
+ this.kanban_view.m2m = this;
+ var loaded = $.Deferred();
+ this.kanban_view.on("kanban_view_loaded",self,function() {
+ self.initial_is_loaded.resolve();
+ loaded.resolve();
+ });
+ this.kanban_view.on('switch_mode', this, this.open_popup);
+ $.async_when().done(function () {
+ self.kanban_view.appendTo(self.$el);
+ });
+ return loaded;
+ },
+ render_value: function() {
+ var self = this;
+ this.dataset.set_ids(this.get("value"));
+ this.is_loaded = this.is_loaded.then(function() {
+ return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
+ });
+ },
+ dataset_changed: function() {
+ this.set({'value': this.dataset.ids});
+ },
+ open_popup: function(type, unused) {
+ if (type !== "form")
+ return;
+ var self = this;
+ var pop;
+ if (this.dataset.index === null) {
+ pop = new common.SelectCreatePopup(this);
+ pop.select_element(
+ this.field.relation,
+ {
+ title: _t("Add: ") + this.string
+ },
+ new data.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
+ this.build_context()
+ );
+ pop.on("elements_selected", self, function(element_ids) {
+ _.each(element_ids, function(one_id) {
+ if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
+ self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
+ self.dataset_changed();
+ self.render_value();
+ }
+ });
+ });
+ } else {
+ var id = self.dataset.ids[self.dataset.index];
+ pop = new common.FormOpenPopup(this);
+ pop.show_element(self.field.relation, id, self.build_context(), {
+ title: _t("Open: ") + self.string,
+ write_function: function(id, data, options) {
+ return self.dataset.write(id, data, {}).done(function() {
+ self.render_value();
+ });
+ },
+ alternative_form_view: self.field.views ? self.field.views.form : undefined,
+ parent_view: self.view,
+ child_name: self.name,
+ readonly: self.get("effective_readonly")
+ });
+ }
+ },
+ add_id: function(id) {
+ this.quick_create.add_id(id);
+ },
+});
+
+/**
+ * Widget for (many2many field) to upload one or more file in same time and display in list.
+ * The user can delete his files.
+ * Options on attribute ; "blockui" {Boolean} block the UI or not
+ * during the file is uploading
+ */
+var FieldMany2ManyBinaryMultiFiles = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: "FieldBinaryFileUploader",
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.field_manager = field_manager;
+ this.node = node;
+ this.session = session;
+ if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
+ throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
+ }
+ this.data = {};
+ this.set_value([]);
+ this.ds_file = new data.DataSetSearch(this, 'ir.attachment');
+ this.fileupload_id = _.uniqueId('oe_fileupload_temp');
+ $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
+ },
+ initialize_content: function() {
+ this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ get_value: function() {
+ return [commands.replace_with(this.get("value"))];
+ },
+ get_file_url: function (attachment) {
+ return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment.id});
+ },
+ read_name_values : function () {
+ var self = this;
+ // don't reset know values
+ var ids = this.get('value');
+ var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
+ // send request for get_name
+ if (_value.length) {
+ return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
+ _.each(datas, function (data) {
+ data.no_unlink = true;
+ data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
+ self.data[data.id] = data;
+ });
+ return ids;
+ });
+ } else {
+ return $.when(ids);
+ }
+ },
+ render_value: function () {
+ var self = this;
+ this.read_name_values().then(function (ids) {
+ var render = $(QWeb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
+ render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
+ self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
+
+ // reinit input type file
+ var $input = self.$('input.oe_form_binary_file');
+ $input.after($input.clone(true)).remove();
+ self.$(".oe_fileupload").show();
+
+ });
+ },
+ on_file_change: function (event) {
+ event.stopPropagation();
+ var self = this;
+ var $target = $(event.target);
+ if ($target.val() !== '') {
+ var filename = $target.val().replace(/.*[\\\/]/,'');
+ // don't uplode more of one file in same time
+ if (self.data[0] && self.data[0].upload ) {
+ return false;
+ }
+ for (var id in this.get('value')) {
+ // if the files exits, delete the file before upload (if it's a new file)
+ if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
+ self.ds_file.unlink([id]);
+ }
+ }
+
+ // block UI or not
+ if(this.node.attrs.blockui>0) {
+ framework.blockUI();
+ }
+
+ // TODO : unactivate send on wizard and form
+
+ // submit file
+ this.$('form.oe_form_binary_form').submit();
+ this.$(".oe_fileupload").hide();
+ // add file on data result
+ this.data[0] = {
+ 'id': 0,
+ 'name': filename,
+ 'filename': filename,
+ 'url': '',
+ 'upload': true
+ };
+ }
+ },
+ on_file_loaded: function (event, result) {
+ var files = this.get('value');
+
+ // unblock UI
+ if(this.node.attrs.blockui>0) {
+ framework.unblockUI();
+ }
+
+ if (result.error || !result.id ) {
+ this.do_warn( _t('Uploading Error'), result.error);
+ delete this.data[0];
+ } else {
+ if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
+ delete this.data[0];
+ this.data[result.id] = {
+ 'id': result.id,
+ 'name': result.name,
+ 'filename': result.filename,
+ 'url': this.get_file_url(result)
+ };
+ } else {
+ this.data[result.id] = {
+ 'id': result.id,
+ 'name': result.name,
+ 'filename': result.filename,
+ 'url': this.get_file_url(result)
+ };
+ }
+ var values = _.clone(this.get('value'));
+ values.push(result.id);
+ this.set({'value': values});
+ }
+ this.render_value();
+ },
+ on_file_delete: function (event) {
+ event.stopPropagation();
+ var file_id=$(event.target).data("id");
+ if (file_id) {
+ var files = _.filter(this.get('value'), function (id) {return id != file_id;});
+ if(!this.data[file_id].no_unlink) {
+ this.ds_file.unlink([file_id]);
+ }
+ this.set({'value': files});
+ }
+ },
+});
+
+/*
+ This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
+ record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
+ will be added to the relation.
+*/
+var FieldMany2ManyCheckBoxes = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ className: "oe_form_many2many_checkboxes",
+ init: function() {
+ this._super.apply(this, arguments);
+ this.set("value", {});
+ this.set("records", []);
+ this.field_manager.on("view_content_has_changed", this, function() {
+ var domain = new data.CompoundDomain(this.build_domain()).eval();
+ if (! _.isEqual(domain, this.get("domain"))) {
+ this.set("domain", domain);
+ }
+ });
+ this.records_orderer = new utils.DropMisordered();
+ },
+ initialize_field: function() {
+ common.ReinitializeFieldMixin.initialize_field.call(this);
+ this.on("change:domain", this, this.query_records);
+ this.set("domain", new data.CompoundDomain(this.build_domain()).eval());
+ this.on("change:records", this, this.render_value);
+ },
+ query_records: function() {
+ var self = this;
+ var model = new Model(this.field.relation);
+ this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
+ return model.call("name_get", [record_ids] , {"context": self.build_context()});
+ })).then(function(res) {
+ self.set("records", res);
+ });
+ },
+ render_value: function() {
+ this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
+ var inputs = this.$("input");
+ inputs.change(_.bind(this.from_dom, this));
+ if (this.get("effective_readonly"))
+ inputs.attr("disabled", "true");
+ },
+ from_dom: function() {
+ var new_value = {};
+ this.$("input").each(function() {
+ var elem = $(this);
+ new_value[elem.data("record-id")] = elem.is(":checked") ? true : undefined;
+ });
+ if (! _.isEqual(new_value, this.get("value")))
+ this.internal_set_value(new_value);
+ },
+ // WARNING: (mostly) duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ var formatted = {};
+ _.each(value_, function(el) {
+ formatted[JSON.stringify(el)] = true;
+ });
+ this._super(formatted);
+ },
+ get_value: function() {
+ var value = _.filter(_.keys(this.get("value")), function(el) {
+ return this.get("value")[el];
+ }, this);
+ value = _.map(value, function(el) {
+ return JSON.parse(el);
+ });
+ return [commands.replace_with(value)];
+ },
+});
+
+/**
+ This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
+ "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
+ action on the model of the relation and show only the linked records.
+
+ Widget options:
+
+ * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
+ to display (or False to take the default one) and the second element is the type of the view. Defaults to
+ [[false, "tree"], [false, "form"]] .
+*/
+var X2ManyCounter = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ className: "oe_form_x2many_counter",
+ init: function() {
+ this._super.apply(this, arguments);
+ this.set("value", []);
+ _.defaults(this.options, {
+ "views": [[false, "tree"], [false, "form"]],
+ });
+ },
+ render_value: function() {
+ var text = _.str.sprintf("%d %s", this.val().length, this.string);
+ this.$().html(QWeb.render("X2ManyCounter", {text: text}));
+ this.$("a").click(_.bind(this.go_to, this));
+ },
+ go_to: function() {
+ return this.view.recursive_save().then(_.bind(function() {
+ var val = this.val();
+ var context = {};
+ if (this.field.type === "one2many") {
+ context["default_" + this.field.relation_field] = this.view.datarecord.id;
+ }
+ var domain = [["id", "in", val]];
+ return this.do_action({
+ type: 'ir.actions.act_window',
+ name: this.string,
+ res_model: this.field.relation,
+ views: this.options.views,
+ target: 'current',
+ context: context,
+ domain: domain,
+ });
+ }, this));
+ },
+ val: function() {
+ var value = this.get("value") || [];
+ if (value.length >= 1 && value[0] instanceof Array) {
+ value = value[0][2];
+ }
+ return value;
+ }
+});
+
+
+
+core.form_widget_registry
+ .add('many2one', FieldMany2One)
+ .add('many2onebutton', Many2OneButton)
+ .add('many2many', FieldMany2Many)
+ .add('many2many_tags', FieldMany2ManyTags)
+ .add('many2many_kanban', FieldMany2ManyKanban)
+ .add('one2many', FieldOne2Many)
+ .add('one2many_list', FieldOne2Many)
+ .add('many2many_binary', FieldMany2ManyBinaryMultiFiles)
+ .add('many2many_checkboxes', FieldMany2ManyCheckBoxes)
+ .add('x2many_counter', X2ManyCounter);
+
+return {
+ FieldMany2ManyTags: FieldMany2ManyTags,
+};
+
+});
+
+odoo.define('web_kanban.Many2ManyKanbanView', ['web.core', 'web_kanban.KanbanView', 'web.Widget'], function (require) {
+ "use strict";
+ // This code has a dependency on the addon web_kanban. This is a weird dependency issue. To fix it,
+ // we should either move this code into web_kanban, or move web_kanban into the web client.
+
+ var core = require('web.core'),
+ KanbanView = require('web_kanban.KanbanView'),
+ Widget = require('web.Widget');
+
+ var Many2ManyQuickCreate = Widget.extend({
+ template: 'Many2ManyKanban.quick_create',
+
+ /**
+ * close_btn: If true, the widget will display a "Close" button able to trigger
+ * a "close" event.
+ */
+ init: function(parent, dataset, context, buttons) {
+ this._super(parent);
+ this.m2m = this.getParent().view.m2m;
+ this.m2m.quick_create = this;
+ this._dataset = dataset;
+ this._buttons = buttons || false;
+ this._context = context || {};
+ },
+ start: function () {
+ var self = this;
+ self.$text = this.$el.find('input').css("width", "200px");
+ self.$text.textext({
+ plugins : 'arrow autocomplete',
+ autocomplete: {
+ render: function(suggestion) {
+ return $('<span class="text-label"/>').
+ data('index', suggestion.index).html(suggestion.label);
+ }
+ },
+ ext: {
+ autocomplete: {
+ selectFromDropdown: function() {
+ $(this).trigger('hideDropdown');
+ var index = Number(this.selectedSuggestionElement().children().children().data('index'));
+ var data = self.search_result[index];
+ if (data.id) {
+ self.add_id(data.id);
+ } else {
+ data.action();
+ }
+ },
+ },
+ itemManager: {
+ itemToString: function(item) {
+ return item.name;
+ },
+ },
+ },
+ }).bind('getSuggestions', function(e, data) {
+ var _this = this;
+ var str = !!data ? data.query || '' : '';
+ self.m2m.get_search_result(str).done(function(result) {
+ self.search_result = result;
+ $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
+ return _.extend(el, {index:i});
+ })});
+ });
+ });
+ self.$text.focusout(function() {
+ self.$text.val("");
+ });
+ },
+ focus: function() {
+ this.$text[0].focus();
+ },
+ add_id: function(id) {
+ var self = this;
+ self.$text.val("");
+ self.trigger('added', id);
+ this.m2m.dataset_changed();
+ },
+ });
+
+ var Many2ManyKanbanView = KanbanView.extend({
+ get_quick_create_class: function () {
+ return Many2ManyQuickCreate;
+ },
+ _is_quick_create_enabled: function() {
+ return this._super() && ! this.group_by;
+ },
+ });
+
+ core.view_registry.add('many2many_kanban', Many2ManyKanbanView);
+
+});
diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js
new file mode 100644
index 00000000000..ac7ea1d7888
--- /dev/null
+++ b/addons/web/static/src/js/views/form_view.js
@@ -0,0 +1,1587 @@
+odoo.define('web.FormView', ['web.core', 'web.crash_manager', 'web.data', 'web.Dialog', 'web.form_common', 'web.Model', 'web.Sidebar', 'web.utils', 'web.View'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var common = require('web.form_common');
+var Model = require('web.Model');
+var Sidebar = require('web.Sidebar');
+var utils = require('web.utils');
+var View = require('web.View');
+
+var _t = core._t;
+var _lt = core._lt;
+var QWeb = core.qweb;
+var Class = core.Class;
+
+/** @namespace */
+// instance.web.form = instance.web.form || {};
+
+/**
+ * Properties:
+ * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
+ * the mode used by the view.
+ */
+var FormView = View.extend(common.FieldManagerMixin, {
+ /**
+ * Indicates that this view is not searchable, and thus that no search
+ * view should be displayed (if there is one active).
+ */
+ searchable: false,
+ template: "FormView",
+ display_name: _lt('Form'),
+ view_type: "form",
+ /**
+ * @constructs instance.web.FormView
+ * @extends instance.web.View
+ *
+ * @param {instance.web.Session} session the current openerp session
+ * @param {instance.web.DataSet} dataset the dataset this view will work with
+ * @param {String} view_id the identifier of the OpenERP view object
+ *
+ * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
+ */
+ init: function(parent, dataset, view_id, options) {
+ var self = this;
+ this._super(parent);
+ this.ViewManager = parent;
+ this.set_default_options(options);
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id || false;
+ this.fields_view = {};
+ this.fields = {};
+ this.fields_order = [];
+ this.datarecord = {};
+ this._onchange_specs = {};
+ this.onchanges_mutex = new utils.Mutex();
+ this.default_focus_field = null;
+ this.default_focus_button = null;
+ this.fields_registry = core.form_widget_registry;
+ this.tags_registry = core.form_tag_registry;
+ this.widgets_registry = core.form_custom_registry;
+ this.has_been_loaded = $.Deferred();
+ this.translatable_fields = [];
+ _.defaults(this.options, {
+ "not_interactible_on_create": false,
+ "initial_mode": "view",
+ "disable_autofocus": false,
+ "footer_to_buttons": false,
+ });
+ this.is_initialized = $.Deferred();
+ this.mutating_mutex = new utils.Mutex();
+ this.save_list = [];
+ this.render_value_defs = [];
+ this.reload_mutex = new utils.Mutex();
+ this.__clicked_inside = false;
+ this.__blur_timeout = null;
+ this.rendering_engine = new FormRenderingEngine(this);
+ self.set({actual_mode: self.options.initial_mode});
+ this.has_been_loaded.done(function() {
+ self._build_onchange_specs();
+ self.on("change:actual_mode", self, self.check_actual_mode);
+ self.check_actual_mode();
+ self.on("change:actual_mode", self, self.init_pager);
+ self.init_pager();
+ });
+ self.on("load_record", self, self.load_record);
+ core.bus.on('clear_uncommitted_changes', this, function(e) {
+ if (!this.can_be_discarded()) {
+ e.preventDefault();
+ }
+ });
+ },
+ view_loading: function(r) {
+ return this.load_form(r);
+ },
+ destroy: function() {
+ _.each(this.get_widgets(), function(w) {
+ w.off('focused blurred');
+ w.destroy();
+ });
+ if (this.$el) {
+ this.$el.off('.formBlur');
+ }
+ this._super();
+ },
+ load_form: function(data) {
+ var self = this;
+ if (!data) {
+ throw new Error(_t("No data provided."));
+ }
+ if (this.arch) {
+ throw "Form view does not support multiple calls to load_form";
+ }
+ this.fields_order = [];
+ this.fields_view = data;
+
+ this.rendering_engine.set_fields_registry(this.fields_registry);
+ this.rendering_engine.set_tags_registry(this.tags_registry);
+ this.rendering_engine.set_widgets_registry(this.widgets_registry);
+ this.rendering_engine.set_fields_view(data);
+ var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
+ this.rendering_engine.render_to($dest);
+
+ this.$el.on('mousedown.formBlur', function () {
+ self.__clicked_inside = true;
+ });
+
+ this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
+ if (this.options.$buttons) {
+ this.$buttons.appendTo(this.options.$buttons);
+ } else {
+ this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
+ }
+ this.$buttons.on('click', '.oe_form_button_create',
+ this.guard_active(this.on_button_create));
+ this.$buttons.on('click', '.oe_form_button_edit',
+ this.guard_active(this.on_button_edit));
+ this.$buttons.on('click', '.oe_form_button_save',
+ this.guard_active(this.on_button_save));
+ this.$buttons.on('click', '.oe_form_button_cancel',
+ this.guard_active(this.on_button_cancel));
+ if (this.options.footer_to_buttons) {
+ this.$el.find('footer').appendTo(this.$buttons);
+ }
+
+ this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
+ if (!this.sidebar && this.options.$sidebar) {
+ this.sidebar = new Sidebar(this);
+ this.sidebar.appendTo(this.$sidebar);
+ if (this.fields_view.toolbar) {
+ this.sidebar.add_toolbar(this.fields_view.toolbar);
+ }
+ this.sidebar.add_items('other', _.compact([
+ self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
+ self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
+ ]));
+ }
+
+ this.has_been_loaded.resolve();
+
+ // Add bounce effect on button 'Edit' when click on readonly page view.
+ this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
+ if(self.get("actual_mode") == "view") {
+ var $button = self.options.$buttons.find(".oe_form_button_edit");
+ $button.openerpBounce();
+ e.stopPropagation();
+ core.bus.trigger('click', e);
+ }
+ });
+ //bounce effect on red button when click on statusbar.
+ this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
+ if((self.get("actual_mode") == "view")) {
+ var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
+ $button.openerpBounce();
+ e.stopPropagation();
+ }
+ });
+ this.trigger('form_view_loaded', data);
+ return $.when();
+ },
+ widgetFocused: function() {
+ // Clear click flag if used to focus a widget
+ this.__clicked_inside = false;
+ if (this.__blur_timeout) {
+ clearTimeout(this.__blur_timeout);
+ this.__blur_timeout = null;
+ }
+ },
+ widgetBlurred: function() {
+ if (this.__clicked_inside) {
+ // clicked in an other section of the form (than the currently
+ // focused widget) => just ignore the blurring entirely?
+ this.__clicked_inside = false;
+ return;
+ }
+ var self = this;
+ // clear timeout, if any
+ this.widgetFocused();
+ this.__blur_timeout = setTimeout(function () {
+ self.trigger('blurred');
+ }, 0);
+ },
+
+ do_load_state: function(state, warm) {
+ if (state.id && this.datarecord.id != state.id) {
+ if (this.dataset.get_id_index(state.id) === null) {
+ this.dataset.ids.push(state.id);
+ }
+ this.dataset.select_id(state.id);
+ this.do_show();
+ }
+ },
+ /**
+ *
+ * @param {Object} [options]
+ * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
+ * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
+ * @return {$.Deferred}
+ */
+ do_show: function (options) {
+ var self = this;
+ options = options || {};
+ if (this.sidebar) {
+ this.sidebar.$el.show();
+ }
+ if (this.$buttons) {
+ this.$buttons.show();
+ }
+ this.$el.show().css({
+ opacity: '0',
+ filter: 'alpha(opacity = 0)'
+ });
+ this.$el.add(this.$buttons).removeClass('oe_form_dirty');
+
+ var shown = this.has_been_loaded;
+ if (options.reload !== false) {
+ shown = shown.then(function() {
+ if (self.dataset.index === null) {
+ // null index means we should start a new record
+ return self.on_button_new();
+ }
+ var fields = _.keys(self.fields_view.fields);
+ fields.push('display_name');
+ return self.dataset.read_index(fields, {
+ context: { 'bin_size': true, 'future_display_name' : true }
+ }).then(function(r) {
+ self.trigger('load_record', r);
+ });
+ });
+ }
+ return $.when(shown, this._super()).then(function() {
+ self._actualize_mode(options.mode || self.options.initial_mode);
+ self.$el.css({
+ opacity: '1',
+ filter: 'alpha(opacity = 100)'
+ });
+ core.bus.trigger('form_view_shown', self);
+ });
+ },
+ do_hide: function () {
+ if (this.sidebar) {
+ this.sidebar.$el.hide();
+ }
+ if (this.$pager) {
+ this.$pager.hide();
+ }
+ this._super();
+ },
+ load_record: function(record) {
+ var self = this, set_values = [];
+ if (!record) {
+ this.set({ 'title' : undefined });
+ this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
+ return $.Deferred().reject();
+ }
+ this.datarecord = record;
+ this._actualize_mode();
+ this.set({ 'title' : record.id ? record.display_name : _t("New") });
+
+ _(this.fields).each(function (field, f) {
+ field._dirty_flag = false;
+ field._inhibit_on_change_flag = true;
+ var result = field.set_value(self.datarecord[f] || false);
+ field._inhibit_on_change_flag = false;
+ set_values.push(result);
+ });
+ return $.when.apply(null, set_values).then(function() {
+ if (!record.id) {
+ // trigger onchanges
+ self.do_onchange(null);
+ }
+ self.on_form_changed();
+ self.rendering_engine.init_fields();
+ self.is_initialized.resolve();
+ self.do_update_pager(record.id === null || record.id === undefined);
+ if (self.sidebar) {
+ self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
+ }
+ if (record.id) {
+ self.do_push_state({id:record.id});
+ } else {
+ self.do_push_state({});
+ }
+ self.$el.add(self.$buttons).removeClass('oe_form_dirty');
+ self.autofocus();
+ });
+ },
+ /**
+ * Loads and sets up the default values for the model as the current
+ * record
+ *
+ * @return {$.Deferred}
+ */
+ load_defaults: function () {
+ var self = this;
+ var keys = _.keys(this.fields_view.fields);
+ if (keys.length) {
+ return this.dataset.default_get(keys).then(function(r) {
+ self.trigger('load_record', r);
+ });
+ }
+ return self.trigger('load_record', {});
+ },
+ on_form_changed: function() {
+ this.trigger("view_content_has_changed");
+ },
+ do_notify_change: function() {
+ this.$el.add(this.$buttons).addClass('oe_form_dirty');
+ },
+ execute_pager_action: function(action) {
+ if (this.can_be_discarded()) {
+ switch (action) {
+ case 'first':
+ this.dataset.index = 0;
+ break;
+ case 'previous':
+ this.dataset.previous();
+ break;
+ case 'next':
+ this.dataset.next();
+ break;
+ case 'last':
+ this.dataset.index = this.dataset.ids.length - 1;
+ break;
+ }
+ var def = this.reload();
+ this.trigger('pager_action_executed');
+ return def;
+ }
+ return $.when();
+ },
+ init_pager: function() {
+ var self = this;
+ if (this.$pager)
+ this.$pager.remove();
+ if (this.get("actual_mode") === "create")
+ return;
+ this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
+ if (this.options.$pager) {
+ this.$pager.appendTo(this.options.$pager);
+ } else {
+ this.$el.find('.oe_form_pager').replaceWith(this.$pager);
+ }
+ this.$pager.on('click','a[data-pager-action]',function() {
+ var $el = $(this);
+ if ($el.attr("disabled"))
+ return;
+ var action = $el.data('pager-action');
+ var def = $.when(self.execute_pager_action(action));
+ $el.attr("disabled");
+ def.always(function() {
+ $el.removeAttr("disabled");
+ });
+ });
+ this.do_update_pager();
+ },
+ do_update_pager: function(hide_index) {
+ this.$pager.toggle(this.dataset.ids.length > 1);
+ if (hide_index) {
+ $(".oe_form_pager_state", this.$pager).html("");
+ } else {
+ $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
+ }
+ },
+
+ _build_onchange_specs: function() {
+ var self = this;
+ var find = function(field_name, root) {
+ var fields = [root];
+ while (fields.length) {
+ var node = fields.pop();
+ if (!node) {
+ continue;
+ }
+ if (node.tag === 'field' && node.attrs.name === field_name) {
+ return node.attrs.on_change || "";
+ }
+ fields = _.union(fields, node.children);
+ }
+ return "";
+ };
+
+ self._onchange_fields = [];
+ self._onchange_specs = {};
+ _.each(this.fields, function(field, name) {
+ self._onchange_fields.push(name);
+ self._onchange_specs[name] = find(name, field.node);
+ _.each(field.field.views, function(view) {
+ _.each(view.fields, function(_, subname) {
+ self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
+ });
+ });
+ });
+ },
+ _get_onchange_values: function() {
+ var field_values = this.get_fields_values();
+ if (field_values.id.toString().match(data.BufferedDataSet.virtual_id_regex)) {
+ delete field_values.id;
+ }
+ if (this.dataset.parent_view) {
+ // this belongs to a parent view: add parent field if possible
+ var parent_view = this.dataset.parent_view;
+ var child_name = this.dataset.child_name;
+ var parent_name = parent_view.get_field_desc(child_name).relation_field;
+ if (parent_name) {
+ // consider all fields except the inverse of the parent field
+ var parent_values = parent_view.get_fields_values();
+ delete parent_values[child_name];
+ field_values[parent_name] = parent_values;
+ }
+ }
+ return field_values;
+ },
+
+ do_onchange: function(widget) {
+ var self = this;
+ var onchange_specs = self._onchange_specs;
+ try {
+ var def = $.when({});
+ var change_spec = widget ? onchange_specs[widget.name] : null;
+ if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
+ var ids = [],
+ trigger_field_name = widget ? widget.name : self._onchange_fields,
+ values = self._get_onchange_values(),
+ context = new data.CompoundContext(self.dataset.get_context());
+
+ if (widget && widget.build_context()) {
+ context.add(widget.build_context());
+ }
+ if (self.dataset.parent_view) {
+ var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
+ context.add({field_parent: parent_name});
+ }
+
+ if (self.datarecord.id && !data.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
+ // In case of a o2m virtual id, we should pass an empty ids list
+ ids.push(self.datarecord.id);
+ }
+ def = self.alive(new Model(self.dataset.model).call(
+ "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
+ }
+ this.onchanges_mutex.exec(function(){
+ return def.then(function(response) {
+ var fields = {};
+ if (widget){
+ fields[widget.name] = widget.field;
+ }
+ else{
+ fields = self.fields_view.fields;
+ }
+ var defs = [];
+ _.each(fields, function(field, fieldname){
+ if (field && field.change_default) {
+ var value_;
+ if (response.value && (fieldname in response.value)) {
+ // Use value from onchange if onchange executed
+ value_ = response.value[fieldname];
+ } else {
+ // otherwise get form value for field
+ value_ = self.fields[fieldname].get_value();
+ }
+ var condition = fieldname + '=' + value_;
+
+ if (value_) {
+ defs.push(self.alive(new Model('ir.values').call(
+ 'get_defaults', [self.model, condition]
+ )).then(function (results) {
+ if (!results.length) {
+ return response;
+ }
+ if (!response.value) {
+ response.value = {};
+ }
+ for(var i=0; i<results.length; ++i) {
+ // [whatever, key, value]
+ var triplet = results[i];
+ response.value[triplet[1]] = triplet[2];
+ }
+ return response;
+ }));
+ }
+ }
+ });
+ return _.isEmpty(defs) ? response : $.when.apply(null, defs);
+ }).then(function(response) {
+ return self.on_processed_onchange(response);
+ });
+ });
+ return this.onchanges_mutex.def;
+ } catch(e) {
+ console.error(e);
+ crash_manager.show_message(e);
+ return $.Deferred().reject();
+ }
+ },
+ on_processed_onchange: function(result) {
+ try {
+ var fields = this.fields;
+ _(result.domain).each(function (domain, fieldname) {
+ var field = fields[fieldname];
+ if (!field) { return; }
+ field.node.attrs.domain = domain;
+ });
+
+ if (!_.isEmpty(result.value)) {
+ this._internal_set_values(result.value);
+ }
+ // FIXME XXX a list of warnings?
+ if (!_.isEmpty(result.warning)) {
+ new Dialog(this, {
+ size: 'medium',
+ title:result.warning.title,
+ buttons: [
+ {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
+ ]
+ }, QWeb.render("CrashManager.warning", result.warning)).open();
+ }
+
+ return $.Deferred().resolve();
+ } catch(e) {
+ console.error(e);
+ crash_manager.show_message(e);
+ return $.Deferred().reject();
+ }
+ },
+ _process_operations: function() {
+ var self = this;
+ return this.mutating_mutex.exec(function() {
+ function iterate() {
+
+ var mutex = new utils.Mutex();
+ _.each(self.fields, function(field) {
+ self.onchanges_mutex.def.then(function(){
+ mutex.exec(function(){
+ return field.commit_value();
+ });
+ });
+ });
+
+ var args = _.toArray(arguments);
+ return $.when.apply(null, [mutex.def, self.onchanges_mutex.def]).then(function() {
+ var save_obj = self.save_list.pop();
+ if (save_obj) {
+ return self._process_save(save_obj).then(function() {
+ save_obj.ret = _.toArray(arguments);
+ return iterate();
+ }, function() {
+ save_obj.error = true;
+ });
+ }
+ return $.when();
+ }).fail(function() {
+ self.save_list.pop();
+ return $.when();
+ });
+ }
+ return iterate();
+ });
+ },
+ _internal_set_values: function(values) {
+ for (var f in values) {
+ if (!values.hasOwnProperty(f)) { continue; }
+ var field = this.fields[f];
+ // If field is not defined in the view, just ignore it
+ if (field) {
+ var value_ = values[f];
+ if (field.get_value() != value_) {
+ field._inhibit_on_change_flag = true;
+ field.set_value(value_);
+ field._inhibit_on_change_flag = false;
+ field._dirty_flag = true;
+ }
+ }
+ }
+ this.on_form_changed();
+ },
+ set_values: function(values) {
+ var self = this;
+ return this.mutating_mutex.exec(function() {
+ self._internal_set_values(values);
+ });
+ },
+ /**
+ * Ask the view to switch to view mode if possible. The view may not do it
+ * if the current record is not yet saved. It will then stay in create mode.
+ */
+ to_view_mode: function() {
+ this._actualize_mode("view");
+ },
+ /**
+ * Ask the view to switch to edit mode if possible. The view may not do it
+ * if the current record is not yet saved. It will then stay in create mode.
+ */
+ to_edit_mode: function() {
+ this.onchanges_mutex = new utils.Mutex();
+ this._actualize_mode("edit");
+ },
+ /**
+ * Ask the view to switch to a precise mode if possible. The view is free to
+ * not respect this command if the state of the dataset is not compatible with
+ * the new mode. For example, it is not possible to switch to edit mode if
+ * the current record is not yet saved in database.
+ *
+ * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
+ * undefined the view will test the actual mode to check if it is still consistent
+ * with the dataset state.
+ */
+ _actualize_mode: function(switch_to) {
+ var mode = switch_to || this.get("actual_mode");
+ if (! this.datarecord.id) {
+ mode = "create";
+ } else if (mode === "create") {
+ mode = "edit";
+ }
+ this.render_value_defs = [];
+ this.set({actual_mode: mode});
+ },
+ check_actual_mode: function(source, options) {
+ var self = this;
+ if(this.get("actual_mode") === "view") {
+ self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
+ self.$buttons.find('.oe_form_buttons_edit').hide();
+ self.$buttons.find('.oe_form_buttons_view').show();
+ self.$sidebar.show();
+ } else {
+ self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
+ self.$buttons.find('.oe_form_buttons_edit').show();
+ self.$buttons.find('.oe_form_buttons_view').hide();
+ self.$sidebar.hide();
+ this.autofocus();
+ }
+ },
+ autofocus: function() {
+ if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
+ var fields_order = this.fields_order.slice(0);
+ if (this.default_focus_field) {
+ fields_order.unshift(this.default_focus_field.name);
+ }
+ for (var i = 0; i < fields_order.length; i += 1) {
+ var field = this.fields[fields_order[i]];
+ if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
+ if (field.focus() !== false) {
+ break;
+ }
+ }
+ }
+ }
+ },
+ on_button_save: function(e) {
+ var self = this;
+ $(e.target).attr("disabled", true);
+ return this.save().done(function(result) {
+ self.trigger("save", result);
+ self.reload().then(function() {
+ self.to_view_mode();
+ core.bus.trigger('do_reload_needaction');
+ core.bus.trigger('form_view_saved', self);
+ });
+ }).always(function(){
+ $(e.target).attr("disabled", false);
+ });
+ },
+ on_button_cancel: function(event) {
+ var self = this;
+ if (this.can_be_discarded()) {
+ if (this.get('actual_mode') === 'create') {
+ this.trigger('history_back');
+ } else {
+ this.to_view_mode();
+ $.when.apply(null, this.render_value_defs).then(function(){
+ self.trigger('load_record', self.datarecord);
+ });
+ }
+ }
+ this.trigger('on_button_cancel');
+ return false;
+ },
+ on_button_new: function() {
+ var self = this;
+ this.to_edit_mode();
+ return $.when(this.has_been_loaded).then(function() {
+ if (self.can_be_discarded()) {
+ return self.load_defaults();
+ }
+ });
+ },
+ on_button_edit: function() {
+ return this.to_edit_mode();
+ },
+ on_button_create: function() {
+ this.dataset.index = null;
+ this.do_show();
+ },
+ on_button_duplicate: function() {
+ var self = this;
+ return this.has_been_loaded.then(function() {
+ return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
+ self.record_created(new_id);
+ self.to_edit_mode();
+ });
+ });
+ },
+ on_button_delete: function() {
+ var self = this;
+ var def = $.Deferred();
+ this.has_been_loaded.done(function() {
+ if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
+ self.dataset.unlink([self.datarecord.id]).done(function() {
+ if (self.dataset.size()) {
+ self.execute_pager_action('next');
+ } else {
+ self.do_action('history_back');
+ }
+ def.resolve();
+ });
+ } else {
+ $.async_when().done(function () {
+ def.reject();
+ });
+ }
+ });
+ return def.promise();
+ },
+ can_be_discarded: function() {
+ if (this.$el.is('.oe_form_dirty')) {
+ if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
+ return false;
+ }
+ this.$el.removeClass('oe_form_dirty');
+ }
+ return true;
+ },
+ /**
+ * Triggers saving the form's record. Chooses between creating a new
+ * record or saving an existing one depending on whether the record
+ * already has an id property.
+ *
+ * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
+ * record, should that record be inserted at the start of the dataset (by
+ * default, records are added at the end)
+ */
+ save: function(prepend_on_create) {
+ var self = this;
+ var save_obj = {prepend_on_create: prepend_on_create, ret: null};
+ this.save_list.push(save_obj);
+ return self._process_operations().then(function() {
+ if (save_obj.error)
+ return $.Deferred().reject();
+ return $.when.apply($, save_obj.ret);
+ }).done(function(result) {
+ self.$el.removeClass('oe_form_dirty');
+ });
+ },
+ _process_save: function(save_obj) {
+ var self = this;
+ var prepend_on_create = save_obj.prepend_on_create;
+ try {
+ var form_invalid = false,
+ values = {},
+ first_invalid_field = null,
+ readonly_values = {};
+ for (var f in self.fields) {
+ if (!self.fields.hasOwnProperty(f)) { continue; }
+ f = self.fields[f];
+ if (!f.is_valid()) {
+ form_invalid = true;
+ if (!first_invalid_field) {
+ first_invalid_field = f;
+ }
+ } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
+ // Special case 'id' field, do not save this field
+ // on 'create' : save all non readonly fields
+ // on 'edit' : save non readonly modified fields
+ if (!f.get("readonly")) {
+ values[f.name] = f.get_value(true);
+ } else {
+ readonly_values[f.name] = f.get_value(true);
+ }
+ }
+ }
+ if (form_invalid) {
+ self.set({'display_invalid_fields': true});
+ first_invalid_field.focus();
+ self.on_invalid();
+ return $.Deferred().reject();
+ } else {
+ self.set({'display_invalid_fields': false});
+ var save_deferral;
+ if (!self.datarecord.id) {
+ // Creation save
+ save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
+ return self.record_created(r, prepend_on_create);
+ }, null);
+ } else if (_.isEmpty(values)) {
+ // Not dirty, noop save
+ save_deferral = $.Deferred().resolve({}).promise();
+ } else {
+ // Write save
+ save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
+ return self.record_saved(r);
+ }, null);
+ }
+ return save_deferral;
+ }
+ } catch (e) {
+ console.error(e);
+ return $.Deferred().reject();
+ }
+ },
+ on_invalid: function() {
+ var warnings = _(this.fields).chain()
+ .filter(function (f) { return !f.is_valid(); })
+ .map(function (f) {
+ return _.str.sprintf('<li>%s</li>',
+ _.escape(f.string));
+ }).value();
+ warnings.unshift('<ul>');
+ warnings.push('</ul>');
+ this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
+ },
+ /**
+ * Reload the form after saving
+ *
+ * @param {Object} r result of the write function.
+ */
+ record_saved: function(r) {
+ this.trigger('record_saved', r);
+ if (!r) {
+ // should not happen in the server, but may happen for internal purpose
+ return $.Deferred().reject();
+ }
+ return r;
+ },
+ /**
+ * Updates the form' dataset to contain the new record:
+ *
+ * * Adds the newly created record to the current dataset (at the end by
+ * default)
+ * * Selects that record (sets the dataset's index to point to the new
+ * record's id).
+ * * Updates the pager and sidebar displays
+ *
+ * @param {Object} r
+ * @param {Boolean} [prepend_on_create=false] adds the newly created record
+ * at the beginning of the dataset instead of the end
+ */
+ record_created: function(r, prepend_on_create) {
+ var self = this;
+ if (!r) {
+ // should not happen in the server, but may happen for internal purpose
+ this.trigger('record_created', r);
+ return $.Deferred().reject();
+ } else {
+ this.datarecord.id = r;
+ if (!prepend_on_create) {
+ this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
+ this.dataset.index = this.dataset.ids.length - 1;
+ } else {
+ this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
+ this.dataset.index = 0;
+ }
+ this.do_update_pager();
+ if (this.sidebar) {
+ this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
+ }
+ //openerp.log("The record has been created with id #" + this.datarecord.id);
+ return $.when(this.reload()).then(function () {
+ self.trigger('record_created', r);
+ return _.extend(r, {created: true});
+ });
+ }
+ },
+ on_action: function (action) {
+ console.debug('Executing action', action);
+ },
+ reload: function() {
+ var self = this;
+ return this.reload_mutex.exec(function() {
+ if (self.dataset.index === null || self.dataset.index === undefined) {
+ self.trigger("previous_view");
+ return $.Deferred().reject().promise();
+ }
+ if (self.dataset.index < 0) {
+ return $.when(self.on_button_new());
+ } else {
+ var fields = _.keys(self.fields_view.fields);
+ fields.push('display_name');
+ return self.dataset.read_index(fields,
+ {
+ context: {
+ 'bin_size': true,
+ 'future_display_name': true
+ },
+ check_access_rule: true
+ }).then(function(r) {
+ self.trigger('load_record', r);
+ }).fail(function (){
+ self.do_action('history_back');
+ });
+ }
+ });
+ },
+ get_widgets: function() {
+ return _.filter(this.getChildren(), function(obj) {
+ return obj instanceof common.FormWidget;
+ });
+ },
+ get_fields_values: function() {
+ var values = {};
+ var ids = this.get_selected_ids();
+ values["id"] = ids.length > 0 ? ids[0] : false;
+ _.each(this.fields, function(value_, key) {
+ values[key] = value_.get_value();
+ });
+ return values;
+ },
+ get_selected_ids: function() {
+ var id = this.dataset.ids[this.dataset.index];
+ return id ? [id] : [];
+ },
+ recursive_save: function() {
+ var self = this;
+ return $.when(this.save()).then(function(res) {
+ if (self.dataset.parent_view)
+ return self.dataset.parent_view.recursive_save();
+ });
+ },
+ recursive_reload: function() {
+ var self = this;
+ var pre = $.when();
+ if (self.dataset.parent_view)
+ pre = self.dataset.parent_view.recursive_reload();
+ return pre.then(function() {
+ return self.reload();
+ });
+ },
+ is_dirty: function() {
+ return _.any(this.fields, function (value_) {
+ return value_._dirty_flag;
+ });
+ },
+ is_interactible_record: function() {
+ var id = this.datarecord.id;
+ if (!id) {
+ if (this.options.not_interactible_on_create)
+ return false;
+ } else if (typeof(id) === "string") {
+ if(data.BufferedDataSet.virtual_id_regex.test(id))
+ return false;
+ }
+ return true;
+ },
+ sidebar_eval_context: function () {
+ return $.when(this.build_eval_context());
+ },
+ open_defaults_dialog: function () {
+ var self = this;
+ var display = function (field, value) {
+ var FieldSelection = core.form_widget_registry.get('selection'),
+ FieldMany2One = core.form_widget_registry.get('many2one');
+
+ if (!value) { return value; }
+ if (field instanceof FieldSelection) {
+ return _(field.get('values')).find(function (option) {
+ return option[0] === value;
+ })[1];
+ } else if (field instanceof FieldMany2One) {
+ return field.get_displayed();
+ }
+ return value;
+ };
+ var fields = _.chain(this.fields)
+ .map(function (field) {
+ var value = field.get_value();
+ // ignore fields which are empty, invisible, readonly, o2m
+ // or m2m
+ if (!value
+ || field.get('invisible')
+ || field.get("readonly")
+ || field.field.type === 'one2many'
+ || field.field.type === 'many2many'
+ || field.field.type === 'binary'
+ || field.password) {
+ return false;
+ }
+
+ return {
+ name: field.name,
+ string: field.string,
+ value: value,
+ displayed: display(field, value),
+ };
+ })
+ .compact()
+ .sortBy(function (field) { return field.string; })
+ .value();
+ var conditions = _.chain(self.fields)
+ .filter(function (field) { return field.field.change_default; })
+ .map(function (field) {
+ var value = field.get_value();
+ return {
+ name: field.name,
+ string: field.string,
+ value: value,
+ displayed: display(field, value),
+ };
+ })
+ .value();
+ var d = new Dialog(this, {
+ title: _t("Set Default"),
+ args: {
+ fields: fields,
+ conditions: conditions
+ },
+ buttons: [
+ {text: _t("Close"), click: function () { d.close(); }},
+ {text: _t("Save default"), click: function () {
+ var $defaults = d.$el.find('#formview_default_fields');
+ var field_to_set = $defaults.val();
+ if (!field_to_set) {
+ $defaults.parent().addClass('oe_form_invalid');
+ return;
+ }
+ var condition = d.$el.find('#formview_default_conditions').val(),
+ all_users = d.$el.find('#formview_default_all').is(':checked');
+ new data.DataSet(self, 'ir.values').call(
+ 'set_default', [
+ self.dataset.model,
+ field_to_set,
+ self.fields[field_to_set].get_value(),
+ all_users,
+ true,
+ condition || false
+ ]).done(function () { d.close(); });
+ }}
+ ]
+ });
+ d.template = 'FormView.set_default';
+ d.open();
+ },
+ register_field: function(field, name) {
+ this.fields[name] = field;
+ this.fields_order.push(name);
+ if (JSON.parse(field.node.attrs.default_focus || "0")) {
+ this.default_focus_field = field;
+ }
+
+ field.on('focused', null, this.proxy('widgetFocused'))
+ .on('blurred', null, this.proxy('widgetBlurred'));
+ if (this.get_field_desc(name).translate) {
+ this.translatable_fields.push(field);
+ }
+ field.on('changed_value', this, function() {
+ if (field.is_syntax_valid()) {
+ this.trigger('field_changed:' + name);
+ }
+ if (field._inhibit_on_change_flag) {
+ return;
+ }
+ field._dirty_flag = true;
+ if (field.is_syntax_valid()) {
+ this.do_onchange(field);
+ this.on_form_changed(true);
+ this.do_notify_change();
+ }
+ });
+ },
+ get_field_desc: function(field_name) {
+ return this.fields_view.fields[field_name];
+ },
+ get_field_value: function(field_name) {
+ return this.fields[field_name].get_value();
+ },
+ compute_domain: function(expression) {
+ return data.compute_domain(expression, this.fields);
+ },
+ _build_view_fields_values: function() {
+ var a_dataset = this.dataset;
+ var fields_values = this.get_fields_values();
+ var active_id = a_dataset.ids[a_dataset.index];
+ _.extend(fields_values, {
+ active_id: active_id || false,
+ active_ids: active_id ? [active_id] : [],
+ active_model: a_dataset.model,
+ parent: {}
+ });
+ if (a_dataset.parent_view) {
+ fields_values.parent = a_dataset.parent_view.get_fields_values();
+ }
+ return fields_values;
+ },
+ build_eval_context: function() {
+ var a_dataset = this.dataset;
+ return new data.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
+ },
+});
+
+/**
+ * Interface to be implemented by rendering engines for the form view.
+ */
+var FormRenderingEngineInterface = Class.extend({
+ set_fields_view: function(fields_view) {},
+ set_fields_registry: function(fields_registry) {},
+ render_to: function($el) {},
+});
+
+/**
+ * Default rendering engine for the form view.
+ *
+ * It is necessary to set the view using set_view() before usage.
+ */
+var FormRenderingEngine = FormRenderingEngineInterface.extend({
+ init: function(view) {
+ this.view = view;
+ },
+ set_fields_view: function(fvg) {
+ this.fvg = fvg;
+ this.version = parseFloat(this.fvg.arch.attrs.version);
+ if (isNaN(this.version)) {
+ this.version = 7.0;
+ }
+ },
+ set_tags_registry: function(tags_registry) {
+ this.tags_registry = tags_registry;
+ },
+ set_fields_registry: function(fields_registry) {
+ this.fields_registry = fields_registry;
+ },
+ set_widgets_registry: function(widgets_registry) {
+ this.widgets_registry = widgets_registry;
+ },
+ // Backward compatibility tools, current default version: v7
+ process_version: function() {
+ if (this.version < 7.0) {
+ this.$form.find('form:first').wrapInner('<group col="4"/>');
+ this.$form.find('page').each(function() {
+ if (!$(this).parents('field').length) {
+ $(this).wrapInner('<group col="4"/>');
+ }
+ });
+ }
+ },
+ get_arch_fragment: function() {
+ var doc = $.parseXML(utils.json_node_to_xml(this.fvg.arch)).documentElement;
+ // IE won't allow custom button@type and will revert it to spec default : 'submit'
+ $('button', doc).each(function() {
+ $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
+ });
+ // IE's html parser is also a css parser. How convenient...
+ $('board', doc).each(function() {
+ $(this).attr('layout', $(this).attr('style'));
+ });
+ return $('<div class="oe_form"/>').append(utils.xml_to_str(doc));
+ },
+ render_to: function($target) {
+ var self = this;
+ this.$target = $target;
+
+ this.$form = this.get_arch_fragment();
+
+ this.process_version();
+
+ this.fields_to_init = [];
+ this.tags_to_init = [];
+ this.widgets_to_init = [];
+ this.labels = {};
+ this.process(this.$form);
+
+ this.$form.appendTo(this.$target);
+
+ this.to_replace = [];
+
+ _.each(this.fields_to_init, function($elem) {
+ var name = $elem.attr("name");
+ if (!self.fvg.fields[name]) {
+ throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
+ }
+ var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
+ if (!obj) {
+ throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
+ }
+ var w = new (obj)(self.view, utils.xml_to_json($elem[0]));
+ var $label = self.labels[$elem.attr("name")];
+ if ($label) {
+ w.set_input_id($label.attr("for"));
+ }
+ self.alter_field(w);
+ self.view.register_field(w, $elem.attr("name"));
+ self.to_replace.push([w, $elem]);
+ });
+ _.each(this.tags_to_init, function($elem) {
+ var tag_name = $elem[0].tagName.toLowerCase();
+ var obj = self.tags_registry.get(tag_name);
+ var w = new (obj)(self.view, utils.xml_to_json($elem[0]));
+ self.to_replace.push([w, $elem]);
+ });
+ _.each(this.widgets_to_init, function($elem) {
+ var widget_type = $elem.attr("type");
+ var obj = self.widgets_registry.get(widget_type);
+ var w = new (obj)(self.view, utils.xml_to_json($elem[0]));
+ self.to_replace.push([w, $elem]);
+ });
+ },
+ init_fields: function() {
+ var defs = [];
+ _.each(this.to_replace, function(el) {
+ defs.push(el[0].replace(el[1]));
+ if (el[1].children().length) {
+ el[0].$el.append(el[1].children());
+ }
+ });
+ this.to_replace = [];
+ return $.when.apply($, defs);
+ },
+ render_element: function(template /* dictionaries */) {
+ var dicts = [].slice.call(arguments).slice(1);
+ var dict = _.extend.apply(_, dicts);
+ dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
+ return $(QWeb.render(template, dict));
+ },
+ alter_field: function(field) {
+ },
+ toggle_layout_debugging: function() {
+ if (!this.$target.has('.oe_layout_debug_cell:first').length) {
+ this.$target.find('[title]').removeAttr('title');
+ this.$target.find('.oe_form_group_cell').each(function() {
+ var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
+ $(this).attr('title', text);
+ });
+ }
+ this.$target.toggleClass('oe_layout_debugging');
+ },
+ process: function($tag) {
+ var self = this;
+ var tagname = $tag[0].nodeName.toLowerCase();
+ if (this.tags_registry.contains(tagname)) {
+ this.tags_to_init.push($tag);
+ return (tagname === 'button') ? this.process_button($tag) : $tag;
+ }
+ var fn = self['process_' + tagname];
+ if (fn) {
+ var args = [].slice.call(arguments);
+ args[0] = $tag;
+ return fn.apply(self, args);
+ } else {
+ // generic tag handling, just process children
+ $tag.children().each(function() {
+ self.process($(this));
+ });
+ self.handle_common_properties($tag, $tag);
+ $tag.removeAttr("modifiers");
+ return $tag;
+ }
+ },
+ process_button: function ($button) {
+ var self = this;
+ $button.children().each(function() {
+ self.process($(this));
+ });
+ return $button;
+ },
+ process_widget: function($widget) {
+ this.widgets_to_init.push($widget);
+ return $widget;
+ },
+ process_sheet: function($sheet) {
+ var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
+ this.handle_common_properties($new_sheet, $sheet);
+ var $dst = $new_sheet.find('.oe_form_sheet');
+ $sheet.contents().appendTo($dst);
+ $sheet.before($new_sheet).remove();
+ this.process($new_sheet);
+ },
+ process_form: function($form) {
+ if ($form.find('> sheet').length === 0) {
+ $form.addClass('oe_form_nosheet');
+ }
+ var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
+ this.handle_common_properties($new_form, $form);
+ $form.contents().appendTo($new_form);
+ if ($form[0] === this.$form[0]) {
+ // If root element, replace it
+ this.$form = $new_form;
+ } else {
+ $form.before($new_form).remove();
+ }
+ this.process($new_form);
+ },
+ /*
+ * Used by direct <field> children of a <group> tag only
+ * This method will add the implicit <label...> for every field
+ * in the <group>
+ */
+ preprocess_field: function($field) {
+ var self = this;
+ var name = $field.attr('name'),
+ field_colspan = parseInt($field.attr('colspan'), 10),
+ field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
+
+ if ($field.attr('nolabel') === '1')
+ return;
+ $field.attr('nolabel', '1');
+ var found = false;
+ this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
+ $(el).parents().each(function(unused, tag) {
+ var name = tag.tagName.toLowerCase();
+ if (name === "field" || name in self.tags_registry.map)
+ found = true;
+ });
+ });
+ if (found)
+ return;
+
+ var $label = $('<label/>').attr({
+ 'for' : name,
+ "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
+ "string": $field.attr('string'),
+ "help": $field.attr('help'),
+ "class": $field.attr('class'),
+ });
+ $label.insertBefore($field);
+ if (field_colspan > 1) {
+ $field.attr('colspan', field_colspan - 1);
+ }
+ return $label;
+ },
+ process_field: function($field) {
+ if ($field.parent().is('group')) {
+ // No implicit labels for normal fields, only for <group> direct children
+ var $label = this.preprocess_field($field);
+ if ($label) {
+ this.process($label);
+ }
+ }
+ this.fields_to_init.push($field);
+ return $field;
+ },
+ process_group: function($group) {
+ var self = this;
+ $group.children('field').each(function() {
+ self.preprocess_field($(this));
+ });
+ var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
+ var $table;
+ if ($new_group.first().is('table.oe_form_group')) {
+ $table = $new_group;
+ } else if ($new_group.filter('table.oe_form_group').length) {
+ $table = $new_group.filter('table.oe_form_group').first();
+ } else {
+ $table = $new_group.find('table.oe_form_group').first();
+ }
+
+ var $tr, $td,
+ cols = parseInt($group.attr('col') || 2, 10),
+ row_cols = cols;
+
+ var children = [];
+ $group.children().each(function() {
+ var $child = $(this);
+ var colspan = parseInt($child.attr('colspan') || 1, 10);
+ var tagName = $child[0].tagName.toLowerCase();
+ var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
+ var newline = tagName === 'newline';
+
+ // Note FME: those classes are used in layout debug mode
+ if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
+ $tr.addClass('oe_form_group_row_incomplete');
+ if (newline) {
+ $tr.addClass('oe_form_group_row_newline');
+ }
+ }
+ if (newline) {
+ $tr = null;
+ return;
+ }
+ if (!$tr || row_cols < colspan) {
+ $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
+ row_cols = cols;
+ } else if (tagName==='group') {
+ // When <group> <group/><group/> </group>, we need a spacing between the two groups
+ $td.addClass('oe_group_right');
+ }
+ row_cols -= colspan;
+
+ // invisibility transfer
+ var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
+ var invisible = field_modifiers.invisible;
+ self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
+
+ $tr.append($td.append($child));
+ children.push($child[0]);
+ });
+ if (row_cols && $td) {
+ $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
+ }
+ $group.before($new_group).remove();
+
+ $table.find('> tbody > tr').each(function() {
+ var to_compute = [],
+ row_cols = cols,
+ total = 100;
+ $(this).children().each(function() {
+ var $td = $(this),
+ $child = $td.children(':first');
+ if ($child.attr('cell-class')) {
+ $td.addClass($child.attr('cell-class'));
+ }
+ switch ($child[0].tagName.toLowerCase()) {
+ case 'separator':
+ break;
+ case 'label':
+ if ($child.attr('for')) {
+ $td.attr('width', '1%').addClass('oe_form_group_cell_label');
+ row_cols-= $td.attr('colspan') || 1;
+ total--;
+ }
+ break;
+ default:
+ var width = _.str.trim($child.attr('width') || ''),
+ iwidth = parseInt(width, 10);
+ if (iwidth) {
+ if (width.substr(-1) === '%') {
+ total -= iwidth;
+ width = iwidth + '%';
+ } else {
+ // Absolute width
+ $td.css('min-width', width + 'px');
+ }
+ $td.attr('width', width);
+ $child.removeAttr('width');
+ row_cols-= $td.attr('colspan') || 1;
+ } else {
+ to_compute.push($td);
+ }
+
+ }
+ });
+ if (row_cols) {
+ var unit = Math.floor(total / row_cols);
+ if (!$(this).is('.oe_form_group_row_incomplete')) {
+ _.each(to_compute, function($td) {
+ var width = parseInt($td.attr('colspan'), 10) * unit;
+ $td.attr('width', width + '%');
+ total -= width;
+ });
+ }
+ }
+ });
+ _.each(children, function(el) {
+ self.process($(el));
+ });
+ this.handle_common_properties($new_group, $group);
+ return $new_group;
+ },
+ process_notebook: function($notebook) {
+ var self = this;
+
+ // Extract useful info from the notebook xml declaration
+ var pages = [];
+ $notebook.find('> page').each(function() {
+ var $page = $(this);
+ var page_attrs = $page.getAttributes();
+ page_attrs.id = _.uniqueId('notebook_page_');
+ page_attrs.contents = $page.html();
+ page_attrs.ref = $page; // reference to the current page node in the xml declaration
+ pages.push(page_attrs);
+ });
+
+ // Render the notebook and replace $notebook with it
+ var $new_notebook = self.render_element('FormRenderingNotebook', {'pages': pages});
+ $notebook.before($new_notebook).remove();
+
+ // Bind the invisibility changers and find the page to display
+ var pageid_to_display;
+ _.each(pages, function(page) {
+ var $tab = $new_notebook.find('a[href=#' + page.id + ']').parent();
+ var $content = $new_notebook.find('#' + page.id);
+
+ // Case: <page autofocus="autofocus">;
+ // We attach the autofocus attribute to the node because it can be useful during the
+ // page execution
+ self.attach_node_attr($tab, page.ref, 'autofocus');
+ self.attach_node_attr($content, page.ref, 'autofocus');
+ if (!pageid_to_display && page.autofocus) {
+ // If multiple autofocus, keep the first one
+ pageid_to_display = page.id;
+ }
+
+ // Case: <page attrs="{'invisible': domain}">;
+ self.handle_common_properties($tab, page.ref, common.NotebookInvisibilityChanger);
+ self.handle_common_properties($content, page.ref, common.NotebookInvisibilityChanger);
+ });
+ if (!pageid_to_display) {
+ pageid_to_display = $new_notebook.find('div[role="tabpanel"]:not(.oe_form_invisible):first').attr('id');
+ }
+
+ // Display page. Note: we can't use bootstrap's show function because it is looking for
+ // document attached DOM, and the form view is only attached when everything is processed
+ $new_notebook.find('a[href=#' + pageid_to_display + ']').parent().addClass('active');
+ $new_notebook.find('#' + pageid_to_display).addClass('active');
+
+ self.process($new_notebook.children());
+ self.handle_common_properties($new_notebook, $notebook);
+
+ return $new_notebook;
+ },
+ process_separator: function($separator) {
+ var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
+ $separator.before($new_separator).remove();
+ this.handle_common_properties($new_separator, $separator);
+ return $new_separator;
+ },
+ process_label: function($label) {
+ var name = $label.attr("for"),
+ field_orm = this.fvg.fields[name];
+ var dict = {
+ string: $label.attr('string') || (field_orm || {}).string || '',
+ help: $label.attr('help') || (field_orm || {}).help || '',
+ _for: name ? _.uniqueId('oe-field-input-') : undefined,
+ };
+ var align = parseFloat(dict.align);
+ if (isNaN(align) || align === 1) {
+ align = 'right';
+ } else if (align === 0) {
+ align = 'left';
+ } else {
+ align = 'center';
+ }
+ dict.align = align;
+ var $new_label = this.render_element('FormRenderingLabel', dict);
+ $label.before($new_label).remove();
+ this.handle_common_properties($new_label, $label);
+ if (name) {
+ this.labels[name] = $new_label;
+ }
+ return $new_label;
+ },
+ handle_common_properties: function($new_element, $node, InvisibilityChanger) {
+ var str_modifiers = $node.attr("modifiers") || "{}";
+ var modifiers = JSON.parse(str_modifiers);
+ var ic = null;
+ if (modifiers.invisible !== undefined) {
+ var InvisibilityChangerCls = InvisibilityChanger || common.InvisibilityChanger;
+ ic = new InvisibilityChangerCls(this.view, this.view, modifiers.invisible, $new_element);
+ }
+ $new_element.addClass($node.attr("class") || "");
+ $new_element.attr('style', $node.attr('style'));
+ return {invisibility_changer: ic,};
+ },
+ /**
+ * Attach a node attribute to an html element to be able to use it after
+ * the processing of the page.
+ */
+ attach_node_attr: function($new_element, $node, attr) {
+ $new_element.data(attr, $node.attr(attr));
+ }
+});
+
+core.view_registry.add('form', FormView);
+
+return FormView;
+
+});
+
diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js
new file mode 100644
index 00000000000..69150e4f199
--- /dev/null
+++ b/addons/web/static/src/js/views/form_widgets.js
@@ -0,0 +1,1590 @@
+odoo.define('web.form_widgets', ['web.core', 'web.crash_manager', 'web.data', 'web.datepicker', 'web.Dialog', 'web.form_common', 'web.formats', 'web.framework', 'web.Model', 'web.pyeval', 'web.session', 'web.utils'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var data = require('web.data');
+var datepicker = require('web.datepicker');
+var Dialog = require('web.Dialog');
+var common = require('web.form_common');
+var formats = require('web.formats');
+var framework = require('web.framework');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+var utils = require('web.utils');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+var WidgetButton = common.FormWidget.extend({
+ template: 'WidgetButton',
+ init: function(field_manager, node) {
+ node.attrs.type = node.attrs['data-button-type'];
+ this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
+ this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw";
+ this._super(field_manager, node);
+ this.force_disabled = false;
+ this.string = (this.node.attrs.string || '').replace(/_/g, '');
+ if (JSON.parse(this.node.attrs.default_focus || "0")) {
+ // TODO fme: provide enter key binding to widgets
+ this.view.default_focus_button = this;
+ }
+ if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
+ this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
+ }
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.view.on('view_content_has_changed', this, this.check_disable);
+ this.check_disable();
+ this.$el.click(this.on_click);
+ if (this.node.attrs.help || session.debug) {
+ this.do_attach_tooltip();
+ }
+ this.setupFocus(this.$el);
+ },
+ on_click: function() {
+ var self = this;
+ this.force_disabled = true;
+ this.check_disable();
+ this.execute_action().always(function() {
+ self.force_disabled = false;
+ self.check_disable();
+ });
+ },
+ execute_action: function() {
+ var self = this;
+ var exec_action = function() {
+ if (self.node.attrs.confirm) {
+ var def = $.Deferred();
+ var dialog = new Dialog(this, {
+ title: _t('Confirm'),
+ buttons: [
+ {text: _t("Cancel"), click: function() {
+ this.parents('.modal').modal('hide');
+ }
+ },
+ {text: _t("Ok"), click: function() {
+ var self2 = this;
+ self.on_confirmed().always(function() {
+ self2.parents('.modal').modal('hide');
+ });
+ }
+ }
+ ],
+ }, $('<div/>').text(self.node.attrs.confirm)).open();
+ dialog.on("closing", null, function() {def.resolve();});
+ return def.promise();
+ } else {
+ return self.on_confirmed();
+ }
+ };
+ if (!this.node.attrs.special) {
+ return this.view.recursive_save().then(exec_action);
+ } else {
+ return exec_action();
+ }
+ },
+ on_confirmed: function() {
+ var self = this;
+
+ var context = this.build_context();
+ return this.view.do_execute_action(
+ _.extend({}, this.node.attrs, {context: context}),
+ this.view.dataset, this.view.datarecord.id, function (reason) {
+ if (!_.isObject(reason)) {
+ self.view.recursive_reload();
+ }
+ });
+ },
+ check_disable: function() {
+ var disabled = (this.force_disabled || !this.view.is_interactible_record());
+ this.$el.prop('disabled', disabled);
+ this.$el.css('color', disabled ? 'grey' : '');
+ }
+});
+
+var FieldChar = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: 'FieldChar',
+ widget_class: 'oe_form_field_char',
+ events: {
+ 'change input': 'store_dom_value',
+ },
+ init: function (field_manager, node) {
+ this._super(field_manager, node);
+ this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
+ },
+ initialize_content: function() {
+ this.setupFocus(this.$('input'));
+ },
+ store_dom_value: function () {
+ if (!this.get('effective_readonly')
+ && this.$('input').length
+ && this.is_syntax_valid()) {
+ this.internal_set_value(
+ this.parse_value(
+ this.$('input').val()));
+ }
+ },
+ commit_value: function () {
+ this.store_dom_value();
+ return this._super();
+ },
+ render_value: function() {
+ var show_value = this.format_value(this.get('value'), '');
+ if (!this.get("effective_readonly")) {
+ this.$el.find('input').val(show_value);
+ } else {
+ if (this.password) {
+ show_value = new Array(show_value.length + 1).join('*');
+ }
+ this.$(".oe_form_char_content").text(show_value);
+ }
+ },
+ is_syntax_valid: function() {
+ if (!this.get("effective_readonly") && this.$("input").size() > 0) {
+ try {
+ this.parse_value(this.$('input').val(), '');
+ return true;
+ } catch(e) {
+ return false;
+ }
+ }
+ return true;
+ },
+ parse_value: function(val, def) {
+ return formats.parse_value(val, this, def);
+ },
+ format_value: function(val, def) {
+ return formats.format_value(val, this, def);
+ },
+ is_false: function() {
+ return this.get('value') === '' || this._super();
+ },
+ focus: function() {
+ var input = this.$('input:first')[0];
+ return input ? input.focus() : false;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ this.$('input').css({
+ height: height,
+ width: width
+ });
+ }
+});
+
+var KanbanSelection = FieldChar.extend({
+ init: function (field_manager, node) {
+ this._super(field_manager, node);
+ },
+ start: function () {
+ var self = this;
+ this.states = [];
+ this._super.apply(this, arguments);
+ // hook on form view content changed: recompute the states, because it may be related to the current stage
+ this.getParent().on('view_content_has_changed', self, function () {
+ self.render_value();
+ });
+ },
+ prepare_dropdown_selection: function() {
+ var self = this;
+ var _data = [];
+ var fetch_stage;
+ var selection = this.field.selection || [];
+ var stage_id = _.isArray(this.view.datarecord.stage_id) ? this.view.datarecord.stage_id[0] : this.view.datarecord.stage_id;
+ var legend_field = this.options && this.options.states_legend_field || false;
+ var fields_to_read = _.map(
+ this.options && this.options.states_legend || {},
+ function (value, key, list) { return value; });
+ if (legend_field && fields_to_read && stage_id) {
+ fetch_stage = new data.DataSet(
+ this,
+ self.view.fields[legend_field].field.relation).read_ids([stage_id],
+ fields_to_read);
+ }
+ else { fetch_stage = $.Deferred().resolve(false); }
+ return $.when(fetch_stage).then(function (stage_data) {
+ _.map(selection, function(res) {
+ var value = {
+ 'name': res[0],
+ 'tooltip': res[1],
+ 'state_name': res[1],
+ };
+ if (stage_data && stage_data[0][self.options.states_legend[res[0]]]) {
+ value['state_name'] = stage_data[0][self.options.states_legend[res[0]]];
+ value['tooltip'] = stage_data[0][self.options.states_legend[res[0]]];
+ }
+ if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
+ else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
+ else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
+ _data.push(value);
+ });
+ return _data;
+ });
+ },
+ render_value: function() {
+ var self = this;
+ var dd_fetched = this.prepare_dropdown_selection();
+ return $.when(dd_fetched).then(function (states) {
+ self.states = states;
+ self.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
+ self.$el.find('li').on('click', self.set_kanban_selection.bind(self));
+ });
+ },
+ /* setting the value: in view mode, perform an asynchronous call and reload
+ the form view; in edit mode, use set_value to save the new value that will
+ be written when saving the record. */
+ set_kanban_selection: function (ev) {
+ var self = this;
+ var li = $(ev.target).closest('li');
+ if (li.length) {
+ var value = String(li.data('value'));
+ if (this.view.get('actual_mode') == 'view') {
+ var write_values = {};
+ write_values[self.name] = value;
+ return this.view.dataset._model.call(
+ 'write', [
+ [this.view.datarecord.id],
+ write_values,
+ self.view.dataset.get_context()
+ ]).done(self.reload_record.bind(self));
+ }
+ else {
+ return this.set_value(value);
+ }
+ }
+ },
+ reload_record: function() {
+ this.view.reload();
+ },
+});
+
+var Priority = FieldChar.extend({
+ init: function (field_manager, node) {
+ this._super(field_manager, node);
+ },
+ prepare_priority: function() {
+ var self = this;
+ var selection = this.field.selection || [];
+ var init_value = selection && selection[0][0] || 0;
+ var data = _.map(selection.slice(1), function(element, index) {
+ var value = {
+ 'value': element[0],
+ 'name': element[1],
+ 'click_value': element[0],
+ };
+ if (index === 0 && self.get('value') === element[0]) {
+ value['click_value'] = init_value;
+ }
+ return value;
+ });
+ return data;
+ },
+ render_value: function() {
+ this.priorities = this.prepare_priority();
+ this.$el.html(QWeb.render("Priority", {'widget': this}));
+ if (!this.get('readonly')){
+ this.$el.find('li').on('click', this.set_priority.bind(this));
+ }
+ },
+ /* setting the value: in view mode, perform an asynchronous call and reload
+ the form view; in edit mode, use set_value to save the new value that will
+ be written when saving the record. */
+ set_priority: function (ev) {
+ var self = this;
+ var li = $(ev.target).closest('li');
+ if (li.length) {
+ var value = String(li.data('value'));
+ if (this.view.get('actual_mode') == 'view') {
+ var write_values = {};
+ write_values[self.name] = value;
+ return this.view.dataset._model.call(
+ 'write', [
+ [this.view.datarecord.id],
+ write_values,
+ self.view.dataset.get_context()
+ ]).done(self.reload_record.bind(self));
+ }
+ else {
+ return this.set_value(value);
+ }
+ }
+
+ },
+ reload_record: function() {
+ this.view.reload();
+ },
+});
+
+var FieldID = FieldChar.extend({
+ process_modifiers: function () {
+ this._super();
+ this.set({ readonly: true });
+ },
+});
+
+var FieldEmail = FieldChar.extend({
+ template: 'FieldEmail',
+ initialize_content: function() {
+ this._super();
+ var $button = this.$el.find('button');
+ $button.click(this.on_button_clicked);
+ this.setupFocus($button);
+ },
+ render_value: function() {
+ if (!this.get("effective_readonly")) {
+ this._super();
+ } else {
+ this.$el.find('a')
+ .attr('href', 'mailto:' + this.get('value'))
+ .text(this.get('value') || '');
+ }
+ },
+ on_button_clicked: function() {
+ if (!this.get('value') || !this.is_syntax_valid()) {
+ this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
+ } else {
+ location.href = 'mailto:' + this.get('value');
+ }
+ }
+});
+
+var FieldUrl = FieldChar.extend({
+ template: 'FieldUrl',
+ initialize_content: function() {
+ this._super();
+ var $button = this.$el.find('button');
+ $button.click(this.on_button_clicked);
+ this.setupFocus($button);
+ },
+ render_value: function() {
+ if (!this.get("effective_readonly")) {
+ this._super();
+ } else {
+ var tmp = this.get('value');
+ var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
+ if (!s) {
+ tmp = "http://" + this.get('value');
+ }
+ var text = this.get('value') ? this.node.attrs.text || tmp : '';
+ this.$el.find('a').attr('href', tmp).text(text);
+ }
+ },
+ on_button_clicked: function() {
+ if (!this.get('value')) {
+ this.do_warn(_t("Resource Error"), _t("This resource is empty"));
+ } else {
+ var url = $.trim(this.get('value'));
+ if(/^www\./i.test(url))
+ url = 'http://'+url;
+ window.open(url);
+ }
+ }
+});
+
+var FieldFloat = FieldChar.extend({
+ is_field_number: true,
+ widget_class: 'oe_form_field_float',
+ init: function (field_manager, node) {
+ this._super(field_manager, node);
+ this.internal_set_value(0);
+ if (this.node.attrs.digits) {
+ this.digits = this.node.attrs.digits;
+ } else {
+ this.digits = this.field.digits;
+ }
+ },
+ set_value: function(value_) {
+ if (value_ === false || value_ === undefined) {
+ // As in GTK client, floats default to 0
+ value_ = 0;
+ }
+ if (this.digits !== undefined && this.digits.length === 2) {
+ value_ = utils.round_decimals(value_, this.digits[1]);
+ }
+ this._super.apply(this, [value_]);
+ },
+ focus: function () {
+ var $input = this.$('input:first');
+ return $input.length ? $input.select() : false;
+ }
+});
+
+var FieldCharDomain = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ render_value: function() {
+ var self = this;
+ this.$el.html(QWeb.render("FieldCharDomain", {widget: this}));
+ if (this.get('value')) {
+ var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
+ var domain = pyeval.eval('domain', this.get('value'));
+ var ds = new data.DataSetStatic(self, model, self.build_context());
+ ds.call('search_count', [domain]).then(function (results) {
+ $('.oe_domain_count', self.$el).text(results + ' records selected');
+ if (self.get('effective_readonly')) {
+ $('button span', self.$el).text(' See selection');
+ }
+ else {
+ $('button span', self.$el).text(' Change selection');
+ }
+ });
+ } else {
+ $('.oe_domain_count', this.$el).text('0 record selected');
+ $('button span', this.$el).text(' Select records');
+ }
+ this.$('.select_records').on('click', self.on_click);
+ },
+ on_click: function(event) {
+ event.preventDefault();
+ var self = this;
+ var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
+
+ var options = {
+ title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
+ readonly: this.get('effective_readonly'),
+ disable_multiple_selection: this.get('effective_readonly'),
+ no_create: this.get('effective_readonly'),
+ };
+ var domain = this.get('value');
+ var popup = new common.DomainEditorPopup(this);
+ popup.select_element(model, options, domain, {});
+ popup.on("elements_selected", self, function(selected_ids) {
+ if (!self.get('effective_readonly')) {
+ self.set_value(popup.get_domain(selected_ids));
+ }
+ });
+ },
+});
+
+var FieldDatetime = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: "FieldDatetime",
+ build_widget: function() {
+ return new datepicker.DateTimeWidget(this);
+ },
+ destroy_content: function() {
+ if (this.datewidget) {
+ this.datewidget.destroy();
+ this.datewidget = undefined;
+ }
+ },
+ initialize_content: function() {
+ if (!this.get("effective_readonly")) {
+ this.datewidget = this.build_widget();
+ this.datewidget.on('datetime_changed', this, _.bind(function() {
+ this.internal_set_value(this.datewidget.get_value());
+ }, this));
+ this.datewidget.appendTo(this.$el);
+ this.setupFocus(this.datewidget.$input);
+ }
+ },
+ render_value: function() {
+ if (!this.get("effective_readonly")) {
+ this.datewidget.set_value(this.get('value'));
+ } else {
+ this.$el.text(formats.format_value(this.get('value'), this, ''));
+ }
+ },
+ is_syntax_valid: function() {
+ if (!this.get("effective_readonly") && this.datewidget) {
+ return this.datewidget.is_valid_();
+ }
+ return true;
+ },
+ is_false: function() {
+ return this.get('value') === '' || this._super();
+ },
+ focus: function() {
+ var input = this.datewidget && this.datewidget.$input[0];
+ return input ? input.focus() : false;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ if (!this.get("effective_readonly")) {
+ this.datewidget.$input.css('height', height);
+ }
+ }
+});
+
+var FieldDate = FieldDatetime.extend({
+ template: "FieldDate",
+ build_widget: function() {
+ return new datepicker.DateWidget(this);
+ }
+});
+
+var FieldText = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: 'FieldText',
+ events: {
+ 'keyup': function (e) {
+ if (e.which === $.ui.keyCode.ENTER) {
+ e.stopPropagation();
+ }
+ },
+ 'keypress': function (e) {
+ if (e.which === $.ui.keyCode.ENTER) {
+ e.stopPropagation();
+ }
+ },
+ 'change textarea': 'store_dom_value',
+ },
+ initialize_content: function() {
+ if (! this.get("effective_readonly")) {
+ this.$textarea = this.$el.find('textarea');
+ this.auto_sized = false;
+ this.default_height = this.$textarea.css('height');
+ if (this.default_height === '0px') this.default_height = '90px';
+ if (this.get("effective_readonly")) {
+ this.$textarea.attr('disabled', 'disabled');
+ }
+ this.setupFocus(this.$textarea);
+ } else {
+ this.$textarea = undefined;
+ }
+ },
+ commit_value: function () {
+ if (! this.get("effective_readonly") && this.$textarea) {
+ this.store_dom_value();
+ }
+ return this._super();
+ },
+ store_dom_value: function () {
+ this.internal_set_value(formats.parse_value(this.$textarea.val(), this));
+ },
+ render_value: function() {
+ if (! this.get("effective_readonly")) {
+ var show_value = formats.format_value(this.get('value'), this, '');
+ if (show_value === '') {
+ this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
+ }
+ this.$textarea.val(show_value);
+ if (! this.auto_sized) {
+ this.auto_sized = true;
+ this.$textarea.autosize();
+ } else {
+ this.$textarea.trigger("autosize");
+ }
+ } else {
+ var txt = this.get("value") || '';
+ this.$(".oe_form_text_content").text(txt);
+ }
+ },
+ is_syntax_valid: function() {
+ if (!this.get("effective_readonly") && this.$textarea) {
+ try {
+ formats.parse_value(this.$textarea.val(), this, '');
+ return true;
+ } catch(e) {
+ return false;
+ }
+ }
+ return true;
+ },
+ is_false: function() {
+ return this.get('value') === '' || this._super();
+ },
+ focus: function($el) {
+ var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
+ return input ? input.focus() : false;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ if (!this.get("effective_readonly") && this.$textarea) {
+ this.$textarea.css({
+ width: width,
+ minHeight: height
+ });
+ }
+ },
+});
+
+/**
+ * FieldTextHtml Widget
+ * Intended for FieldText widgets meant to display HTML content. This
+ * widget will instantiate the CLEditor (see cleditor in static/src/lib)
+ * To find more information about CLEditor configutation: go to
+ * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
+ */
+var FieldTextHtml = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: 'FieldTextHtml',
+ init: function() {
+ this._super.apply(this, arguments);
+ },
+ initialize_content: function() {
+ var self = this;
+ if (! this.get("effective_readonly")) {
+ self._updating_editor = false;
+ this.$textarea = this.$el.find('textarea');
+ var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
+ var height = ((this.node.attrs || {}).editor_height || 250);
+ this.$textarea.cleditor({
+ width: width, // width not including margins, borders or padding
+ height: height, // height not including margins, borders or padding
+ controls: // controls to add to the toolbar
+ "bold italic underline strikethrough " +
+ "| removeformat | bullets numbering | outdent " +
+ "indent | link unlink | source",
+ bodyStyle: // style to assign to document body contained within the editor
+ "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
+ });
+ this.$cleditor = this.$textarea.cleditor()[0];
+ this.$cleditor.change(function() {
+ if (! self._updating_editor) {
+ self.$cleditor.updateTextArea();
+ self.internal_set_value(self.$textarea.val());
+ }
+ });
+ if (this.field.translate) {
+ var $img = $('<img class="oe_field_translate oe_input_icon" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>')
+ .click(this.on_translate);
+ this.$cleditor.$toolbar.append($img);
+ }
+ }
+ },
+ render_value: function() {
+ if (! this.get("effective_readonly")) {
+ this.$textarea.val(this.get('value') || '');
+ this._updating_editor = true;
+ this.$cleditor.updateFrame();
+ this._updating_editor = false;
+ } else {
+ this.$el.html(this.get('value'));
+ }
+ },
+});
+
+var FieldBoolean = common.AbstractField.extend({
+ template: 'FieldBoolean',
+ start: function() {
+ var self = this;
+ this.$checkbox = $("input", this.$el);
+ this.setupFocus(this.$checkbox);
+ this.$el.click(_.bind(function() {
+ this.internal_set_value(this.$checkbox.is(':checked'));
+ }, this));
+ var check_readonly = function() {
+ self.$checkbox.prop('disabled', self.get("effective_readonly"));
+ self.click_disabled_boolean();
+ };
+ this.on("change:effective_readonly", this, check_readonly);
+ check_readonly.call(this);
+ this._super.apply(this, arguments);
+ },
+ render_value: function() {
+ this.$checkbox[0].checked = this.get('value');
+ },
+ focus: function() {
+ var input = this.$checkbox && this.$checkbox[0];
+ return input ? input.focus() : false;
+ },
+ click_disabled_boolean: function(){
+ var $disabled = this.$el.find('input[type=checkbox]:disabled');
+ $disabled.each(function (){
+ $(this).next('div').remove();
+ $(this).closest("span").append($('<div class="boolean"></div>'));
+ });
+ }
+});
+
+/**
+ The progressbar field expect a float from 0 to 100.
+*/
+var FieldProgressBar = common.AbstractField.extend({
+ template: 'FieldProgressBar',
+ render_value: function() {
+ this.$el.progressbar({
+ value: this.get('value') || 0,
+ disabled: this.get("effective_readonly")
+ });
+ var formatted_value = formats.format_value(this.get('value') || 0, { type : 'float' });
+ this.$('span').html(formatted_value + '%');
+ }
+});
+
+/**
+ The PercentPie field expect a float from 0 to 100.
+*/
+var FieldPercentPie = common.AbstractField.extend({
+ template: 'FieldPercentPie',
+
+ render_value: function() {
+ var value = this.get('value'),
+ formatted_value = Math.round(value || 0) + '%',
+ svg = this.$('svg')[0];
+
+ svg.innerHTML = "";
+ nv.addGraph(function() {
+ var width = 42, height = 42;
+ var chart = nv.models.pieChart()
+ .width(width)
+ .height(height)
+ .margin({top: 0, right: 0, bottom: 0, left: 0})
+ .donut(true)
+ .showLegend(false)
+ .showLabels(false)
+ .tooltips(false)
+ .color(['#7C7BAD','#DDD'])
+ .donutRatio(0.62);
+
+ d3.select(svg)
+ .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
+ .transition()
+ .call(chart)
+ .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
+
+ d3.select(svg)
+ .append("text")
+ .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
+ .style({"font-size": "10px", "font-weight": "bold"})
+ .text(formatted_value);
+
+ return chart;
+ });
+
+ }
+});
+
+/**
+ The FieldBarChart expectsa list of values (indeed)
+*/
+var FieldBarChart = common.AbstractField.extend({
+ template: 'FieldBarChart',
+
+ render_value: function() {
+ var value = JSON.parse(this.get('value'));
+ var svg = this.$('svg')[0];
+ svg.innerHTML = "";
+ nv.addGraph(function() {
+ var width = 34, height = 34;
+ var chart = nv.models.discreteBarChart()
+ .x(function (d) { return d.tooltip; })
+ .y(function (d) { return d.value; })
+ .width(width)
+ .height(height)
+ .margin({top: 0, right: 0, bottom: 0, left: 0})
+ .tooltips(false)
+ .showValues(false)
+ .transitionDuration(350)
+ .showXAxis(false)
+ .showYAxis(false);
+
+ d3.select(svg)
+ .datum([{key: 'values', values: value}])
+ .transition()
+ .call(chart)
+ .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
+
+ nv.utils.windowResize(chart.update);
+
+ return chart;
+ });
+
+ }
+});
+
+
+var FieldSelection = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: 'FieldSelection',
+ events: {
+ 'change select': 'store_dom_value',
+ },
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.set("value", false);
+ this.set("values", []);
+ this.records_orderer = new utils.DropMisordered();
+ this.field_manager.on("view_content_has_changed", this, function() {
+ var domain = new data.CompoundDomain(this.build_domain()).eval();
+ if (! _.isEqual(domain, this.get("domain"))) {
+ this.set("domain", domain);
+ }
+ });
+ },
+ initialize_field: function() {
+ common.ReinitializeFieldMixin.initialize_field.call(this);
+ this.on("change:domain", this, this.query_values);
+ this.set("domain", new data.CompoundDomain(this.build_domain()).eval());
+ this.on("change:values", this, this.render_value);
+ },
+ query_values: function() {
+ var self = this;
+ var def;
+ if (this.field.type === "many2one") {
+ var model = new Model(this.field.relation);
+ def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
+ } else {
+ var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
+ def = $.when(values);
+ }
+ this.records_orderer.add(def).then(function(values) {
+ if (! _.isEqual(values, self.get("values"))) {
+ self.set("values", values);
+ }
+ });
+ },
+ initialize_content: function() {
+ // Flag indicating whether we're in an event chain containing a change
+ // event on the select, in order to know what to do on keyup[RETURN]:
+ // * If the user presses [RETURN] as part of changing the value of a
+ // selection, we should just let the value change and not let the
+ // event broadcast further (e.g. to validating the current state of
+ // the form in editable list view, which would lead to saving the
+ // current row or switching to the next one)
+ // * If the user presses [RETURN] with a select closed (side-effect:
+ // also if the user opened the select and pressed [RETURN] without
+ // changing the selected value), takes the action as validating the
+ // row
+ var ischanging = false;
+ var $select = this.$el.find('select')
+ .change(function () { ischanging = true; })
+ .click(function () { ischanging = false; })
+ .keyup(function (e) {
+ if (e.which !== 13 || !ischanging) { return; }
+ e.stopPropagation();
+ ischanging = false;
+ });
+ this.setupFocus($select);
+ },
+ commit_value: function () {
+ this.store_dom_value();
+ return this._super();
+ },
+ store_dom_value: function () {
+ if (!this.get('effective_readonly') && this.$('select').length) {
+ var val = JSON.parse(this.$('select').val());
+ this.internal_set_value(val);
+ }
+ },
+ set_value: function(value_) {
+ value_ = value_ === null ? false : value_;
+ value_ = value_ instanceof Array ? value_[0] : value_;
+ this._super(value_);
+ },
+ render_value: function() {
+ var values = this.get("values");
+ values = [[false, this.node.attrs.placeholder || '']].concat(values);
+ var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
+ if (! found) {
+ found = [this.get("value"), _t('Unknown')];
+ values = [found].concat(values);
+ }
+ if (! this.get("effective_readonly")) {
+ this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
+ this.$("select").val(JSON.stringify(found[0]));
+ } else {
+ this.$el.text(found[1]);
+ }
+ },
+ focus: function() {
+ var input = this.$('select:first')[0];
+ return input ? input.focus() : false;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ this.$('select').css({
+ height: height,
+ width: width
+ });
+ }
+});
+
+var FieldRadio = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: 'FieldRadio',
+ events: {
+ 'click input': 'click_change_value'
+ },
+ init: function(field_manager, node) {
+ /* Radio button widget: Attributes options:
+ * - "horizontal" to display in column
+ * - "no_radiolabel" don't display text values
+ */
+ this._super(field_manager, node);
+ this.selection = _.clone(this.field.selection) || [];
+ this.domain = false;
+ this.uniqueId = _.uniqueId("radio");
+ },
+ initialize_content: function () {
+ this.on("change:effective_readonly", this, this.render_value);
+ this.field_manager.on("view_content_has_changed", this, this.get_selection);
+ this.get_selection();
+ },
+ click_change_value: function (event) {
+ var val = $(event.target).val();
+ val = this.field.type == "selection" ? val : +val;
+ if (val !== this.get_value()) {
+ this.set_value(val);
+ }
+ },
+ /** Get the selection and render it
+ * selection: [[identifier, value_to_display], ...]
+ * For selection fields: this is directly given by this.field.selection
+ * For many2one fields: perform a search on the relation of the many2one field
+ */
+ get_selection: function() {
+ var self = this;
+ var selection = [];
+ var def = $.Deferred();
+ if (self.field.type == "many2one") {
+ var domain = pyeval.eval('domain', this.build_domain()) || [];
+ if (! _.isEqual(self.domain, domain)) {
+ self.domain = domain;
+ var ds = new data.DataSetStatic(self, self.field.relation, self.build_context());
+ ds.call('search', [self.domain])
+ .then(function (records) {
+ ds.name_get(records).then(function (records) {
+ selection = records;
+ def.resolve();
+ });
+ });
+ } else {
+ selection = self.selection;
+ def.resolve();
+ }
+ }
+ else if (self.field.type == "selection") {
+ selection = self.field.selection || [];
+ def.resolve();
+ }
+ return def.then(function () {
+ if (! _.isEqual(selection, self.selection)) {
+ self.selection = _.clone(selection);
+ self.renderElement();
+ self.render_value();
+ }
+ });
+ },
+ set_value: function (value_) {
+ if (this.field.type == "selection") {
+ value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
+ }
+ else if (!this.selection.length) {
+ this.selection = [value_];
+ }
+
+ this._super(value_);
+ },
+ get_value: function () {
+ var value = this.get('value');
+ return value instanceof Array ? value[0] : value;
+ },
+ render_value: function () {
+ var self = this;
+ this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
+ this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
+ this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
+ }
+});
+
+var FieldReference = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: 'FieldReference',
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.reference_ready = true;
+ },
+ destroy_content: function() {
+ if (this.fm) {
+ this.fm.destroy();
+ this.fm = undefined;
+ }
+ },
+ initialize_content: function() {
+ var self = this;
+ var fm = new common.DefaultFieldManager(this);
+ this.fm = fm;
+ fm.extend_field_desc({
+ "selection": {
+ selection: this.field_manager.get_field_desc(this.name).selection,
+ type: "selection",
+ },
+ "m2o": {
+ relation: null,
+ type: "many2one",
+ },
+ });
+ this.selection = new FieldSelection(fm, { attrs: {
+ name: 'selection',
+ modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
+ }});
+ this.selection.on("change:value", this, this.on_selection_changed);
+ this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
+ this.selection
+ .on('focused', null, function () {self.trigger('focused');})
+ .on('blurred', null, function () {self.trigger('blurred');});
+
+ var FieldMany2One = core.form_widget_registry.get('many2one');
+ this.m2o = new FieldMany2One(fm, { attrs: {
+ name: 'Referenced Document',
+ modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
+ }});
+ this.m2o.on("change:value", this, this.data_changed);
+ this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
+ this.m2o
+ .on('focused', null, function () {self.trigger('focused');})
+ .on('blurred', null, function () {self.trigger('blurred');});
+ },
+ on_selection_changed: function() {
+ if (this.reference_ready) {
+ this.internal_set_value([this.selection.get_value(), false]);
+ this.render_value();
+ }
+ },
+ data_changed: function() {
+ if (this.reference_ready) {
+ this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
+ }
+ },
+ set_value: function(val) {
+ if (val) {
+ val = val.split(',');
+ val[0] = val[0] || false;
+ val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
+ }
+ this._super(val || [false, false]);
+ },
+ get_value: function() {
+ return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
+ },
+ render_value: function() {
+ this.reference_ready = false;
+ if (!this.get("effective_readonly")) {
+ this.selection.set_value(this.get('value')[0]);
+ }
+ this.m2o.field.relation = this.get('value')[0];
+ this.m2o.set_value(this.get('value')[1]);
+ this.m2o.$el.toggle(!!this.get('value')[0]);
+ this.reference_ready = true;
+ },
+});
+
+var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ init: function(field_manager, node) {
+ var self = this;
+ this._super(field_manager, node);
+ this.binary_value = false;
+ this.useFileAPI = !!window.FileReader;
+ this.max_upload_size = 25 * 1024 * 1024; // 25Mo
+ if (!this.useFileAPI) {
+ this.fileupload_id = _.uniqueId('oe_fileupload');
+ $(window).on(this.fileupload_id, function() {
+ var args = [].slice.call(arguments).slice(1);
+ self.on_file_uploaded.apply(self, args);
+ });
+ }
+ },
+ stop: function() {
+ if (!this.useFileAPI) {
+ $(window).off(this.fileupload_id);
+ }
+ this._super.apply(this, arguments);
+ },
+ initialize_content: function() {
+ var self= this;
+ this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
+ this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
+ this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
+ this.$el.find('.oe_form_binary_file_edit').click(function(event){
+ self.$el.find('input.oe_form_binary_file').click();
+ });
+ },
+ on_file_change: function(e) {
+ var self = this;
+ var file_node = e.target;
+ if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
+ if (this.useFileAPI) {
+ var file = file_node.files[0];
+ if (file.size > this.max_upload_size) {
+ var msg = _t("The selected file exceed the maximum file size of %s.");
+ core.bus.trigger('display_notification_warning', _t("File upload"), _.str.sprintf(msg, utils.human_size(this.max_upload_size)));
+ return false;
+ }
+ var filereader = new FileReader();
+ filereader.readAsDataURL(file);
+ filereader.onloadend = function(upload) {
+ var data = upload.target.result;
+ data = data.split(',')[1];
+ self.on_file_uploaded(file.size, file.name, file.type, data);
+ };
+ } else {
+ this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
+ this.$el.find('form.oe_form_binary_form').submit();
+ }
+ this.$el.find('.oe_form_binary_progress').show();
+ this.$el.find('.oe_form_binary').hide();
+ }
+ },
+ on_file_uploaded: function(size, name, content_type, file_base64) {
+ if (size === false) {
+ this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
+ // TODO: use openerp web crashmanager
+ console.warn("Error while uploading file : ", name);
+ } else {
+ this.filename = name;
+ this.on_file_uploaded_and_valid.apply(this, arguments);
+ }
+ this.$el.find('.oe_form_binary_progress').hide();
+ this.$el.find('.oe_form_binary').show();
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ },
+ on_save_as: function(ev) {
+ var value = this.get('value');
+ if (!value) {
+ this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
+ ev.stopPropagation();
+ } else {
+ framework.blockUI();
+ var c = crash_manager;
+ var filename_fieldname = this.node.attrs.filename;
+ var filename_field = this.view.fields && this.view.fields[filename_fieldname];
+ this.session.get_file({
+ url: '/web/binary/saveas_ajax',
+ data: {data: JSON.stringify({
+ model: this.view.dataset.model,
+ id: (this.view.datarecord.id || ''),
+ field: this.name,
+ filename_field: (filename_fieldname || ''),
+ data: utils.is_bin_size(value) ? null : value,
+ filename: filename_field ? filename_field.get('value') : null,
+ context: this.view.dataset.get_context()
+ })},
+ complete: framework.unblockUI,
+ error: c.rpc_error.bind(c)
+ });
+ ev.stopPropagation();
+ return false;
+ }
+ },
+ set_filename: function(value) {
+ var filename = this.node.attrs.filename;
+ if (filename) {
+ var tmp = {};
+ tmp[filename] = value;
+ this.field_manager.set_values(tmp);
+ }
+ },
+ on_clear: function() {
+ if (this.get('value') !== false) {
+ this.binary_value = false;
+ this.internal_set_value(false);
+ }
+ return false;
+ }
+});
+
+var FieldBinaryFile = FieldBinary.extend({
+ template: 'FieldBinaryFile',
+ initialize_content: function() {
+ this._super();
+ if (this.get("effective_readonly")) {
+ var self = this;
+ this.$el.find('a').click(function(ev) {
+ if (self.get('value')) {
+ self.on_save_as(ev);
+ }
+ return false;
+ });
+ }
+ },
+ render_value: function() {
+ var show_value;
+ if (!this.get("effective_readonly")) {
+ if (this.node.attrs.filename) {
+ show_value = this.view.datarecord[this.node.attrs.filename] || '';
+ } else {
+ show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
+ }
+ this.$el.find('input').eq(0).val(show_value);
+ } else {
+ this.$el.find('a').toggle(!!this.get('value'));
+ if (this.get('value')) {
+ show_value = _t("Download");
+ if (this.view)
+ show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
+ this.$el.find('a').text(show_value);
+ }
+ }
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ this.binary_value = true;
+ this.internal_set_value(file_base64);
+ var show_value = name + " (" + utils.human_size(size) + ")";
+ this.$el.find('input').eq(0).val(show_value);
+ this.set_filename(name);
+ },
+ on_clear: function() {
+ this._super.apply(this, arguments);
+ this.$el.find('input').eq(0).val('');
+ this.set_filename('');
+ }
+});
+
+var FieldBinaryImage = FieldBinary.extend({
+ template: 'FieldBinaryImage',
+ placeholder: "/web/static/src/img/placeholder.png",
+ render_value: function() {
+ var self = this;
+ var url;
+ this.session = session;
+ if (this.get('value') && !utils.is_bin_size(this.get('value'))) {
+ url = 'data:image/png;base64,' + this.get('value');
+ } else if (this.get('value')) {
+ var id = JSON.stringify(this.view.datarecord.id || null);
+ var field = this.name;
+ if (this.options.preview_image)
+ field = this.options.preview_image;
+ url = session.url('/web/binary/image', {
+ model: this.view.dataset.model,
+ id: id,
+ field: field,
+ t: (new Date().getTime()),
+ });
+ } else {
+ url = this.placeholder;
+ }
+ var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
+ $($img).click(function(e) {
+ if(self.view.get("actual_mode") == "view") {
+ var $button = $(".oe_form_button_edit");
+ $button.openerpBounce();
+ e.stopPropagation();
+ }
+ });
+ this.$el.find('> img').remove();
+ this.$el.prepend($img);
+ $img.load(function() {
+ if (! self.options.size)
+ return;
+ $img.css("max-width", "" + self.options.size[0] + "px");
+ $img.css("max-height", "" + self.options.size[1] + "px");
+ });
+ $img.on('error', function() {
+ $img.attr('src', self.placeholder);
+ core.bus.trigger('display_notification_warning', _t("Image"), _t("Could not display the selected image."));
+ });
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ this.internal_set_value(file_base64);
+ this.binary_value = true;
+ this.render_value();
+ this.set_filename(name);
+ },
+ on_clear: function() {
+ this._super.apply(this, arguments);
+ this.render_value();
+ this.set_filename('');
+ },
+ set_value: function(value_){
+ var changed = value_ !== this.get_value();
+ this._super.apply(this, arguments);
+ // By default, on binary images read, the server returns the binary size
+ // This is possible that two images have the exact same size
+ // Therefore we trigger the change in case the image value hasn't changed
+ // So the image is re-rendered correctly
+ if (!changed){
+ this.trigger("change:value", this, {
+ oldValue: value_,
+ newValue: value_
+ });
+ }
+ }
+});
+
+var FieldStatus = common.AbstractField.extend({
+ template: "FieldStatus",
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
+ this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
+ this.set({value: false});
+ this.selection = {'unfolded': [], 'folded': []};
+ this.set("selection", {'unfolded': [], 'folded': []});
+ this.selection_dm = new utils.DropMisordered();
+ this.dataset = new data.DataSetStatic(this, this.field.relation, this.build_context());
+ },
+ start: function() {
+ this.field_manager.on("view_content_has_changed", this, this.calc_domain);
+ this.calc_domain();
+ this.on("change:value", this, this.get_selection);
+ this.on("change:evaluated_selection_domain", this, this.get_selection);
+ this.on("change:selection", this, function() {
+ this.selection = this.get("selection");
+ this.render_value();
+ });
+ this.get_selection();
+ if (this.options.clickable) {
+ this.$el.on('click','li[data-id]',this.on_click_stage);
+ }
+ if (this.$el.parent().is('header')) {
+ this.$el.after('<div class="oe_clear"/>');
+ }
+ this._super();
+ },
+ set_value: function(value_) {
+ if (value_ instanceof Array) {
+ value_ = value_[0];
+ }
+ this._super(value_);
+ },
+ render_value: function() {
+ var self = this;
+ var content = QWeb.render("FieldStatus.content", {
+ 'widget': self,
+ 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
+ });
+ self.$el.html(content);
+ },
+ calc_domain: function() {
+ var d = pyeval.eval('domain', this.build_domain());
+ var domain = []; //if there is no domain defined, fetch all the records
+
+ if (d.length) {
+ domain = ['|',['id', '=', this.get('value')]].concat(d);
+ }
+
+ if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
+ this.set("evaluated_selection_domain", domain);
+ }
+ },
+ /** Get the selection and render it
+ * selection: [[identifier, value_to_display], ...]
+ * For selection fields: this is directly given by this.field.selection
+ * For many2one fields: perform a search on the relation of the many2one field
+ */
+ get_selection: function() {
+ var self = this;
+ var selection_unfolded = [];
+ var selection_folded = [];
+ var fold_field = this.options.fold_field;
+
+ var calculation = _.bind(function() {
+ if (this.field.type == "many2one") {
+ return self.get_distant_fields().then(function (fields) {
+ return new data.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
+ .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
+ var ids = _.pluck(records, 'id');
+ return self.dataset.name_get(ids).then(function (records_name) {
+ _.each(records, function (record) {
+ var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
+ if (fold_field && record[fold_field] && record.id != self.get('value')) {
+ selection_folded.push([record.id, name]);
+ } else {
+ selection_unfolded.push([record.id, name]);
+ }
+ });
+ });
+ });
+ });
+ } else {
+ // For field type selection filter values according to
+ // statusbar_visible attribute of the field. For example:
+ // statusbar_visible="draft,open".
+ var select = this.field.selection;
+ for(var i=0; i < select.length; i++) {
+ var key = select[i][0];
+ if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
+ selection_unfolded.push(select[i]);
+ }
+ }
+ return $.when();
+ }
+ }, this);
+ this.selection_dm.add(calculation()).then(function () {
+ var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
+ if (! _.isEqual(selection, self.get("selection"))) {
+ self.set("selection", selection);
+ }
+ });
+ },
+ /*
+ * :deprecated: this feature will probably be removed with OpenERP v8
+ */
+ get_distant_fields: function() {
+ var self = this;
+ if (! this.options.fold_field) {
+ this.distant_fields = {};
+ }
+ if (this.distant_fields) {
+ return $.when(this.distant_fields);
+ }
+ return new Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
+ self.distant_fields = fields;
+ return fields;
+ });
+ },
+ on_click_stage: function (ev) {
+ var self = this;
+ var $li = $(ev.currentTarget);
+ var val;
+ if (this.field.type == "many2one") {
+ val = parseInt($li.data("id"), 10);
+ }
+ else {
+ val = $li.data("id");
+ }
+ if (val != self.get('value')) {
+ this.view.recursive_save().done(function() {
+ var change = {};
+ change[self.name] = val;
+ self.view.dataset.write(self.view.datarecord.id, change).done(function() {
+ self.view.reload();
+ });
+ });
+ }
+ },
+});
+
+var FieldMonetary = FieldFloat.extend({
+ template: "FieldMonetary",
+ widget_class: 'oe_form_field_float oe_form_field_monetary',
+ init: function() {
+ this._super.apply(this, arguments);
+ this.set({"currency": false});
+ if (this.options.currency_field) {
+ this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
+ this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
+ });
+ }
+ this.on("change:currency", this, this.get_currency_info);
+ this.get_currency_info();
+ this.ci_dm = new utils.DropMisordered();
+ },
+ start: function() {
+ var tmp = this._super();
+ this.on("change:currency_info", this, this.reinitialize);
+ return tmp;
+ },
+ get_currency_info: function() {
+ var self = this;
+ if (this.get("currency") === false) {
+ this.set({"currency_info": null});
+ return;
+ }
+ return this.ci_dm.add(self.alive(new Model("res.currency").query(["symbol", "position"])
+ .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
+ self.set({"currency_info": res});
+ });
+ },
+ parse_value: function(val, def) {
+ return formats.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
+ },
+ format_value: function(val, def) {
+ return formats.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
+ },
+});
+
+/**
+ This widget is intended to be used on stat button numeric fields. It will display
+ the value many2many and one2many. It is a read-only field that will
+ display a simple string "<value of field> <label of the field>"
+*/
+var StatInfo = common.AbstractField.extend({
+ is_field_number: true,
+ init: function() {
+ this._super.apply(this, arguments);
+ this.internal_set_value(0);
+ },
+ set_value: function(value_) {
+ if (value_ === false || value_ === undefined) {
+ value_ = 0;
+ }
+ this._super.apply(this, [value_]);
+ },
+ render_value: function() {
+ var options = {
+ value: this.get("value") || 0,
+ };
+ if (! this.node.attrs.nolabel) {
+ if(this.options.label_field && this.view.datarecord[this.options.label_field]) {
+ options.text = this.view.datarecord[this.options.label_field];
+ }
+ else {
+ options.text = this.string;
+ }
+ }
+ this.$el.html(QWeb.render("StatInfo", options));
+ },
+
+});
+
+/**
+ This widget is intended to be used on boolean fields. It toggles a button
+ switching between a green bullet / gray bullet.
+*/
+var FieldToggleBoolean = common.AbstractField.extend({
+ render_value: function () {
+ this.icon = this.get_value() ? 'gtk-yes.png' : 'gtk-normal.png';
+ this.$el.html(QWeb.render("FieldToggleBoolean", {'widget': this}));
+ this.$('.oe_toggle_button').on('click', this.set_toggle_button.bind(this));
+ },
+ set_toggle_button: function () {
+ var self = this;
+ var toggle_value = this.get_value() === false ? true: false;
+ if (this.view.get('actual_mode') == 'view') {
+ var rec_values = {};
+ rec_values[self.node.attrs.name] = toggle_value;
+ return this.view.dataset._model.call(
+ 'write', [
+ [this.view.datarecord.id],
+ rec_values,
+ self.view.dataset.get_context()
+ ]).done(function () { self.reload_record(); });
+ }
+ else {
+ this.set_value(toggle_value);
+ }
+ },
+ reload_record: function () {
+ this.view.reload();
+ },
+});
+
+
+/**
+ * Registry of form fields, called by :js:`instance.web.FormView`.
+ *
+ * All referenced classes must implement FieldInterface. Those represent the classes whose instances
+ * will substitute to the <field> tags as defined in OpenERP's views.
+ */
+core.form_widget_registry
+ .add('char', FieldChar)
+ .add('id', FieldID)
+ .add('email', FieldEmail)
+ .add('url', FieldUrl)
+ .add('text',FieldText)
+ .add('html', FieldTextHtml)
+ .add('char_domain', FieldCharDomain)
+ .add('date', FieldDate)
+ .add('datetime', FieldDatetime)
+ .add('selection', FieldSelection)
+ .add('radio', FieldRadio)
+ .add('reference', FieldReference)
+ .add('boolean', FieldBoolean)
+ .add('toggle_button', FieldToggleBoolean)
+ .add('float', FieldFloat)
+ .add('percentpie', FieldPercentPie)
+ .add('barchart', FieldBarChart)
+ .add('integer', FieldFloat)
+ .add('float_time', FieldFloat)
+ .add('progressbar', FieldProgressBar)
+ .add('image', FieldBinaryImage)
+ .add('binary', FieldBinaryFile)
+ .add('statusbar', FieldStatus)
+ .add('monetary', FieldMonetary)
+ .add('priority', Priority)
+ .add('kanban_state_selection', KanbanSelection)
+ .add('statinfo', StatInfo);
+
+
+/**
+ * Registry of widgets usable in the form view that can substitute to any possible
+ * tags defined in OpenERP's form views.
+ *
+ * Every referenced class should extend FormWidget.
+ */
+core.form_tag_registry.add('button', WidgetButton);
+
+return {
+ FieldChar: FieldChar,
+};
+
+});
diff --git a/addons/web/static/src/js/views/graph_view.js b/addons/web/static/src/js/views/graph_view.js
new file mode 100644
index 00000000000..26826b19aac
--- /dev/null
+++ b/addons/web/static/src/js/views/graph_view.js
@@ -0,0 +1,128 @@
+odoo.define('web.GraphView', ['web.core', 'web.GraphWidget', 'web.Model', 'web.View'], function (require) {
+"use strict";
+/*---------------------------------------------------------
+ * Odoo Graph view
+ *---------------------------------------------------------*/
+
+var core = require('web.core');
+var GraphWidget = require('web.GraphWidget');
+var Model = require('web.Model');
+var View = require('web.View');
+
+var _lt = core._lt;
+var _t = core._t;
+var QWeb = core.qweb;
+
+var GraphView = View.extend({
+ className: 'oe_graph',
+ display_name: _lt('Graph'),
+ view_type: 'graph',
+
+ init: function(parent, dataset, view_id, options) {
+ this._super(parent, dataset, view_id, options);
+
+ this.model = new Model(dataset.model, {group_by_no_leaf: true});
+ this.measures = [];
+ this.active_measure = '__count__';
+ this.initial_groupbys = [];
+ this.widget = undefined;
+ },
+ start: function () {
+ var load_fields = this.model.call('fields_get', [])
+ .then(this.prepare_fields.bind(this));
+
+ return $.when(this._super(), load_fields).then(this.render_buttons.bind(this));
+ },
+ render_buttons: function () {
+ var context = {measures: _.pairs(_.omit(this.measures, '__count__'))};
+ this.$buttons.html(QWeb.render('GraphView.buttons', context));
+ this.$measure_list = this.$buttons.find('.oe-measure-list');
+ this.update_measure();
+ this.$buttons.find('button').tooltip();
+ this.$buttons.click(this.on_button_click.bind(this));
+ },
+ update_measure: function () {
+ var self = this;
+ this.$measure_list.find('li').each(function (index, li) {
+ $(li).toggleClass('selected', $(li).data('field') === self.active_measure);
+ });
+ },
+ view_loading: function (fvg) {
+ var self = this;
+ fvg.arch.children.forEach(function (field) {
+ var name = field.attrs.name;
+ if (field.attrs.interval) {
+ name += ':' + field.attrs.interval;
+ }
+ if (field.attrs.type === 'measure') {
+ self.active_measure = name;
+ } else {
+ self.initial_groupbys.push(name);
+ }
+ });
+ },
+ do_show: function () {
+ this.do_push_state({});
+ this.$el.show();
+ return this._super();
+ },
+ prepare_fields: function (fields) {
+ var self = this;
+ this.fields = fields;
+ _.each(fields, function (field, name) {
+ if ((name !== 'id') && (field.store === true)) {
+ if (field.type === 'integer' || field.type === 'float') {
+ self.measures[name] = field;
+ }
+ }
+ });
+ this.measures.__count__ = {string: _t("Quantity"), type: "integer"};
+ },
+ do_search: function (domain, context, group_by) {
+ if (!this.widget) {
+ this.initial_groupbys = context.graph_groupbys || this.initial_groupbys;
+ this.widget = new GraphWidget(this, this.dataset.model, {
+ measure: context.graph_measure || this.active_measure,
+ mode: context.graph_mode || this.active_mode,
+ domain: domain,
+ groupbys: this.initial_groupbys,
+ context: context,
+ fields: this.fields,
+ });
+ // append widget
+ this.$el.hide();
+ this.widget.appendTo(this.$el);
+ } else {
+ var groupbys = group_by.length ? group_by : this.initial_groupbys.slice(0);
+ this.widget.update_data(domain, groupbys);
+ }
+ },
+ get_context: function () {
+ return !this.widget ? {} : {
+ graph_mode: this.widget.mode,
+ graph_measure: this.widget.measure,
+ graph_groupbys: this.widget.groupbys
+ };
+ },
+ on_button_click: function (event) {
+ var $target = $(event.target);
+ if ($target.hasClass('oe-bar-mode')) {this.widget.set_mode('bar');}
+ if ($target.hasClass('oe-line-mode')) {this.widget.set_mode('line');}
+ if ($target.hasClass('oe-pie-mode')) {this.widget.set_mode('pie');}
+ if ($target.parents('.oe-measure-list').length) {
+ var parent = $target.parent(),
+ field = parent.data('field');
+ this.active_measure = field;
+ parent.toggleClass('selected');
+ event.stopPropagation();
+ this.update_measure();
+ this.widget.set_measure(this.active_measure);
+ }
+ },
+});
+
+core.view_registry.add('graph', GraphView);
+
+return GraphView;
+
+});
diff --git a/addons/web/static/src/js/view_graph.js b/addons/web/static/src/js/views/graph_widget.js
index 385f347e307..b06ab42f346 100644
--- a/addons/web/static/src/js/view_graph.js
+++ b/addons/web/static/src/js/views/graph_widget.js
@@ -1,134 +1,20 @@
-/*---------------------------------------------------------
- * Odoo Pivot Table view
- *---------------------------------------------------------*/
+odoo.define('web.GraphWidget', ['web.core', 'web.Model', 'web.Widget'], function (require) {
+"use strict";
-(function (a, b, c) {
-'use strict';
+var core = require('web.core');
+var Model = require('web.Model');
+var Widget = require('web.Widget');
-var instance = openerp,
- _lt = instance.web._lt,
- _t = instance.web._t,
- QWeb = instance.web.qweb;
+var _t = core._t;
+var QWeb = core.qweb;
-nv.dev = false; // sets nvd3 library in production mode
-
-instance.web.views.add('graph', 'instance.web.GraphView');
-
-instance.web.GraphView = instance.web.View.extend({
- className: 'oe_graph',
- display_name: _lt('Graph'),
- view_type: 'graph',
-
- init: function(parent, dataset, view_id, options) {
- this._super(parent, dataset, view_id, options);
-
- this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true});
- this.measures = [];
- this.active_measure = '__count__';
- this.initial_groupbys = [];
- this.widget = undefined;
- },
- start: function () {
- var load_fields = this.model.call('fields_get', [])
- .then(this.prepare_fields.bind(this));
-
- return $.when(this._super(), load_fields).then(this.render_buttons.bind(this));
- },
- render_buttons: function () {
- var context = {measures: _.pairs(_.omit(this.measures, '__count__'))};
- this.$buttons.html(QWeb.render('GraphView.buttons', context));
- this.$measure_list = this.$buttons.find('.oe-measure-list');
- this.update_measure();
- this.$buttons.find('button').tooltip();
- this.$buttons.click(this.on_button_click.bind(this));
- },
- update_measure: function () {
- var self = this;
- this.$measure_list.find('li').each(function (index, li) {
- $(li).toggleClass('selected', $(li).data('field') === self.active_measure);
- });
- },
- view_loading: function (fvg) {
- var self = this;
- fvg.arch.children.forEach(function (field) {
- var name = field.attrs.name;
- if (field.attrs.interval) {
- name += ':' + field.attrs.interval;
- }
- if (field.attrs.type === 'measure') {
- self.active_measure = name;
- } else {
- self.initial_groupbys.push(name);
- }
- });
- },
- do_show: function () {
- this.do_push_state({});
- this.$el.show();
- return this._super();
- },
- prepare_fields: function (fields) {
- var self = this;
- this.fields = fields;
- _.each(fields, function (field, name) {
- if ((name !== 'id') && (field.store === true)) {
- if (field.type === 'integer' || field.type === 'float') {
- self.measures[name] = field;
- }
- }
- });
- this.measures.__count__ = {string: _t("Quantity"), type: "integer"};
- },
- do_search: function (domain, context, group_by) {
- if (!this.widget) {
- this.initial_groupbys = context.graph_groupbys || this.initial_groupbys;
- this.widget = new instance.web.GraphWidget(this, this.dataset.model, {
- measure: context.graph_measure || this.active_measure,
- mode: context.graph_mode || this.active_mode,
- domain: domain,
- groupbys: this.initial_groupbys,
- context: context,
- fields: this.fields,
- });
- // append widget
- this.$el.hide();
- this.widget.appendTo(this.$el);
- } else {
- var groupbys = group_by.length ? group_by : this.initial_groupbys.slice(0);
- this.widget.update_data(domain, groupbys);
- }
- },
- get_context: function () {
- return !this.widget ? {} : {
- graph_mode: this.widget.mode,
- graph_measure: this.widget.measure,
- graph_groupbys: this.widget.groupbys
- };
- },
- on_button_click: function (event) {
- var $target = $(event.target);
- if ($target.hasClass('oe-bar-mode')) {this.widget.set_mode('bar');}
- if ($target.hasClass('oe-line-mode')) {this.widget.set_mode('line');}
- if ($target.hasClass('oe-pie-mode')) {this.widget.set_mode('pie');}
- if ($target.parents('.oe-measure-list').length) {
- var parent = $target.parent(),
- field = parent.data('field');
- this.active_measure = field;
- parent.toggleClass('selected');
- event.stopPropagation();
- this.update_measure();
- this.widget.set_measure(this.active_measure);
- }
- },
-});
-
-instance.web.GraphWidget = instance.web.Widget.extend({
+return Widget.extend({
init: function (parent, model, options) {
this._super(parent);
this.context = options.context;
this.fields = options.fields;
this.fields.__count__ = _t("Quantity");
- this.model = new instance.web.Model(model, {group_by_no_leaf: true});
+ this.model = new Model(model, {group_by_no_leaf: true});
this.domain = options.domain || [];
this.groupbys = options.groupbys || [];
@@ -288,17 +174,14 @@ instance.web.GraphWidget = instance.web.Widget.extend({
},
display_pie: function () {
var data = [],
- tickValues,
- tickFormat,
- measure = this.fields[this.measure].string;
-
- var all_negative = true,
+ all_negative = true,
some_negative = false,
all_zero = true;
+
this.data.forEach(function (datapt) {
all_negative = all_negative && (datapt.value < 0);
some_negative = some_negative || (datapt.value < 0);
- all_zero = all_zero && (datapt.value === 0)
+ all_zero = all_zero && (datapt.value === 0);
});
if (some_negative && !all_negative) {
return this.$el.append(QWeb.render('GraphView.error', {
@@ -417,29 +300,4 @@ instance.web.GraphWidget = instance.web.Widget.extend({
}
});
-// monkey patch nvd3 to allow removing eventhandler on windowresize events
-// see https://github.com/novus/nvd3/pull/396 for more details
-
-// Adds a resize listener to the window.
-nv.utils.onWindowResize = function(fun) {
- if (fun === null) return;
- window.addEventListener('resize', fun);
-};
-
-// Backwards compatibility with current API.
-nv.utils.windowResize = nv.utils.onWindowResize;
-
-// Removes a resize listener from the window.
-nv.utils.offWindowResize = function(fun) {
- if (fun === null) return;
- window.removeEventListener('resize', fun);
-};
-
-// monkey patch nvd3 to prevent crashes when user changes view and nvd3 tries
-// to remove tooltips after 500 ms... seriously nvd3, what were you thinking?
-nv.tooltip.cleanup = function () {
- $('.nvtooltip').remove();
-};
-
-
-})();
+});
diff --git a/addons/web/static/src/js/views/list_common.js b/addons/web/static/src/js/views/list_common.js
new file mode 100644
index 00000000000..e2e9ec1eb41
--- /dev/null
+++ b/addons/web/static/src/js/views/list_common.js
@@ -0,0 +1,402 @@
+odoo.define('web.list_common', ['web.core'], function (require) {
+"use strict";
+
+var core = require('web.core');
+
+var _t = core._t;
+var Class = core.Class;
+
+/**
+ * @mixin Events
+ */
+var Events = /** @lends Events# */{
+ /**
+ * @param {String} event event to listen to on the current object, null for all events
+ * @param {Function} handler event handler to bind to the relevant event
+ * @returns this
+ */
+ bind: function (event, handler) {
+ var calls = this['_callbacks'] || (this._callbacks = {});
+
+ if (event in calls) {
+ calls[event].push(handler);
+ } else {
+ calls[event] = [handler];
+ }
+ return this;
+ },
+ /**
+ * @param {String} event event to unbind on the current object
+ * @param {function} [handler] specific event handler to remove (otherwise unbind all handlers for the event)
+ * @returns this
+ */
+ unbind: function (event, handler) {
+ var calls = this._callbacks || {};
+ if (!(event in calls)) { return this; }
+ if (!handler) {
+ delete calls[event];
+ } else {
+ var handlers = calls[event];
+ handlers.splice(
+ _(handlers).indexOf(handler),
+ 1);
+ }
+ return this;
+ },
+ /**
+ * @param {String} event
+ * @returns this
+ */
+ trigger: function (event) {
+ var calls;
+ if (!(calls = this._callbacks)) { return this; }
+ var callbacks = (calls[event] || []).concat(calls[null] || []);
+ for(var i=0, length=callbacks.length; i<length; ++i) {
+ callbacks[i].apply(this, arguments);
+ }
+ return this;
+ }
+};
+var Record = Class.extend(/** @lends Record# */{
+ /**
+ * @constructs Record
+ * @extends instance.web.Class
+ *
+ * @mixes Events
+ * @param {Object} [data]
+ */
+ init: function (data) {
+ this.attributes = data || {};
+ },
+ /**
+ * @param {String} key
+ * @returns {Object}
+ */
+ get: function (key) {
+ return this.attributes[key];
+ },
+ /**
+ * @param key
+ * @param value
+ * @param {Object} [options]
+ * @param {Boolean} [options.silent=false]
+ * @returns {Record}
+ */
+ set: function (key, value, options) {
+ options = options || {};
+ var old_value = this.attributes[key];
+ if (old_value === value) {
+ return this;
+ }
+ this.attributes[key] = value;
+ if (!options.silent) {
+ this.trigger('change:' + key, this, value, old_value);
+ this.trigger('change', this, key, value, old_value);
+ }
+ return this;
+ },
+ /**
+ * Converts the current record to the format expected by form views:
+ *
+ * .. code-block:: javascript
+ *
+ * data: {
+ * $fieldname: {
+ * value: $value
+ * }
+ * }
+ *
+ *
+ * @returns {Object} record displayable in a form view
+ */
+ toForm: function () {
+ var form_data = {}, attrs = this.attributes;
+ for(var k in attrs) {
+ form_data[k] = {value: attrs[k]};
+ }
+
+ return {data: form_data};
+ },
+ /**
+ * Converts the current record to a format expected by context evaluations
+ * (identical to record.attributes, except m2o fields are their integer
+ * value rather than a pair)
+ */
+ toContext: function () {
+ var output = {}, attrs = this.attributes;
+ for(var k in attrs) {
+ var val = attrs[k];
+ if (typeof val !== 'object') {
+ output[k] = val;
+ } else if (val instanceof Array) {
+ output[k] = val[0];
+ } else {
+ throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
+ }
+ }
+ return output;
+ }
+});
+Record.include(Events);
+var Collection = Class.extend(/** @lends Collection# */{
+ /**
+ * Smarter collections, with events, very strongly inspired by Backbone's.
+ *
+ * Using a "dumb" array of records makes synchronization between the
+ * various serious
+ *
+ * @constructs Collection
+ * @extends instance.web.Class
+ *
+ * @mixes Events
+ * @param {Array} [records] records to initialize the collection with
+ * @param {Object} [options]
+ */
+ init: function (records, options) {
+ options = options || {};
+ _.bindAll(this, '_onRecordEvent');
+ this.length = 0;
+ this.records = [];
+ this._byId = {};
+ this._proxies = {};
+ this._key = options.key;
+ this._parent = options.parent;
+
+ if (records) {
+ this.add(records);
+ }
+ },
+ /**
+ * @param {Object|Array} record
+ * @param {Object} [options]
+ * @param {Number} [options.at]
+ * @param {Boolean} [options.silent=false]
+ * @returns this
+ */
+ add: function (record, options) {
+ options = options || {};
+ var records = record instanceof Array ? record : [record];
+
+ for(var i=0, length=records.length; i<length; ++i) {
+ var instance_ = (records[i] instanceof Record) ? records[i] : new Record(records[i]);
+ instance_.bind(null, this._onRecordEvent);
+ this._byId[instance_.get('id')] = instance_;
+ if (options.at === undefined || options.at === null) {
+ this.records.push(instance_);
+ if (!options.silent) {
+ this.trigger('add', this, instance_, this.records.length-1);
+ }
+ } else {
+ var insertion_index = options.at + i;
+ this.records.splice(insertion_index, 0, instance_);
+ if (!options.silent) {
+ this.trigger('add', this, instance_, insertion_index);
+ }
+ }
+ this.length++;
+ }
+ return this;
+ },
+
+ /**
+ * Get a record by its index in the collection, can also take a group if
+ * the collection is not degenerate
+ *
+ * @param {Number} index
+ * @param {String} [group]
+ * @returns {Record|undefined}
+ */
+ at: function (index, group) {
+ if (group) {
+ var groups = group.split('.');
+ return this._proxies[groups[0]].at(index, groups.join('.'));
+ }
+ return this.records[index];
+ },
+ /**
+ * Get a record by its database id
+ *
+ * @param {Number} id
+ * @returns {Record|undefined}
+ */
+ get: function (id) {
+ if (!_(this._proxies).isEmpty()) {
+ var record = null;
+ _(this._proxies).detect(function (proxy) {
+ record = proxy.get(id);
+ return record;
+ });
+ return record;
+ }
+ return this._byId[id];
+ },
+ /**
+ * Builds a proxy (insert/retrieve) to a subtree of the collection, by
+ * the subtree's group
+ *
+ * @param {String} section group path section
+ * @returns {Collection}
+ */
+ proxy: function (section) {
+ this._proxies[section] = new Collection(null, {
+ parent: this,
+ key: section
+ }).bind(null, this._onRecordEvent);
+ return this._proxies[section];
+ },
+ /**
+ * @param {Array} [records]
+ * @returns this
+ */
+ reset: function (records, options) {
+ options = options || {};
+ _(this._proxies).each(function (proxy) {
+ proxy.reset();
+ });
+ this._proxies = {};
+ _(this.records).invoke('unbind', null, this._onRecordEvent);
+ this.length = 0;
+ this.records = [];
+ this._byId = {};
+ if (records) {
+ this.add(records);
+ }
+ if (!options.silent) {
+ this.trigger('reset', this);
+ }
+ return this;
+ },
+ /**
+ * Removes the provided record from the collection
+ *
+ * @param {Record} record
+ * @returns this
+ */
+ remove: function (record) {
+ var index = this.indexOf(record);
+ if (index === -1) {
+ _(this._proxies).each(function (proxy) {
+ proxy.remove(record);
+ });
+ return this;
+ }
+
+ record.unbind(null, this._onRecordEvent);
+ this.records.splice(index, 1);
+ delete this._byId[record.get('id')];
+ this.length--;
+ this.trigger('remove', record, this);
+ return this;
+ },
+
+ _onRecordEvent: function (event) {
+ switch(event) {
+ // don't propagate reset events
+ case 'reset': return;
+ case 'change:id':
+ var record = arguments[1];
+ var new_value = arguments[2];
+ var old_value = arguments[3];
+ // [change:id, record, new_value, old_value]
+ if (this._byId[old_value] === record) {
+ delete this._byId[old_value];
+ this._byId[new_value] = record;
+ }
+ break;
+ }
+ this.trigger.apply(this, arguments);
+ },
+
+ // underscore-type methods
+ find: function (callback) {
+ var record;
+ for(var section in this._proxies) {
+ if (!this._proxies.hasOwnProperty(section)) {
+ continue;
+ }
+ if ((record = this._proxies[section].find(callback))) {
+ return record;
+ }
+ }
+ for(var i=0; i<this.length; ++i) {
+ record = this.records[i];
+ if (callback(record)) {
+ return record;
+ }
+ }
+ },
+ each: function (callback) {
+ for(var section in this._proxies) {
+ if (this._proxies.hasOwnProperty(section)) {
+ this._proxies[section].each(callback);
+ }
+ }
+ for(var i=0; i<this.length; ++i) {
+ callback(this.records[i]);
+ }
+ },
+ map: function (callback) {
+ var results = [];
+ this.each(function (record) {
+ results.push(callback(record));
+ });
+ return results;
+ },
+ pluck: function (fieldname) {
+ return this.map(function (record) {
+ return record.get(fieldname);
+ });
+ },
+ indexOf: function (record) {
+ return _(this.records).indexOf(record);
+ },
+ succ: function (record, options) {
+ options = options || {wraparound: false};
+ var result;
+ for(var section in this._proxies) {
+ if (!this._proxies.hasOwnProperty(section)) {
+ continue;
+ }
+ if ((result = this._proxies[section].succ(record, options))) {
+ return result;
+ }
+ }
+ var index = this.indexOf(record);
+ if (index === -1) { return null; }
+ var next_index = index + 1;
+ if (options.wraparound && (next_index === this.length)) {
+ return this.at(0);
+ }
+ return this.at(next_index);
+ },
+ pred: function (record, options) {
+ options = options || {wraparound: false};
+
+ var result;
+ for (var section in this._proxies) {
+ if (!this._proxies.hasOwnProperty(section)) {
+ continue;
+ }
+ if ((result = this._proxies[section].pred(record, options))) {
+ return result;
+ }
+ }
+
+ var index = this.indexOf(record);
+ if (index === -1) { return null; }
+ var next_index = index - 1;
+ if (options.wraparound && (next_index === -1)) {
+ return this.at(this.length - 1);
+ }
+ return this.at(next_index);
+ }
+});
+Collection.include(Events);
+
+return {
+ Events: Events,
+ Record: Record,
+ Collection: Collection
+};
+
+});
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/views/list_view.js
index 97c4abda05f..ef875e51679 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/views/list_view.js
@@ -1,12 +1,26 @@
-(function() {
-
-var instance = openerp;
-openerp.web.list = {};
-var _t = instance.web._t,
- _lt = instance.web._lt;
-var QWeb = instance.web.qweb;
-instance.web.views.add('list', 'instance.web.ListView');
-instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ {
+
+odoo.define('web.ListView', ['web.core', 'web.data', 'web.DataExport', 'web.formats', 'web.list_common', 'web.Model', 'web.pyeval', 'web.session', 'web.Sidebar', 'web.utils', 'web.View'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var DataExport = require('web.DataExport');
+var formats = require('web.formats');
+var common = require('web.list_common');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+var Sidebar = require('web.Sidebar');
+var utils = require('web.utils');
+var View = require('web.View');
+
+var Class = core.Class;
+var _t = core._t;
+var _lt = core._lt;
+var QWeb = core.qweb;
+var list_widget_registry = core.list_widget_registry;
+
+var ListView = View.extend( /** @lends instance.web.ListView# */ {
_template: 'ListView',
display_name: _lt('List'),
defaults: {
@@ -68,11 +82,11 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
this.columns = [];
- this.records = new Collection();
+ this.records = new common.Collection();
this.set_groups(new (this.options.GroupsType)(this));
- if (this.dataset instanceof instance.web.DataSetStatic) {
+ if (this.dataset instanceof data.DataSetStatic) {
this.groups.datagroup = new StaticDataGroup(this.dataset);
} else {
this.groups.datagroup = new DataGroup(
@@ -100,8 +114,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
set_default_options: function (options) {
this._super(options);
_.defaults(this.options, {
- GroupsType: instance.web.ListView.Groups,
- ListType: instance.web.ListView.List
+ GroupsType: ListView.Groups,
+ ListType: ListView.List
});
},
@@ -170,7 +184,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
var len, style= '';
var context = _.extend({}, record.attributes, {
- uid: this.session.uid,
+ uid: session.uid,
current_date: moment().format('YYYY-MM-DD')
// TODO: time, datetime, relativedelta
});
@@ -347,7 +361,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
// Sidebar
if (!this.sidebar && this.options.$sidebar) {
- this.sidebar = new instance.web.Sidebar(this);
+ this.sidebar = new Sidebar(this);
this.sidebar.appendTo(this.options.$sidebar);
this.sidebar.add_items('other', _.compact([
{ label: _t("Export"), callback: this.on_sidebar_export },
@@ -433,16 +447,15 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
* @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
*/
setup_columns: function (fields, grouped) {
- var registry = instance.web.list.columns;
this.columns.splice(0, this.columns.length);
this.columns.push.apply(this.columns,
_(this.fields_view.arch.children).map(function (field) {
var id = field.attrs.name;
- return registry.for_(id, fields[id], field);
+ return for_(id, fields[id], field);
}));
if (grouped) {
this.columns.unshift(
- new instance.web.list.MetaColumn('_group', _t("Group")));
+ new ListView.MetaColumn('_group', _t("Group")));
}
this.visible_columns = _.filter(this.columns, function (column) {
@@ -495,7 +508,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
* @param {Boolean} [grouped] Should the list be displayed grouped
* @param {Object} [context] context to send the server while loading the view
*/
- reload_view: function (grouped, context, initial) {
+ reload_view: function (grouped, context) {
return this.load_view(context);
},
/**
@@ -513,7 +526,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
});
this.$el.find('.oe_list_content').append(
this.groups.render(function () {
- if (self.dataset.index == null) {
+ if (self.dataset.index === null) {
if (self.records.length) {
self.dataset.index = 0;
}
@@ -576,6 +589,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
this.reload_content();
}
},
+ on_sidebar_export: function() {
+ new DataExport(this, this.dataset).open();
+ },
/**
* Handler for the result of eval_domain_and_context, actually perform the
* searching
@@ -689,7 +705,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
return;
}
- var c = new instance.web.CompoundContext();
+ var c = new data.CompoundContext();
c.set_eval_context(_.extend({
active_id: id,
active_ids: [id],
@@ -808,7 +824,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
this.display_aggregates(aggregates);
},
display_aggregates: function (aggregation) {
- var self = this;
var $footer_cells = this.$el.find('.oe_list_footer');
_(this.aggregate_columns).each(function (column) {
if (!column['function']) {
@@ -833,13 +848,13 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (this.$('.oe_list_record_selector').prop('checked')) {
var search_view = this.getParent().searchview;
var search_data = search_view.build_search_data();
- return instance.web.pyeval.eval_domains_and_contexts({
+ return pyeval.eval_domains_and_contexts({
domains: search_data.domains,
contexts: search_data.contexts,
group_by_seq: search_data.groupbys || []
}).then(function (results) {
var domain = self.dataset.domain.concat(results.domain || []);
- return domain
+ return domain;
});
}
else {
@@ -906,7 +921,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
});
}
});
-instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{
+core.view_registry.add('list', ListView);
+
+ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
/**
* List display for the ListView, handles basic DOM events and transforms
* them in the relevant higher-level events, to which the list view (or
@@ -1073,7 +1090,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
// to get a correctly displayable value in the field
var model = ref_match[1],
id = parseInt(ref_match[2], 10);
- new instance.web.DataSet(this.view, model).name_get([id]).done(function(names) {
+ new data.DataSet(this.view, model).name_get([id]).done(function(names) {
if (!names.length) { return; }
record.set(column.id + '__display', names[0][1]);
});
@@ -1088,7 +1105,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
// fetch the name, set it on the record (in the right field)
// and let the various registered events handle refreshing the
// row
- new instance.web.DataSet(this.view, column.relation)
+ new data.DataSet(this.view, column.relation)
.name_get([value]).done(function (names) {
if (!names.length) { return; }
record.set(column.id, names[0]);
@@ -1112,7 +1129,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
// 2. an array of ids
ids = value;
}
- new instance.web.Model(column.relation)
+ new Model(column.relation)
.call('name_get', [ids, this.dataset.context]).done(function (names) {
// FIXME: nth horrible hack in this poor listview
record.set(column.id + '__display',
@@ -1129,6 +1146,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
});
},
render: function () {
+ var self = this;
this.$current.empty().append(
QWeb.render('ListView.rows', _.extend({
render_cell: function () {
@@ -1234,7 +1252,8 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
});
}
});
-instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{
+
+ListView.Groups = Class.extend( /** @lends instance.web.ListView.Groups# */{
passthrough_events: 'action deleted row_link',
/**
* Grouped display for the ListView. Handles basic DOM events and interacts
@@ -1683,19 +1702,20 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
* having concurrent list views becomes possible and common.
*/
function synchronized(fn) {
- var fn_mutex = new $.Mutex();
+ var fn_mutex = new utils.Mutex();
return function () {
var obj = this;
var args = _.toArray(arguments);
return fn_mutex.exec(function () {
if (obj.isDestroyed()) { return $.when(); }
- return fn.apply(obj, args)
+ return fn.apply(obj, args);
});
};
}
-var DataGroup = instance.web.Class.extend({
+
+var DataGroup = Class.extend({
init: function(parent, model, domain, context, group_by, level) {
- this.model = new instance.web.Model(model, context, domain);
+ this.model = new Model(model, context, domain);
this.group_by = group_by;
this.context = context;
this.domain = domain;
@@ -1712,7 +1732,7 @@ var DataGroup = instance.web.Class.extend({
$.when(query).done(function (querygroups) {
// leaf node
if (!querygroups) {
- var ds = new instance.web.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain());
+ var ds = new data.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain());
ds._sort = self.sort;
ifRecords(ds);
return;
@@ -1741,6 +1761,7 @@ var DataGroup = instance.web.Class.extend({
});
}
});
+
var StaticDataGroup = DataGroup.extend({
init: function (dataset) {
this.dataset = dataset;
@@ -1750,436 +1771,7 @@ var StaticDataGroup = DataGroup.extend({
}
});
-/**
- * @mixin Events
- */
-var Events = /** @lends Events# */{
- /**
- * @param {String} event event to listen to on the current object, null for all events
- * @param {Function} handler event handler to bind to the relevant event
- * @returns this
- */
- bind: function (event, handler) {
- var calls = this['_callbacks'] || (this._callbacks = {});
-
- if (event in calls) {
- calls[event].push(handler);
- } else {
- calls[event] = [handler];
- }
- return this;
- },
- /**
- * @param {String} event event to unbind on the current object
- * @param {function} [handler] specific event handler to remove (otherwise unbind all handlers for the event)
- * @returns this
- */
- unbind: function (event, handler) {
- var calls = this._callbacks || {};
- if (!(event in calls)) { return this; }
- if (!handler) {
- delete calls[event];
- } else {
- var handlers = calls[event];
- handlers.splice(
- _(handlers).indexOf(handler),
- 1);
- }
- return this;
- },
- /**
- * @param {String} event
- * @returns this
- */
- trigger: function (event) {
- var calls;
- if (!(calls = this._callbacks)) { return this; }
- var callbacks = (calls[event] || []).concat(calls[null] || []);
- for(var i=0, length=callbacks.length; i<length; ++i) {
- callbacks[i].apply(this, arguments);
- }
- return this;
- }
-};
-var Record = instance.web.Class.extend(/** @lends Record# */{
- /**
- * @constructs Record
- * @extends instance.web.Class
- *
- * @mixes Events
- * @param {Object} [data]
- */
- init: function (data) {
- this.attributes = data || {};
- },
- /**
- * @param {String} key
- * @returns {Object}
- */
- get: function (key) {
- return this.attributes[key];
- },
- /**
- * @param key
- * @param value
- * @param {Object} [options]
- * @param {Boolean} [options.silent=false]
- * @returns {Record}
- */
- set: function (key, value, options) {
- options = options || {};
- var old_value = this.attributes[key];
- if (old_value === value) {
- return this;
- }
- this.attributes[key] = value;
- if (!options.silent) {
- this.trigger('change:' + key, this, value, old_value);
- this.trigger('change', this, key, value, old_value);
- }
- return this;
- },
- /**
- * Converts the current record to the format expected by form views:
- *
- * .. code-block:: javascript
- *
- * data: {
- * $fieldname: {
- * value: $value
- * }
- * }
- *
- *
- * @returns {Object} record displayable in a form view
- */
- toForm: function () {
- var form_data = {}, attrs = this.attributes;
- for(var k in attrs) {
- form_data[k] = {value: attrs[k]};
- }
-
- return {data: form_data};
- },
- /**
- * Converts the current record to a format expected by context evaluations
- * (identical to record.attributes, except m2o fields are their integer
- * value rather than a pair)
- */
- toContext: function () {
- var output = {}, attrs = this.attributes;
- for(var k in attrs) {
- var val = attrs[k];
- if (typeof val !== 'object') {
- output[k] = val;
- } else if (val instanceof Array) {
- output[k] = val[0];
- } else {
- throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
- }
- }
- return output;
- }
-});
-Record.include(Events);
-var Collection = instance.web.Class.extend(/** @lends Collection# */{
- /**
- * Smarter collections, with events, very strongly inspired by Backbone's.
- *
- * Using a "dumb" array of records makes synchronization between the
- * various serious
- *
- * @constructs Collection
- * @extends instance.web.Class
- *
- * @mixes Events
- * @param {Array} [records] records to initialize the collection with
- * @param {Object} [options]
- */
- init: function (records, options) {
- options = options || {};
- _.bindAll(this, '_onRecordEvent');
- this.length = 0;
- this.records = [];
- this._byId = {};
- this._proxies = {};
- this._key = options.key;
- this._parent = options.parent;
-
- if (records) {
- this.add(records);
- }
- },
- /**
- * @param {Object|Array} record
- * @param {Object} [options]
- * @param {Number} [options.at]
- * @param {Boolean} [options.silent=false]
- * @returns this
- */
- add: function (record, options) {
- options = options || {};
- var records = record instanceof Array ? record : [record];
-
- for(var i=0, length=records.length; i<length; ++i) {
- var instance_ = (records[i] instanceof Record) ? records[i] : new Record(records[i]);
- instance_.bind(null, this._onRecordEvent);
- this._byId[instance_.get('id')] = instance_;
- if (options.at === undefined || options.at === null) {
- this.records.push(instance_);
- if (!options.silent) {
- this.trigger('add', this, instance_, this.records.length-1);
- }
- } else {
- var insertion_index = options.at + i;
- this.records.splice(insertion_index, 0, instance_);
- if (!options.silent) {
- this.trigger('add', this, instance_, insertion_index);
- }
- }
- this.length++;
- }
- return this;
- },
-
- /**
- * Get a record by its index in the collection, can also take a group if
- * the collection is not degenerate
- *
- * @param {Number} index
- * @param {String} [group]
- * @returns {Record|undefined}
- */
- at: function (index, group) {
- if (group) {
- var groups = group.split('.');
- return this._proxies[groups[0]].at(index, groups.join('.'));
- }
- return this.records[index];
- },
- /**
- * Get a record by its database id
- *
- * @param {Number} id
- * @returns {Record|undefined}
- */
- get: function (id) {
- if (!_(this._proxies).isEmpty()) {
- var record = null;
- _(this._proxies).detect(function (proxy) {
- record = proxy.get(id);
- return record;
- });
- return record;
- }
- return this._byId[id];
- },
- /**
- * Builds a proxy (insert/retrieve) to a subtree of the collection, by
- * the subtree's group
- *
- * @param {String} section group path section
- * @returns {Collection}
- */
- proxy: function (section) {
- this._proxies[section] = new Collection(null, {
- parent: this,
- key: section
- }).bind(null, this._onRecordEvent);
- return this._proxies[section];
- },
- /**
- * @param {Array} [records]
- * @returns this
- */
- reset: function (records, options) {
- options = options || {};
- _(this._proxies).each(function (proxy) {
- proxy.reset();
- });
- this._proxies = {};
- _(this.records).invoke('unbind', null, this._onRecordEvent);
- this.length = 0;
- this.records = [];
- this._byId = {};
- if (records) {
- this.add(records);
- }
- if (!options.silent) {
- this.trigger('reset', this);
- }
- return this;
- },
- /**
- * Removes the provided record from the collection
- *
- * @param {Record} record
- * @returns this
- */
- remove: function (record) {
- var index = this.indexOf(record);
- if (index === -1) {
- _(this._proxies).each(function (proxy) {
- proxy.remove(record);
- });
- return this;
- }
-
- record.unbind(null, this._onRecordEvent);
- this.records.splice(index, 1);
- delete this._byId[record.get('id')];
- this.length--;
- this.trigger('remove', record, this);
- return this;
- },
-
- _onRecordEvent: function (event) {
- switch(event) {
- // don't propagate reset events
- case 'reset': return;
- case 'change:id':
- var record = arguments[1];
- var new_value = arguments[2];
- var old_value = arguments[3];
- // [change:id, record, new_value, old_value]
- if (this._byId[old_value] === record) {
- delete this._byId[old_value];
- this._byId[new_value] = record;
- }
- break;
- }
- this.trigger.apply(this, arguments);
- },
-
- // underscore-type methods
- find: function (callback) {
- var record;
- for(var section in this._proxies) {
- if (!this._proxies.hasOwnProperty(section)) {
- continue;
- }
- if ((record = this._proxies[section].find(callback))) {
- return record;
- }
- }
- for(var i=0; i<this.length; ++i) {
- record = this.records[i];
- if (callback(record)) {
- return record;
- }
- }
- },
- each: function (callback) {
- for(var section in this._proxies) {
- if (this._proxies.hasOwnProperty(section)) {
- this._proxies[section].each(callback);
- }
- }
- for(var i=0; i<this.length; ++i) {
- callback(this.records[i]);
- }
- },
- map: function (callback) {
- var results = [];
- this.each(function (record) {
- results.push(callback(record));
- });
- return results;
- },
- pluck: function (fieldname) {
- return this.map(function (record) {
- return record.get(fieldname);
- });
- },
- indexOf: function (record) {
- return _(this.records).indexOf(record);
- },
- succ: function (record, options) {
- options = options || {wraparound: false};
- var result;
- for(var section in this._proxies) {
- if (!this._proxies.hasOwnProperty(section)) {
- continue;
- }
- if ((result = this._proxies[section].succ(record, options))) {
- return result;
- }
- }
- var index = this.indexOf(record);
- if (index === -1) { return null; }
- var next_index = index + 1;
- if (options.wraparound && (next_index === this.length)) {
- return this.at(0);
- }
- return this.at(next_index);
- },
- pred: function (record, options) {
- options = options || {wraparound: false};
-
- var result;
- for (var section in this._proxies) {
- if (!this._proxies.hasOwnProperty(section)) {
- continue;
- }
- if ((result = this._proxies[section].pred(record, options))) {
- return result;
- }
- }
-
- var index = this.indexOf(record);
- if (index === -1) { return null; }
- var next_index = index - 1;
- if (options.wraparound && (next_index === -1)) {
- return this.at(this.length - 1);
- }
- return this.at(next_index);
- }
-});
-Collection.include(Events);
-instance.web.list = {
- Events: Events,
- Record: Record,
- Collection: Collection
-};
-/**
- * Registry for column objects used to format table cells (and some other tasks
- * e.g. aggregation computations).
- *
- * Maps a field or button to a Column type via its ``$tag.$widget``,
- * ``$tag.$type`` or its ``$tag`` (alone).
- *
- * This specific registry has a dedicated utility method ``for_`` taking a
- * field (from fields_get/fields_view_get.field) and a node (from a view) and
- * returning the right object *already instantiated from the data provided*.
- *
- * @type {instance.web.Registry}
- */
-instance.web.list.columns = new instance.web.Registry({
- 'field': 'instance.web.list.Column',
- 'field.boolean': 'instance.web.list.Boolean',
- 'field.binary': 'instance.web.list.Binary',
- 'field.char': 'instance.web.list.Char',
- 'field.progressbar': 'instance.web.list.ProgressBar',
- 'field.handle': 'instance.web.list.Handle',
- 'button': 'instance.web.list.Button',
- 'field.many2onebutton': 'instance.web.list.Many2OneButton',
- 'field.reference': 'instance.web.list.Reference',
- 'field.many2many': 'instance.web.list.Many2Many',
- 'field.url': 'instance.web.list.Url',
- 'button.toggle_button': 'instance.web.list.toggle_button',
-});
-instance.web.list.columns.for_ = function (id, field, node) {
- var description = _.extend({tag: node.tag}, field, node.attrs);
- var tag = description.tag;
- var Type = this.get_any([
- tag + '.' + description.widget,
- tag + '.'+ description.type,
- tag
- ]);
- return new Type(id, node.tag, description);
-};
-
-instance.web.list.Column = instance.web.Class.extend({
+var Column = Class.extend({
init: function (id, tag, attrs) {
_.extend(attrs, {
id: id,
@@ -2196,7 +1788,7 @@ instance.web.list.Column = instance.web.Class.extend({
},
modifiers_for: function (fields) {
var out = {};
- var domain_computer = instance.web.form.compute_domain;
+ var domain_computer = data.compute_domain;
for (var attr in this.modifiers) {
if (!this.modifiers.hasOwnProperty(attr)) { continue; }
@@ -2259,17 +1851,21 @@ instance.web.list.Column = instance.web.Class.extend({
* @private
*/
_format: function (row_data, options) {
- return _.escape(instance.web.format_value(
+ return _.escape(formats.format_value(
row_data[this.id].value, this, options.value_if_empty));
}
});
-instance.web.list.MetaColumn = instance.web.list.Column.extend({
+
+var MetaColumn = Column.extend({
meta: true,
init: function (id, string) {
this._super(id, '', {string: string});
}
});
-instance.web.list.Button = instance.web.list.Column.extend({
+// to do: do this in a better way (communicate with view_list_editable)
+ListView.MetaColumn = MetaColumn;
+
+var ColumnButton = Column.extend({
/**
* Return an actual ``<button>`` tag
*/
@@ -2283,14 +1879,15 @@ instance.web.list.Button = instance.web.list.Column.extend({
var template = this.icon && 'ListView.row.button' || 'ListView.row.text_button';
return QWeb.render(template, {
widget: this,
- prefix: instance.session.prefix,
+ prefix: session.prefix,
disabled: attrs.readonly
|| isNaN(row_data.id.value)
- || instance.web.BufferedDataSet.virtual_id_regex.test(row_data.id.value)
+ || data.BufferedDataSet.virtual_id_regex.test(row_data.id.value)
});
}
});
-instance.web.list.Boolean = instance.web.list.Column.extend({
+
+var ColumnBoolean = Column.extend({
/**
* Return a potentially disabled checkbox input
*
@@ -2301,7 +1898,8 @@ instance.web.list.Boolean = instance.web.list.Column.extend({
row_data[this.id].value ? 'checked="checked"' : '');
}
});
-instance.web.list.Binary = instance.web.list.Column.extend({
+
+var ColumnBinary = Column.extend({
/**
* Return a link to the binary data as a file
*
@@ -2318,23 +1916,24 @@ instance.web.list.Binary = instance.web.list.Column.extend({
if (value.substr(0, 10).indexOf(' ') == -1) {
download_url = "data:application/octet-stream;base64," + value;
} else {
- download_url = instance.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
+ download_url = session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
if (this.filename) {
download_url += '&filename_field=' + this.filename;
}
}
if (this.filename && row_data[this.filename]) {
- text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
+ text = _.str.sprintf(_t("Download \"%s\""), formats.format_value(
row_data[this.filename].value, {type: 'char'}));
}
return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', {
text: text,
href: download_url,
- size: instance.web.binary_to_binsize(value),
+ size: utils.binary_to_binsize(value),
});
}
});
-instance.web.list.Char = instance.web.list.Column.extend({
+
+var ColumnChar = Column.extend({
replacement: '*',
/**
* If password field, only display replacement characters (if value is
@@ -2348,7 +1947,8 @@ instance.web.list.Char = instance.web.list.Column.extend({
return this._super(row_data, options);
}
});
-instance.web.list.ProgressBar = instance.web.list.Column.extend({
+
+var ColumnProgressBar = Column.extend({
/**
* Return a formatted progress bar display
*
@@ -2361,7 +1961,8 @@ instance.web.list.ProgressBar = instance.web.list.Column.extend({
});
}
});
-instance.web.list.Handle = instance.web.list.Column.extend({
+
+var ColumnHandle = Column.extend({
init: function () {
this._super.apply(this, arguments);
// Handle overrides the field to not be form-editable.
@@ -2376,18 +1977,20 @@ instance.web.list.Handle = instance.web.list.Column.extend({
return '<i class="oe_list_handle fa fa-circle"/>';
}
});
-instance.web.list.Many2OneButton = instance.web.list.Column.extend({
+
+var ColumnMany2OneButton = Column.extend({
_format: function (row_data, options) {
this.has_value = !!row_data[this.id].value;
this.icon = this.has_value ? 'gtk-yes' : 'gtk-no';
this.string = this.has_value ? _t('View') : _t('Create');
return QWeb.render('Many2OneButton.cell', {
'widget': this,
- 'prefix': instance.session.prefix,
+ 'prefix': session.prefix,
});
},
});
-instance.web.list.Many2Many = instance.web.list.Column.extend({
+
+var ColumnMany2Many = Column.extend({
_format: function (row_data, options) {
if (!_.isEmpty(row_data[this.id].value)) {
// If value, use __display version for printing
@@ -2396,7 +1999,8 @@ instance.web.list.Many2Many = instance.web.list.Column.extend({
return this._super(row_data, options);
}
});
-instance.web.list.Reference = instance.web.list.Column.extend({
+
+var ColumnReference = Column.extend({
_format: function (row_data, options) {
if (!_.isEmpty(row_data[this.id].value)) {
// If value, use __display version for printing
@@ -2409,7 +2013,8 @@ instance.web.list.Reference = instance.web.list.Column.extend({
return this._super(row_data, options);
}
});
-instance.web.list.Url = instance.web.list.Column.extend({
+
+var ColumnUrl = Column.extend({
/**
* Regex checking if a URL has a scheme
*/
@@ -2435,7 +2040,8 @@ instance.web.list.Url = instance.web.list.Column.extend({
return this._super(row_data, options);
}
});
-instance.web.list.toggle_button = instance.web.list.Column.extend({
+
+var ColumnToggleButton = Column.extend({
format: function (row_data, options) {
this._super(row_data, options);
var button_tips = JSON.parse(this.options);
@@ -2445,8 +2051,50 @@ instance.web.list.toggle_button = instance.web.list.Column.extend({
this.string = has_value ? _t(button_tips ? button_tips['active']: ''): _t(button_tips ? button_tips['inactive']: '');
return QWeb.render('toggle_button', {
widget: this,
- prefix: instance.session.prefix,
+ prefix: session.prefix,
});
},
});
-})();
+
+/**
+ * Registry for column objects used to format table cells (and some other tasks
+ * e.g. aggregation computations).
+ *
+ * Maps a field or button to a Column type via its ``$tag.$widget``,
+ * ``$tag.$type`` or its ``$tag`` (alone).
+ *
+ * This specific registry has a dedicated utility method ``for_`` taking a
+ * field (from fields_get/fields_view_get.field) and a node (from a view) and
+ * returning the right object *already instantiated from the data provided*.
+ *
+ * @type {instance.web.Registry}
+ */
+list_widget_registry
+ .add('field', Column)
+ .add('field.boolean', ColumnBoolean)
+ .add('field.binary', ColumnBinary)
+ .add('field.char', ColumnChar)
+ .add('field.progressbar', ColumnProgressBar)
+ .add('field.handle', ColumnHandle)
+ .add('button', ColumnButton)
+ .add('field.many2onebutton', ColumnMany2OneButton)
+ .add('field.reference', ColumnReference)
+ .add('field.many2many', ColumnMany2Many)
+ .add('field.url', ColumnUrl)
+ .add('button.toggle_button', ColumnToggleButton);
+
+function for_ (id, field, node) {
+ var description = _.extend({tag: node.tag}, field, node.attrs);
+ var tag = description.tag;
+ var Type = list_widget_registry.get_any([
+ tag + '.' + description.widget,
+ tag + '.'+ description.type,
+ tag
+ ]);
+ return new Type(id, node.tag, description);
+}
+
+return ListView;
+});
+
+
diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js
new file mode 100644
index 00000000000..c4262fbce97
--- /dev/null
+++ b/addons/web/static/src/js/views/list_view_editable.js
@@ -0,0 +1,843 @@
+odoo.define('web.ListEditor', ['web.core', 'web.FormView', 'web.list_common', 'web.ListView', 'web.utils', 'web.Widget'], function (require) {
+"use strict";
+/*---------------------------------------------------------
+ * Odoo Editable List view
+ *---------------------------------------------------------*/
+/**
+ * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
+ * @namespace
+ */
+
+var core = require('web.core');
+var FormView = require('web.FormView');
+var common = require('web.list_common');
+var ListView = require('web.ListView');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+
+
+var Editor = Widget.extend({
+ /**
+ * @constructs instance.web.list.Editor
+ * @extends instance.web.Widget
+ *
+ * Adapter between listview and formview for editable-listview purposes
+ *
+ * @param {instance.web.Widget} parent
+ * @param {Object} options
+ * @param {instance.web.FormView} [options.formView=instance.web.FormView]
+ * @param {Object} [options.delegate]
+ */
+ init: function (parent, options) {
+ this._super(parent);
+ this.options = options || {};
+ _.defaults(this.options, {
+ formView: FormView,
+ delegate: this.getParent()
+ });
+ this.delegate = this.options.delegate;
+
+ this.record = null;
+
+ this.form = new (this.options.formView)(
+ this, this.delegate.dataset, false, {
+ initial_mode: 'edit',
+ disable_autofocus: true,
+ $buttons: $(),
+ $pager: $()
+ });
+ },
+ start: function () {
+ var self = this;
+ var _super = this._super();
+ this.form.embedded_view = this._validate_view(
+ this.delegate.edition_view(this));
+ var form_ready = this.form.appendTo(this.$el).done(
+ self.form.proxy('do_hide'));
+ return $.when(_super, form_ready);
+ },
+ _validate_view: function (edition_view) {
+ if (!edition_view) {
+ throw new Error("editor delegate's #edition_view must return "
+ + "a view descriptor");
+ }
+ var arch = edition_view.arch;
+ if (!(arch && arch.children instanceof Array)) {
+ throw new Error("Editor delegate's #edition_view must have a" +
+ " non-empty arch");
+ }
+ if (arch.tag !== "form") {
+ throw new Error("Editor delegate's #edition_view must have a" +
+ " 'form' root node");
+ }
+ if (!(arch.attrs && arch.attrs.version === "7.0")) {
+ throw new Error("Editor delegate's #edition_view must be a" +
+ " version 7 view");
+ }
+ if (!/\boe_form_container\b/.test(arch.attrs['class'])) {
+ throw new Error("Editor delegate's #edition_view must have the" +
+ " class 'oe_form_container' on its root" +
+ " element");
+ }
+
+ return edition_view;
+ },
+
+ /**
+ *
+ * @param {String} [state] either ``new`` or ``edit``
+ * @return {Boolean}
+ */
+ is_editing: function (state) {
+ if (!this.record) {
+ return false;
+ }
+ switch(state) {
+ case null: case undefined:
+ return true;
+ case 'new': return !this.record.id;
+ case 'edit': return !!this.record.id;
+ }
+ throw new Error("is_editing's state filter must be either `new` or" +
+ " `edit` if provided");
+ },
+ edit: function (record, configureField, options) {
+ // TODO: specify sequence of edit calls
+ var self = this;
+ var form = self.form;
+ var loaded = record
+ ? form.trigger('load_record', _.extend({}, record))
+ : form.load_defaults();
+ return $.when(loaded).then(function () {
+ return form.do_show({reload: false});
+ }).then(function () {
+ self.record = form.datarecord;
+ _(form.fields).each(function (field, name) {
+ configureField(name, field);
+ });
+ return form;
+ });
+ },
+ save: function () {
+ var self = this;
+ return this.form
+ .save(this.delegate.prepends_on_create())
+ .then(function (result) {
+ var created = result.created && !self.record.id;
+ if (created) {
+ self.record.id = result.result;
+ }
+ return self.cancel();
+ });
+ },
+ cancel: function (force) {
+ if (!(force || this.form.can_be_discarded())) {
+ return $.Deferred().reject({
+ message: _t("The form's data can not be discarded")}).promise();
+ }
+ var record = this.record;
+ this.record = null;
+ this.form.do_hide();
+ return $.when(record);
+ },
+});
+
+// editability status of list rows
+ListView.prototype.defaults.editable = null;
+
+// TODO: not sure second @lends on existing item is correct, to check
+ListView.include(/** @lends instance.web.ListView# */{
+ init: function () {
+ var self = this;
+ this._super.apply(this, arguments);
+
+ this.saving_mutex = new utils.Mutex();
+
+ this._force_editability = null;
+ this._context_editable = false;
+ this.editor = this.make_editor();
+ // Stores records of {field, cell}, allows for re-rendering fields
+ // depending on cell state during and after resize events
+ this.fields_for_resize = [];
+ core.bus.on('resize', this, this.resize_fields);
+
+ $(this.groups).bind({
+ 'edit': function (e, id, dataset) {
+ self.do_edit(dataset.index, id, dataset);
+ },
+ 'saved': function () {
+ if (self.groups.get_selection().length) {
+ return;
+ }
+ self.configure_pager(self.dataset);
+ self.compute_aggregates();
+ }
+ });
+
+ this.records.bind('remove', function () {
+ if (self.editor.is_editing()) {
+ self.cancel_edition();
+ }
+ });
+
+ this.on('edit:before', this, function (event) {
+ if (!self.editable() || self.editor.is_editing()) {
+ event.cancel = true;
+ }
+ });
+ this.on('edit:after', this, function () {
+ self.$el.add(self.$buttons).addClass('oe_editing');
+ self.$('.ui-sortable').sortable('disable');
+ });
+ this.on('save:after cancel:after', this, function () {
+ self.$('.ui-sortable').sortable('enable');
+ self.$el.add(self.$buttons).removeClass('oe_editing');
+ });
+ },
+ destroy: function () {
+ core.bus.off('resize', this, this.resize_fields);
+ this._super();
+ },
+ do_hide: function () {
+ if (this.editor.is_editing()) {
+ this.cancel_edition(true);
+ }
+ this._super();
+ },
+ sort_by_column: function (e) {
+ e.stopPropagation();
+ if (!this.editor.is_editing()) {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
+ * Handles the activation of a record in editable mode (making a record
+ * editable), called *after* the record has become editable.
+ *
+ * The default behavior is to setup the listview's dataset to match
+ * whatever dataset was provided by the editing List
+ *
+ * @param {Number} index index of the record in the dataset
+ * @param {Object} id identifier of the record being edited
+ * @param {instance.web.DataSet} dataset dataset in which the record is available
+ */
+ do_edit: function (index, id, dataset) {
+ _.extend(this.dataset, dataset);
+ },
+ do_delete: function (ids) {
+ var nonfalse = _.compact(ids);
+ var _super = this._super.bind(this);
+ var next = this.editor.is_editing()
+ ? this.cancel_edition(true)
+ : $.when();
+ return next.then(function () {
+ return _super(nonfalse);
+ });
+ },
+ editable: function () {
+ return !this.grouped
+ && !this.options.disable_editable_mode
+ && (this.fields_view.arch.attrs.editable
+ || this._context_editable
+ || this.options.editable);
+ },
+ /**
+ * Replace do_search to handle editability process
+ */
+ do_search: function(domain, context, group_by) {
+ var self=this, _super = self._super, args=arguments;
+ var ready = this.editor.is_editing()
+ ? this.cancel_edition(true)
+ : $.when();
+
+ return ready.then(function () {
+ self._context_editable = !!context.set_editable;
+ return _super.apply(self, args);
+ });
+ },
+ /**
+ * Replace do_add_record to handle editability (and adding new record
+ * as an editable row at the top or bottom of the list)
+ */
+ do_add_record: function () {
+ var self = this;
+ if (this.editable()) {
+ this.$el.find('table:first').show();
+ this.$el.find('.oe_view_nocontent').remove();
+ this.start_edition();
+ } else {
+ this._super();
+ }
+ },
+ load_list: function (data, grouped) {
+ var self = this;
+ // tree/@editable takes priority on everything else if present.
+ var result = this._super(data, grouped);
+
+ // In case current editor was started previously, also has to run
+ // when toggling from editable to non-editable in case form widgets
+ // have setup global behaviors expecting themselves to exist
+ // somehow.
+ this.editor.destroy();
+ // Editor is not restartable due to formview not being restartable
+ this.editor = this.make_editor();
+
+ if (this.editable()) {
+ this.$el.addClass('oe_list_editable');
+ // FIXME: any hook available to ensure this is only done once?
+ this.$buttons
+ .off('click', '.oe_list_save')
+ .on('click', '.oe_list_save', this.proxy('save_edition'))
+ .off('click', '.oe_list_discard')
+ .on('click', '.oe_list_discard', function (e) {
+ e.preventDefault();
+ self.cancel_edition();
+ });
+ var editor_ready = this.editor.prependTo(this.$el)
+ .done(this.proxy('setup_events'));
+
+ return $.when(result, editor_ready);
+ } else {
+ this.$el.removeClass('oe_list_editable');
+ }
+
+ return result;
+ },
+ /**
+ * Builds a new editor object
+ *
+ * @return {instance.web.list.Editor}
+ */
+ make_editor: function () {
+ return new Editor(this);
+ },
+ do_button_action: function (name, id, callback) {
+ var self = this, args = arguments;
+ this.ensure_saved().done(function (done) {
+ if (!id && done.created) {
+ id = done.record.get('id');
+ }
+ self.handle_button(name, id, callback);
+ });
+ },
+ /**
+ * Ensures the editable list is saved (saves any pending edition if
+ * needed, or tries to)
+ *
+ * Returns a deferred to the end of the saving.
+ *
+ * @returns {$.Deferred}
+ */
+ ensure_saved: function () {
+ return this.save_edition();
+ },
+ /**
+ * Builds a record with the provided id (``false`` for a creation),
+ * setting all columns with ``false`` value so code which relies on
+ * having an actual value behaves correctly
+ *
+ * @param {*} id
+ * @return {instance.web.list.Record}
+ */
+ make_empty_record: function (id) {
+ var attrs = {id: id};
+ _(this.columns).chain()
+ .filter(function (x) { return x.tag === 'field';})
+ .pluck('name')
+ .each(function (field) { attrs[field] = false; });
+ return new common.Record(attrs);
+ },
+ /**
+ * Set up the edition of a record of the list view "inline"
+ *
+ * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record
+ * @param {Object} [options]
+ * @param {String} [options.focus_field] field to focus at start of edition
+ * @return {jQuery.Deferred}
+ */
+ start_edition: function (record, options) {
+ var self = this;
+ var item = false;
+ if (record) {
+ item = record.attributes;
+ this.dataset.select_id(record.get('id'));
+ } else {
+ record = this.make_empty_record(false);
+ this.records.add(record, {
+ at: this.prepends_on_create() ? 0 : null});
+ }
+ return this.ensure_saved().then(function(){
+ return $.when.apply(null, self.editor.form.render_value_defs);
+ }).then(function () {
+ var $recordRow = self.groups.get_row_for(record);
+ var cells = self.get_cells_for($recordRow);
+ var fields = {};
+ self.fields_for_resize.splice(0, self.fields_for_resize.length);
+ return self.with_event('edit', {
+ record: record.attributes,
+ cancel: false
+ }, function () {
+ return self.editor.edit(item, function (field_name, field) {
+ var cell = cells[field_name];
+ if (!cell) {
+ return;
+ }
+
+ // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
+ field.$el.attr('data-fieldname', field_name);
+ fields[field_name] = field;
+ self.fields_for_resize.push({field: field, cell: cell});
+ }, options).then(function () {
+ $recordRow.addClass('oe_edition');
+ self.resize_fields();
+ var focus_field = options && options.focus_field ? options.focus_field : undefined;
+ if (!focus_field || !fields[focus_field]){
+ focus_field = _.find(self.editor.form.fields_order, function(field){ return fields[field] && fields[field].$el.is(':visible:has(input)'); });
+ }
+ if (focus_field && fields[focus_field]) fields[focus_field].$el.find('input').select();
+ return record.attributes;
+ });
+ }).fail(function () {
+ // if the start_edition event is cancelled and it was a
+ // creation, remove the newly-created empty record
+ if (!record.get('id')) {
+ self.records.remove(record);
+ }
+ });
+ });
+ },
+ get_cells_for: function ($row) {
+ var cells = {};
+ $row.children('td').each(function (index, el) {
+ cells[el.getAttribute('data-field')] = el;
+ });
+ return cells;
+ },
+ /**
+ * If currently editing a row, resizes all registered form fields based
+ * on the corresponding row cell
+ */
+ resize_fields: function () {
+ if (!this.editor.is_editing()) { return; }
+ for(var i=0, len=this.fields_for_resize.length; i<len; ++i) {
+ var item = this.fields_for_resize[i];
+ this.resize_field(item.field, item.cell);
+ }
+ },
+ /**
+ * Resizes a field's root element based on the corresponding cell of
+ * a listview row
+ *
+ * @param {instance.web.form.AbstractField} field
+ * @param {jQuery} cell
+ */
+ resize_field: function (field, cell) {
+ var $cell = $(cell);
+
+ field.set_dimensions($cell.outerHeight(), $cell.outerWidth());
+ field.$el.position({
+ my: 'left top',
+ at: 'left top',
+ of: $cell
+ });
+ if (field.get('effective_readonly')) {
+ field.$el.addClass('oe_readonly');
+ }
+ if(field.widget == "handle")
+ field.$el.addClass('oe_list_field_handle');
+ },
+ /**
+ * @return {jQuery.Deferred}
+ */
+ save_edition: function () {
+ var self = this;
+ return self.saving_mutex.exec(function() {
+ if (!self.editor.is_editing()) {
+ return $.when();
+ }
+ return self.with_event('save', {
+ editor: self.editor,
+ form: self.editor.form,
+ cancel: false
+ }, function () {
+ return self.editor.save().then(function (attrs) {
+ var created = false;
+ var record = self.records.get(attrs.id);
+ if (!record) {
+ // new record
+ created = true;
+ record = self.records.find(function (r) {
+ return !r.get('id');
+ }).set('id', attrs.id);
+ }
+ // onwrite callback could be altering & reloading the
+ // record which has *just* been saved, so first perform all
+ // onwrites then do a final reload of the record
+ return self.handle_onwrite(record)
+ .then(function () {
+ return self.reload_record(record); })
+ .then(function () {
+ return { created: created, record: record }; });
+ });
+ });
+ });
+ },
+ /**
+ * @param {Boolean} [force=false] discards the data even if the form has been edited
+ * @return {jQuery.Deferred}
+ */
+ cancel_edition: function (force) {
+ var self = this;
+ return this.with_event('cancel', {
+ editor: this.editor,
+ form: this.editor.form,
+ cancel: false
+ }, function () {
+ return this.editor.cancel(force).then(function (attrs) {
+ if (attrs.id) {
+ var record = self.records.get(attrs.id);
+ if (!record) {
+ // Record removed by third party during edition
+ return;
+ }
+ return self.reload_record(record, {do_not_evict: true});
+ }
+ var to_delete = self.records.find(function (r) {
+ return !r.get('id');
+ });
+ if (to_delete) {
+ self.records.remove(to_delete);
+ }
+ });
+ });
+ },
+ /**
+ * Executes an action on the view's editor bracketed by a cancellable
+ * event of the name provided.
+ *
+ * The event name provided will be post-fixed with ``:before`` and
+ * ``:after``, the ``event`` parameter will be passed alongside the
+ * ``:before`` variant and if the parameter's ``cancel`` key is set to
+ * ``true`` the action *will not be called* and the method will return
+ * a rejection
+ *
+ * @param {String} event_name name of the event
+ * @param {Object} event event object, provided to ``:before`` sub-event
+ * @param {Function} action callable, called with the view's editor as its context
+ * @param {Array} [args] supplementary arguments provided to the action
+ * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function
+ * @return {jQuery.Deferred}
+ */
+ with_event: function (event_name, event, action) {
+ var self = this;
+ event = event || {};
+ this.trigger(event_name + ':before', event);
+ if (event.cancel) {
+ return $.Deferred().reject({
+ message: _.str.sprintf("Event %s:before cancelled",
+ event_name)});
+ }
+ return $.when(action.call(this)).done(function () {
+ self.trigger.apply(self, [event_name + ':after']
+ .concat(_.toArray(arguments)));
+ });
+ },
+ edition_view: function (editor) {
+ var view = $.extend(true, {}, this.fields_view);
+ view.arch.tag = 'form';
+ _.extend(view.arch.attrs, {
+ 'class': 'oe_form_container',
+ version: '7.0'
+ });
+ _(view.arch.children).chain()
+ .zip(_(this.columns).filter(function (c) {
+ return !(c instanceof ListView.MetaColumn);}))
+ .each(function (ar) {
+ var widget = ar[0], column = ar[1];
+ var modifiers = _.extend({}, column.modifiers);
+ widget.attrs.nolabel = true;
+ if (modifiers['tree_invisible'] || widget.tag === 'button') {
+ modifiers.invisible = true;
+ }
+ widget.attrs.modifiers = JSON.stringify(modifiers);
+ });
+ return view;
+ },
+ handle_onwrite: function (source_record) {
+ var self = this;
+ var on_write_callback = self.fields_view.arch.attrs.on_write;
+ if (!on_write_callback) { return $.when(); }
+ return this.dataset.call(on_write_callback, [source_record.get('id')])
+ .then(function (ids) {
+ return $.when.apply(
+ null, _(ids).map(
+ _.bind(self.handle_onwrite_record, self, source_record)));
+ });
+ },
+ handle_onwrite_record: function (source_record, id) {
+ var record = this.records.get(id);
+ if (!record) {
+ // insert after the source record
+ var index = this.records.indexOf(source_record) + 1;
+ record = this.make_empty_record(id);
+ this.records.add(record, {at: index});
+ }
+ return this.reload_record(record);
+ },
+ prepends_on_create: function () {
+ return this.editable() === 'top';
+ },
+ setup_events: function () {
+ var self = this;
+ _.each(this.editor.form.fields, function(field, field_name) {
+ field.on("change:effective_readonly", self, function(){
+ var item = _(self.fields_for_resize).find(function (item) {
+ return item.field === field;
+ });
+ if (item) {
+ setTimeout(function() {
+ self.resize_field(item.field, item.cell);
+ }, 0);
+ }
+
+ });
+ });
+
+ this.editor.$el.on('keyup keypress keydown', function (e) {
+ if (!self.editor.is_editing()) { return true; }
+ var key = _($.ui.keyCode).chain()
+ .map(function (v, k) { return {name: k, code: v}; })
+ .find(function (o) { return o.code === e.which; })
+ .value();
+ if (!key) { return true; }
+ var method = e.type + '_' + key.name;
+ if (!(method in self)) { return true; }
+ return self[method](e);
+ });
+ },
+ /**
+ * Saves the current record, and goes to the next one (creation or
+ * edition)
+ *
+ * @private
+ * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit
+ * @param {Object} [options]
+ * @param {String} [options.focus_field]
+ * @return {*}
+ */
+ _next: function (next_record, options) {
+ next_record = next_record || 'succ';
+ var self = this;
+ return this.save_edition().then(function (saveInfo) {
+ if (!saveInfo) { return null; }
+ if (saveInfo.created) {
+ return self.start_edition();
+ }
+ var record = self.records[next_record](
+ saveInfo.record, {wraparound: true});
+ return self.start_edition(record, options);
+ });
+ },
+ keypress_ENTER: function () {
+ return this._next();
+ },
+ keydown_ESCAPE: function (e) {
+ return false;
+ },
+ keyup_ESCAPE: function (e) {
+ return this.cancel_edition();
+ },
+ /**
+ * Gets the selection range (start, end) for the provided element,
+ * returns ``null`` if it can't get a range.
+ *
+ * @private
+ */
+ _text_selection_range: function (el) {
+ var selectionStart;
+ try {
+ selectionStart = el.selectionStart;
+ } catch (e) {
+ // radio or checkbox throw on selectionStart access
+ return null;
+ }
+ if (selectionStart !== undefined) {
+ return {
+ start: selectionStart,
+ end: el.selectionEnd
+ };
+ } else if (document.body.createTextRange) {
+ throw new Error("Implement text range handling for MSIE");
+ }
+ // Element without selection ranges (select, div/@contenteditable)
+ return null;
+ },
+ _text_cursor: function (el) {
+ var selection = this._text_selection_range(el);
+ if (!selection) {
+ return null;
+ }
+ if (selection.start !== selection.end) {
+ return {position: null, collapsed: false};
+ }
+ return {position: selection.start, collapsed: true};
+ },
+ /**
+ * Checks if the cursor is at the start of the provided el
+ *
+ * @param {HTMLInputElement | HTMLTextAreaElement}
+ * @returns {Boolean}
+ * @private
+ */
+ _at_start: function (cursor, el) {
+ return cursor.collapsed && (cursor.position === 0);
+ },
+ /**
+ * Checks if the cursor is at the end of the provided el
+ *
+ * @param {HTMLInputElement | HTMLTextAreaElement}
+ * @returns {Boolean}
+ * @private
+ */
+ _at_end: function (cursor, el) {
+ return cursor.collapsed && (cursor.position === el.value.length);
+ },
+ /**
+ * @param DOMEvent event
+ * @param {String} record_direction direction to move into to get the next record (pred | succ)
+ * @param {Function} is_valid_move whether the edition should be moved to the next record
+ * @private
+ */
+ _key_move_record: function (event, record_direction, is_valid_move) {
+ if (!this.editor.is_editing('edit')) { return $.when(); }
+ var cursor = this._text_cursor(event.target);
+ // if text-based input (has a cursor)
+ // and selecting (not collapsed) or not at a field boundary
+ // don't move to the next record
+ if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); }
+
+ event.preventDefault();
+ var source_field = $(event.target).closest('[data-fieldname]')
+ .attr('data-fieldname');
+ return this._next(record_direction, {focus_field: source_field});
+
+ },
+ keydown_UP: function (e) {
+ var self = this;
+ return this._key_move_record(e, 'pred', function (el, cursor) {
+ return self._at_start(cursor, el);
+ });
+ },
+ keydown_DOWN: function (e) {
+ var self = this;
+ return this._key_move_record(e, 'succ', function (el, cursor) {
+ return self._at_end(cursor, el);
+ });
+ },
+
+ keydown_LEFT: function (e) {
+ // If the cursor is at the beginning of the field
+ var source_field = $(e.target).closest('[data-fieldname]')
+ .attr('data-fieldname');
+ var cursor = this._text_cursor(e.target);
+ if (cursor && !this._at_start(cursor, e.target)) { return $.when(); }
+
+ var fields_order = this.editor.form.fields_order;
+ var field_index = _(fields_order).indexOf(source_field);
+
+ // Look for the closest visible form field to the left
+ var fields = this.editor.form.fields;
+ var field;
+ do {
+ if (--field_index < 0) { return $.when(); }
+
+ field = fields[fields_order[field_index]];
+ } while (!field.$el.is(':visible'));
+
+ // and focus it
+ field.focus();
+ return $.when();
+ },
+ keydown_RIGHT: function (e) {
+ // same as above, but with cursor at the end of the field and
+ // looking for new fields at the right
+ var source_field = $(e.target).closest('[data-fieldname]')
+ .attr('data-fieldname');
+ var cursor = this._text_cursor(e.target);
+ if (cursor && !this._at_end(cursor, e.target)) { return $.when(); }
+
+ var fields_order = this.editor.form.fields_order;
+ var field_index = _(fields_order).indexOf(source_field);
+
+ var fields = this.editor.form.fields;
+ var field;
+ do {
+ if (++field_index >= fields_order.length) { return $.when(); }
+
+ field = fields[fields_order[field_index]];
+ } while (!field.$el.is(':visible'));
+
+ field.focus();
+ return $.when();
+ },
+ keydown_TAB: function (e) {
+ var form = this.editor.form;
+ var last_field = _(form.fields_order).chain()
+ .map(function (name) { return form.fields[name]; })
+ .filter(function (field) { return field.$el.is(':visible') && !field.get('effective_readonly'); })
+ .last()
+ .value();
+ // tabbed from last field in form
+ if (last_field && last_field.$el.has(e.target).length) {
+ e.preventDefault();
+ return this._next();
+ }
+ return $.when();
+ }
+});
+
+
+ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
+ passthrough_events: ListView.Groups.prototype.passthrough_events + " edit saved",
+ get_row_for: function (record) {
+ return _(this.children).chain()
+ .invoke('get_row_for', record)
+ .compact()
+ .first()
+ .value();
+ }
+});
+
+ListView.List.include(/** @lends instance.web.ListView.List# */{
+ row_clicked: function (event) {
+ if (!this.view.editable() || ! this.view.is_action_enabled('edit')) {
+ return this._super.apply(this, arguments);
+ }
+ var record_id = $(event.currentTarget).data('id');
+ return this.view.start_edition(
+ record_id ? this.records.get(record_id) : null, {
+ focus_field: $(event.target).not(".oe_readonly").data('field')
+ });
+ },
+ /**
+ * If a row mapping to the record (@data-id matching the record's id or
+ * no @data-id if the record has no id), returns it. Otherwise returns
+ * ``null``.
+ *
+ * @param {Record} record the record to get a row for
+ * @return {jQuery|null}
+ */
+ get_row_for: function (record) {
+ var $row = this.$current.children('[data-id=' + record.get('id') + ']');
+ if ($row.length) {
+ return $row;
+ }
+ return null;
+ }
+});
+
+return Editor;
+
+});
diff --git a/addons/web/static/src/js/view_pivot.js b/addons/web/static/src/js/views/pivot_view.js
index 1f12347fb42..8ca6e2976fb 100644
--- a/addons/web/static/src/js/view_pivot.js
+++ b/addons/web/static/src/js/views/pivot_view.js
@@ -1,20 +1,25 @@
+odoo.define('web.PivotView', ['web.core', 'web.crash_manager', 'web.formats', 'web.framework', 'web.Model', 'web.session', 'web.Sidebar', 'web.utils', 'web.View'], function (require) {
+"use strict";
/*---------------------------------------------------------
* Odoo Pivot Table view
*---------------------------------------------------------*/
-(function () {
-'use strict';
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var formats = require('web.formats');
+var framework = require('web.framework');
+var Model = require('web.Model');
+var session = require('web.session');
+var Sidebar = require('web.Sidebar');
+var utils = require('web.utils');
+var View = require('web.View');
-var instance = openerp,
- _lt = instance.web._lt,
- _t = instance.web._t,
- QWeb = instance.web.qweb,
- format_value = instance.web.format_value,
- total = _t("Total");
+var _lt = core._lt;
+var _t = core._t;
+var QWeb = core.qweb;
+var total = _t("Total");
-instance.web.views.add('pivot', 'instance.web.PivotView');
-
-instance.web.PivotView = instance.web.View.extend({
+var PivotView = View.extend({
template: 'PivotView',
display_name: _lt('Pivot'),
view_type: 'pivot',
@@ -28,7 +33,7 @@ instance.web.PivotView = instance.web.View.extend({
init: function(parent, dataset, view_id, options) {
this._super(parent, dataset, view_id, options);
- this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true});
+ this.model = new Model(dataset.model, {group_by_no_leaf: true});
this.$sidebar = options.$sidebar;
this.fields = {};
@@ -65,9 +70,9 @@ instance.web.PivotView = instance.web.View.extend({
.then(this.prepare_fields.bind(this));
if (this.$sidebar) {
- openerp.session.rpc('/web/pivot/check_xlwt').then(function (result) {
+ this.rpc('/web/pivot/check_xlwt').then(function (result) {
if (result) {
- self.sidebar = new instance.web.Sidebar(self);
+ self.sidebar = new Sidebar(self);
self.sidebar.appendTo(self.$sidebar);
self.sidebar.add_items('other', [{
label: _t("Download xls"),
@@ -90,7 +95,7 @@ instance.web.PivotView = instance.web.View.extend({
var another_ctx = {fields: _.chain(this.groupable_fields).pairs().sortBy(function(f){return f[1].string;}).value()};
this.$field_selection = this.$('.o-field-selection');
this.$field_selection.html(QWeb.render('PivotView.FieldSelection', another_ctx));
- openerp.web.bus.on('click', self, function () {
+ core.bus.on('click', self, function () {
self.$field_selection.find('ul').first().hide();
});
this.$buttons.find('button').tooltip();
@@ -284,7 +289,6 @@ instance.web.PivotView = instance.web.View.extend({
}
},
on_field_menu_selection: function (event) {
- var self = this;
var field = $(event.target).parent().data('field'),
interval = $(event.target).data('interval'),
header = this.headers[this.last_header_selected];
@@ -299,7 +303,6 @@ instance.web.PivotView = instance.web.View.extend({
var self = this;
var other_root = header.root.other_root,
- this_groupbys = header.root.groupbys,
other_groupbys = header.root.other_root.groupbys,
fields = [].concat(field, other_groupbys, this.active_measures),
groupbys = [];
@@ -481,7 +484,7 @@ instance.web.PivotView = instance.web.View.extend({
parent = value.length ? find_path_in_tree(root, path) : null;
}
var header = {
- id: generate_id(),
+ id: utils.generate_id(),
expanded: false,
domain: data_pt.model._domain,
children: [],
@@ -531,8 +534,7 @@ instance.web.PivotView = instance.web.View.extend({
},
draw_headers: function ($thead, headers) {
var self = this,
- i, j, cell, $row, $cell,
- display_total = this.main_col.width > 1;
+ i, j, cell, $row, $cell;
var groupby_labels = _.map(this.main_col.groupbys, function (gb) {
return self.groupable_fields[gb.split(':')[0]].string;
@@ -593,7 +595,7 @@ instance.web.PivotView = instance.web.View.extend({
if (rows[i].indent > 0) $header.attr('title', groupby_labels[rows[i].indent - 1]);
$header.appendTo($row);
for (j = 0; j < length; j++) {
- value = format_value(rows[i].values[j], {type: measure_types[j % nbr_measures]});
+ value = formats.format_value(rows[i].values[j], {type: measure_types[j % nbr_measures]});
$cell = $('<td>')
.data('id', rows[i].id)
.data('col_id', rows[i].col_ids[Math.floor(j / nbr_measures)])
@@ -678,8 +680,7 @@ instance.web.PivotView = instance.web.View.extend({
compute_rows: function () {
var self = this,
aggregates, i,
- result = [],
- nbr_measures = this.active_measures.length;
+ result = [];
traverse_tree(this.main_row.root, function (header) {
var values = [],
col_ids = [];
@@ -732,7 +733,7 @@ instance.web.PivotView = instance.web.View.extend({
}
},
download_table: function () {
- openerp.web.blockUI();
+ framework.blockUI();
var nbr_measures = this.active_measures.length,
headers = this.compute_headers(),
measure_row = nbr_measures > 1 ? _.last(headers) : [],
@@ -760,23 +761,15 @@ instance.web.PivotView = instance.web.View.extend({
nbr_measures: nbr_measures,
title: this.title,
};
- var c = openerp.webclient.crashmanager;
- this.session.get_file({
+ session.get_file({
url: '/web/pivot/export_xls',
data: {data: JSON.stringify(table)},
- complete: openerp.web.unblockUI,
- error: c.rpc_error.bind(c)
+ complete: framework.unblockUI,
+ error: crash_manager.rpc_error.bind(crash_manager)
});
},
});
-// helpers
-var id = -1;
-
-function generate_id () {
- return ++id;
-}
-
function traverse_tree(root, f, arg1, arg2, arg3) {
f(root, arg1, arg2, arg3);
if (!root.expanded) return;
@@ -799,4 +792,9 @@ function find_path_in_tree(root, path) {
return null;
}
-})();
+core.view_registry.add('pivot', PivotView);
+
+return PivotView;
+
+});
+
diff --git a/addons/web/static/src/js/views/search_filters.js b/addons/web/static/src/js/views/search_filters.js
new file mode 100644
index 00000000000..dfc0d0bf4bc
--- /dev/null
+++ b/addons/web/static/src/js/views/search_filters.js
@@ -0,0 +1,311 @@
+odoo.define('web.search_filters', ['web.core', 'web.datepicker', 'web.formats', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var datepicker = require('web.datepicker');
+var formats = require('web.formats');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var _lt = core._lt;
+
+var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{
+ template: 'SearchView.extended_search.proposition',
+ events: {
+ 'change .searchview_extended_prop_field': 'changed',
+ 'change .searchview_extended_prop_op': 'operator_changed',
+ 'click .searchview_extended_delete_prop': function (e) {
+ e.stopPropagation();
+ this.getParent().remove_proposition(this);
+ },
+ },
+ /**
+ * @constructs instance.web.search.ExtendedSearchProposition
+ * @extends instance.web.Widget
+ *
+ * @param parent
+ * @param fields
+ */
+ init: function (parent, fields) {
+ this._super(parent);
+ this.fields = _(fields).chain()
+ .map(function(val, key) { return _.extend({}, val, {'name': key}); })
+ .filter(function (field) { return !field.deprecated && field.searchable; })
+ .sortBy(function(field) {return field.string;})
+ .value();
+ this.attrs = {_: _, fields: this.fields, selected: null};
+ this.value = null;
+ },
+ start: function () {
+ return this._super().done(this.proxy('changed'));
+ },
+ changed: function() {
+ var nval = this.$(".searchview_extended_prop_field").val();
+ if(this.attrs.selected === null || this.attrs.selected === undefined || nval != this.attrs.selected.name) {
+ this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
+ }
+ },
+ operator_changed: function (e) {
+ var $value = this.$('.searchview_extended_prop_value');
+ switch ($(e.target).val()) {
+ case '∃':
+ case '∄':
+ $value.hide();
+ break;
+ default:
+ $value.show();
+ }
+ },
+ /**
+ * Selects the provided field object
+ *
+ * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
+ */
+ select_field: function(field) {
+ var self = this;
+ if(this.attrs.selected !== null && this.attrs.selected !== undefined) {
+ this.value.destroy();
+ this.value = null;
+ this.$('.searchview_extended_prop_op').html('');
+ }
+ this.attrs.selected = field;
+ if(field === null || field === undefined) {
+ return;
+ }
+
+ var type = field.type;
+ var Field = core.search_filters_registry.get_any([type, "char"]);
+
+ this.value = new Field(this, field);
+ _.each(this.value.operators, function(operator) {
+ $('<option>', {value: operator.value})
+ .text(String(operator.text))
+ .appendTo(self.$('.searchview_extended_prop_op'));
+ });
+ var $value_loc = this.$('.searchview_extended_prop_value').show().empty();
+ this.value.appendTo($value_loc);
+
+ },
+ get_filter: function () {
+ if (this.attrs.selected === null || this.attrs.selected === undefined)
+ return null;
+ var field = this.attrs.selected,
+ op_select = this.$('.searchview_extended_prop_op')[0],
+ operator = op_select.options[op_select.selectedIndex];
+
+ return {
+ attrs: {
+ domain: [this.value.get_domain(field, operator)],
+ string: this.value.get_label(field, operator),
+ },
+ children: [],
+ tag: 'filter',
+ };
+ },
+});
+
+ExtendedSearchProposition.Field = Widget.extend({
+ init: function (parent, field) {
+ this._super(parent);
+ this.field = field;
+ },
+ get_label: function (field, operator) {
+ var format;
+ switch (operator.value) {
+ case '∃': case '∄': format = _t('%(field)s %(operator)s'); break;
+ default: format = _t('%(field)s %(operator)s "%(value)s"'); break;
+ }
+ return this.format_label(format, field, operator);
+ },
+ format_label: function (format, field, operator) {
+ return _.str.sprintf(format, {
+ field: field.string,
+ // According to spec, HTMLOptionElement#label should return
+ // HTMLOptionElement#text when not defined/empty, but it does
+ // not in older Webkit (between Safari 5.1.5 and Chrome 17) and
+ // Gecko (pre Firefox 7) browsers, so we need a manual fallback
+ // for those
+ operator: operator.label || operator.text,
+ value: this
+ });
+ },
+ get_domain: function (field, operator) {
+ switch (operator.value) {
+ case '∃': return this.make_domain(field.name, '!=', false);
+ case '∄': return this.make_domain(field.name, '=', false);
+ default: return this.make_domain(
+ field.name, operator.value, this.get_value());
+ }
+ },
+ make_domain: function (field, operator, value) {
+ return [field, operator, value];
+ },
+ /**
+ * Returns a human-readable version of the value, in case the "logical"
+ * and the "semantic" values of a field differ (as for selection fields,
+ * for instance).
+ *
+ * The default implementation simply returns the value itself.
+ *
+ * @return {String} human-readable version of the value
+ */
+ toString: function () {
+ return this.get_value();
+ }
+});
+
+ExtendedSearchProposition.Char = ExtendedSearchProposition.Field.extend({
+ template: 'SearchView.extended_search.proposition.char',
+ operators: [
+ {value: "ilike", text: _lt("contains")},
+ {value: "not ilike", text: _lt("doesn't contain")},
+ {value: "=", text: _lt("is equal to")},
+ {value: "!=", text: _lt("is not equal to")},
+ {value: "∃", text: _lt("is set")},
+ {value: "∄", text: _lt("is not set")}
+ ],
+ get_value: function() {
+ return this.$el.val();
+ }
+});
+
+ExtendedSearchProposition.DateTime = ExtendedSearchProposition.Field.extend({
+ template: 'SearchView.extended_search.proposition.empty',
+ operators: [
+ {value: "=", text: _lt("is equal to")},
+ {value: "!=", text: _lt("is not equal to")},
+ {value: ">", text: _lt("greater than")},
+ {value: "<", text: _lt("less than")},
+ {value: ">=", text: _lt("greater or equal than")},
+ {value: "<=", text: _lt("less or equal than")},
+ {value: "∃", text: _lt("is set")},
+ {value: "∄", text: _lt("is not set")}
+ ],
+ widget: function () { return datepicker.DateTimeWidget; },
+ get_value: function() {
+ return this.datewidget.get_value();
+ },
+ toString: function () {
+ return formats.format_value(this.get_value(), { type:"datetime" });
+ },
+ start: function() {
+ var ready = this._super();
+ this.datewidget = new (this.widget())(this);
+ this.datewidget.appendTo(this.$el);
+ return ready;
+ }
+});
+
+ExtendedSearchProposition.Date = ExtendedSearchProposition.DateTime.extend({
+ widget: function () { return datepicker.DateWidget; },
+ toString: function () {
+ return formats.format_value(this.get_value(), { type:"date" });
+ }
+});
+
+ExtendedSearchProposition.Integer = ExtendedSearchProposition.Field.extend({
+ template: 'SearchView.extended_search.proposition.integer',
+ operators: [
+ {value: "=", text: _lt("is equal to")},
+ {value: "!=", text: _lt("is not equal to")},
+ {value: ">", text: _lt("greater than")},
+ {value: "<", text: _lt("less than")},
+ {value: ">=", text: _lt("greater or equal than")},
+ {value: "<=", text: _lt("less or equal than")},
+ {value: "∃", text: _lt("is set")},
+ {value: "∄", text: _lt("is not set")}
+ ],
+ toString: function () {
+ return this.$el.val();
+ },
+ get_value: function() {
+ try {
+ var val =this.$el.val();
+ return formats.parse_value(val === "" ? 0 : val, {'widget': 'integer'});
+ } catch (e) {
+ return "";
+ }
+ }
+});
+
+ExtendedSearchProposition.Id = ExtendedSearchProposition.Integer.extend({
+ operators: [{value: "=", text: _lt("is")}]
+});
+
+ExtendedSearchProposition.Float = ExtendedSearchProposition.Field.extend({
+ template: 'SearchView.extended_search.proposition.float',
+ operators: [
+ {value: "=", text: _lt("is equal to")},
+ {value: "!=", text: _lt("is not equal to")},
+ {value: ">", text: _lt("greater than")},
+ {value: "<", text: _lt("less than")},
+ {value: ">=", text: _lt("greater or equal than")},
+ {value: "<=", text: _lt("less or equal than")},
+ {value: "∃", text: _lt("is set")},
+ {value: "∄", text: _lt("is not set")}
+ ],
+ toString: function () {
+ return this.$el.val();
+ },
+ get_value: function() {
+ try {
+ var val =this.$el.val();
+ return formats.parse_value(val === "" ? 0.0 : val, {'widget': 'float'});
+ } catch (e) {
+ return "";
+ }
+ }
+});
+
+ExtendedSearchProposition.Selection = ExtendedSearchProposition.Field.extend({
+ template: 'SearchView.extended_search.proposition.selection',
+ operators: [
+ {value: "=", text: _lt("is")},
+ {value: "!=", text: _lt("is not")},
+ {value: "∃", text: _lt("is set")},
+ {value: "∄", text: _lt("is not set")}
+ ],
+ toString: function () {
+ var select = this.$el[0];
+ var option = select.options[select.selectedIndex];
+ return option.label || option.text;
+ },
+ get_value: function() {
+ return this.$el.val();
+ }
+});
+
+ExtendedSearchProposition.Boolean = ExtendedSearchProposition.Field.extend({
+ template: 'SearchView.extended_search.proposition.empty',
+ operators: [
+ {value: "=", text: _lt("is true")},
+ {value: "!=", text: _lt("is false")}
+ ],
+ get_label: function (field, operator) {
+ return this.format_label(
+ _t('%(field)s %(operator)s'), field, operator);
+ },
+ get_value: function() {
+ return true;
+ }
+});
+
+core.search_filters_registry
+ .add('char', ExtendedSearchProposition.Char)
+ .add('text', ExtendedSearchProposition.Char)
+ .add('one2many', ExtendedSearchProposition.Char)
+ .add('many2one', ExtendedSearchProposition.Char)
+ .add('many2many', ExtendedSearchProposition.Char)
+ .add('datetime', ExtendedSearchProposition.DateTime)
+ .add('date', ExtendedSearchProposition.Date)
+ .add('integer', ExtendedSearchProposition.Integer)
+ .add('float', ExtendedSearchProposition.Float)
+ .add('boolean', ExtendedSearchProposition.Boolean)
+ .add('selection', ExtendedSearchProposition.Selection)
+ .add('id', ExtendedSearchProposition.Id);
+
+return {
+ ExtendedSearchProposition: ExtendedSearchProposition
+};
+
+});
diff --git a/addons/web/static/src/js/views/search_inputs.js b/addons/web/static/src/js/views/search_inputs.js
new file mode 100644
index 00000000000..97678cc60c8
--- /dev/null
+++ b/addons/web/static/src/js/views/search_inputs.js
@@ -0,0 +1,736 @@
+odoo.define('web.search_inputs', ['web.core', 'web.data', 'web.formats', 'web.Model', 'web.pyeval', 'web.time', 'web.utils', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var formats = require('web.formats');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var time = require('web.time');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var _lt = core._lt;
+
+var Input = Widget.extend( /** @lends instance.web.search.Input# */{
+ /**
+ * @constructs instance.web.search.Input
+ * @extends instance.web.Widget
+ *
+ * @param parent
+ */
+ init: function (parent) {
+ this._super(parent);
+ this.load_attrs({});
+ },
+ /**
+ * Fetch auto-completion values for the widget.
+ *
+ * The completion values should be an array of objects with keys category,
+ * label, value prefixed with an object with keys type=section and label
+ *
+ * @param {String} value value to complete
+ * @returns {jQuery.Deferred<null|Array>}
+ */
+ complete: function (value) {
+ return $.when(null);
+ },
+ /**
+ * Returns a Facet instance for the provided defaults if they apply to
+ * this widget, or null if they don't.
+ *
+ * This default implementation will try calling
+ * :js:func:`instance.web.search.Input#facet_for` if the widget's name
+ * matches the input key
+ *
+ * @param {Object} defaults
+ * @returns {jQuery.Deferred<null|Object>}
+ */
+ facet_for_defaults: function (defaults) {
+ if (!this.attrs ||
+ !(this.attrs.name in defaults && defaults[this.attrs.name])) {
+ return $.when(null);
+ }
+ return this.facet_for(defaults[this.attrs.name]);
+ },
+ get_context: function () {
+ throw new Error(
+ "get_context not implemented for widget " + this.attrs.type);
+ },
+ get_groupby: function () {
+ throw new Error(
+ "get_groupby not implemented for widget " + this.attrs.type);
+ },
+ get_domain: function () {
+ throw new Error(
+ "get_domain not implemented for widget " + this.attrs.type);
+ },
+ load_attrs: function (attrs) {
+ attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
+ this.attrs = attrs;
+ },
+ /**
+ * Returns whether the input is "visible". The default behavior is to
+ * query the ``modifiers.invisible`` flag on the input's description or
+ * view node.
+ *
+ * @returns {Boolean}
+ */
+ visible: function () {
+ return !this.attrs.modifiers.invisible;
+ },
+});
+
+var Field = Input.extend( /** @lends instance.web.search.Field# */ {
+ template: 'SearchView.field',
+ default_operator: '=',
+ /**
+ * @constructs instance.web.search.Field
+ * @extends instance.web.search.Input
+ *
+ * @param view_section
+ * @param field
+ * @param parent
+ */
+ init: function (view_section, field, parent) {
+ this._super(parent);
+ this.load_attrs(_.extend({}, field, view_section.attrs));
+ },
+ facet_for: function (value) {
+ return $.when({
+ field: this,
+ category: this.attrs.string || this.attrs.name,
+ values: [{label: String(value), value: value}]
+ });
+ },
+ value_from: function (facetValue) {
+ return facetValue.get('value');
+ },
+ get_context: function (facet) {
+ var self = this;
+ // A field needs a context to send when active
+ var context = this.attrs.context;
+ if (_.isEmpty(context) || !facet.values.length) {
+ return;
+ }
+ var contexts = facet.values.map(function (facetValue) {
+ return new data.CompoundContext(context)
+ .set_eval_context({self: self.value_from(facetValue)});
+ });
+
+ if (contexts.length === 1) { return contexts[0]; }
+
+ return _.extend(new data.CompoundContext(), {
+ __contexts: contexts
+ });
+ },
+ get_groupby: function () { },
+ /**
+ * Function creating the returned domain for the field, override this
+ * methods in children if you only need to customize the field's domain
+ * without more complex alterations or tests (and without the need to
+ * change override the handling of filter_domain)
+ *
+ * @param {String} name the field's name
+ * @param {String} operator the field's operator (either attribute-specified or default operator for the field
+ * @param {Number|String} facet parsed value for the field
+ * @returns {Array<Array>} domain to include in the resulting search
+ */
+ make_domain: function (name, operator, facet) {
+ return [[name, operator, this.value_from(facet)]];
+ },
+ get_domain: function (facet) {
+ if (!facet.values.length) { return; }
+
+ var value_to_domain;
+ var self = this;
+ var domain = this.attrs.filter_domain;
+ if (domain) {
+ value_to_domain = function (facetValue) {
+ return new data.CompoundDomain(domain)
+ .set_eval_context({self: self.value_from(facetValue)});
+ };
+ } else {
+ value_to_domain = function (facetValue) {
+ return self.make_domain(
+ self.attrs.name,
+ self.attrs.operator || self.default_operator,
+ facetValue);
+ };
+ }
+ var domains = facet.values.map(value_to_domain);
+
+ if (domains.length === 1) { return domains[0]; }
+ for (var i = domains.length; --i;) {
+ domains.unshift(['|']);
+ }
+
+ return _.extend(new data.CompoundDomain(), {
+ __domains: domains
+ });
+ }
+});
+
+/**
+ * Implementation of the ``char`` OpenERP field type:
+ *
+ * * Default operator is ``ilike`` rather than ``=``
+ *
+ * * The Javascript and the HTML values are identical (strings)
+ *
+ * @class
+ * @extends instance.web.search.Field
+ */
+var CharField = Field.extend( /** @lends instance.web.search.CharField# */ {
+ default_operator: 'ilike',
+ complete: function (value) {
+ if (_.isEmpty(value)) { return $.when(null); }
+ var label = _.str.sprintf(_.str.escapeHTML(
+ _t("Search %(field)s for: %(value)s")), {
+ field: '<em>' + _.escape(this.attrs.string) + '</em>',
+ value: '<strong>' + _.escape(value) + '</strong>'});
+ return $.when([{
+ label: label,
+ facet: {
+ category: this.attrs.string,
+ field: this,
+ values: [{label: value, value: value}]
+ }
+ }]);
+ }
+});
+
+var NumberField = Field.extend(/** @lends instance.web.search.NumberField# */{
+ complete: function (value) {
+ var val = this.parse(value);
+ if (isNaN(val)) { return $.when(); }
+ var label = _.str.sprintf(
+ _t("Search %(field)s for: %(value)s"), {
+ field: '<em>' + _.escape(this.attrs.string) + '</em>',
+ value: '<strong>' + _.escape(value) + '</strong>'});
+ return $.when([{
+ label: label,
+ facet: {
+ category: this.attrs.string,
+ field: this,
+ values: [{label: value, value: val}]
+ }
+ }]);
+ },
+});
+
+/**
+ * @class
+ * @extends instance.web.search.NumberField
+ */
+var IntegerField = NumberField.extend(/** @lends instance.web.search.IntegerField# */{
+ error_message: _t("not a valid integer"),
+ parse: function (value) {
+ try {
+ return formats.parse_value(value, {'widget': 'integer'});
+ } catch (e) {
+ return NaN;
+ }
+ }
+});
+
+/**
+ * @class
+ * @extends instance.web.search.NumberField
+ */
+var FloatField = NumberField.extend(/** @lends instance.web.search.FloatField# */{
+ error_message: _t("not a valid number"),
+ parse: function (value) {
+ try {
+ return formats.parse_value(value, {'widget': 'float'});
+ } catch (e) {
+ return NaN;
+ }
+ }
+});
+
+/**
+ * Utility function for m2o & selection fields taking a selection/name_get pair
+ * (value, name) and converting it to a Facet descriptor
+ *
+ * @param {instance.web.search.Field} field holder field
+ * @param {Array} pair pair value to convert
+ */
+function facet_from(field, pair) {
+ return {
+ field: field,
+ category: field.attrs.string,
+ values: [{label: pair[1], value: pair[0]}]
+ };
+}
+
+/**
+ * @class
+ * @extends instance.web.search.Field
+ */
+var SelectionField = Field.extend(/** @lends instance.web.search.SelectionField# */{
+ // This implementation is a basic <select> field, but it may have to be
+ // altered to be more in line with the GTK client, which uses a combo box
+ // (~ jquery.autocomplete):
+ // * If an option was selected in the list, behave as currently
+ // * If something which is not in the list was entered (via the text input),
+ // the default domain should become (`ilike` string_value) but **any
+ // ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
+ // is specified. So at least get_domain needs to be quite a bit
+ // overridden (if there's no @value and there is no filter_domain and
+ // there is no @operator, return [[name, 'ilike', str_val]]
+ template: 'SearchView.field.selection',
+ init: function () {
+ this._super.apply(this, arguments);
+ // prepend empty option if there is no empty option in the selection list
+ this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
+ return !item[1];
+ });
+ },
+ complete: function (needle) {
+ var self = this;
+ var results = _(this.attrs.selection).chain()
+ .filter(function (sel) {
+ var value = sel[0], label = sel[1];
+ if (value === undefined || !label) { return false; }
+ return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
+ })
+ .map(function (sel) {
+ return {
+ label: _.escape(sel[1]),
+ indent: true,
+ facet: facet_from(self, sel)
+ };
+ }).value();
+ if (_.isEmpty(results)) { return $.when(null); }
+ return $.when.call(null, [{
+ label: _.escape(this.attrs.string)
+ }].concat(results));
+ },
+ facet_for: function (value) {
+ var match = _(this.attrs.selection).detect(function (sel) {
+ return sel[0] === value;
+ });
+ if (!match) { return $.when(null); }
+ return $.when(facet_from(this, match));
+ }
+});
+
+var BooleanField = SelectionField.extend(/** @lends instance.web.search.BooleanField# */{
+ /**
+ * @constructs instance.web.search.BooleanField
+ * @extends instance.web.search.BooleanField
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.attrs.selection = [
+ [true, _t("Yes")],
+ [false, _t("No")]
+ ];
+ }
+});
+
+/**
+ * @class
+ * @extends instance.web.search.DateField
+ */
+var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
+ value_from: function (facetValue) {
+ return time.date_to_str(facetValue.get('value'));
+ },
+ complete: function (needle) {
+ var m = moment(needle);
+ if (!m.isValid()) { return $.when(null); }
+ var d = m.toDate();
+ var date_string = formats.format_value(d, this.attrs);
+ var label = _.str.sprintf(_.str.escapeHTML(
+ _t("Search %(field)s at: %(value)s")), {
+ field: '<em>' + _.escape(this.attrs.string) + '</em>',
+ value: '<strong>' + date_string + '</strong>'});
+ return $.when([{
+ label: label,
+ facet: {
+ category: this.attrs.string,
+ field: this,
+ values: [{label: date_string, value: d}]
+ }
+ }]);
+ }
+});
+
+/**
+ * Implementation of the ``datetime`` openerp field type:
+ *
+ * * Uses the same widget as the ``date`` field type (a simple date)
+ *
+ * * Builds a slighly more complex, it's a datetime range (includes time)
+ * spanning the whole day selected by the date widget
+ *
+ * @class
+ * @extends instance.web.DateField
+ */
+var DateTimeField = DateField.extend(/** @lends instance.web.search.DateTimeField# */{
+ value_from: function (facetValue) {
+ return time.datetime_to_str(facetValue.get('value'));
+ }
+});
+
+var ManyToOneField = CharField.extend({
+ default_operator: {},
+ init: function (view_section, field, parent) {
+ this._super(view_section, field, parent);
+ this.model = new Model(this.attrs.relation);
+ this.searchview = parent;
+ },
+
+ complete: function (value) {
+ if (_.isEmpty(value)) { return $.when(null); }
+ var label = _.str.sprintf(_.str.escapeHTML(
+ _t("Search %(field)s for: %(value)s")), {
+ field: '<em>' + _.escape(this.attrs.string) + '</em>',
+ value: '<strong>' + _.escape(value) + '</strong>'});
+ return $.when([{
+ label: label,
+ facet: {
+ category: this.attrs.string,
+ field: this,
+ values: [{label: value, value: value, operator: 'ilike'}]
+ },
+ expand: this.expand.bind(this),
+ }]);
+ },
+
+ expand: function (needle) {
+ var self = this;
+ // FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
+ var context = pyeval.eval(
+ 'contexts', [this.searchview.dataset.get_context()]);
+ return this.model.call('name_search', [], {
+ name: needle,
+ args: (typeof this.attrs.domain === 'string') ? [] : this.attrs.domain,
+ limit: 8,
+ context: context
+ }).then(function (results) {
+ if (_.isEmpty(results)) { return null; }
+ return _(results).map(function (result) {
+ return {
+ label: _.escape(result[1]),
+ facet: facet_from(self, result)
+ };
+ });
+ });
+ },
+ facet_for: function (value) {
+ var self = this;
+ if (value instanceof Array) {
+ if (value.length === 2 && _.isString(value[1])) {
+ return $.when(facet_from(this, value));
+ }
+ utils.assert(value.length <= 1,
+ _t("M2O search fields do not currently handle multiple default values"));
+ // there are many cases of {search_default_$m2ofield: [id]}, need
+ // to handle this as if it were a single value.
+ value = value[0];
+ }
+ return this.model.call('name_get', [value]).then(function (names) {
+ if (_(names).isEmpty()) { return null; }
+ return facet_from(self, names[0]);
+ });
+ },
+ value_from: function (facetValue) {
+ return facetValue.get('label');
+ },
+ make_domain: function (name, operator, facetValue) {
+ operator = facetValue.get('operator') || operator;
+
+ switch(operator){
+ case this.default_operator:
+ return [[name, '=', facetValue.get('value')]];
+ case 'ilike':
+ return [[name, 'ilike', facetValue.get('value')]];
+ case 'child_of':
+ return [[name, 'child_of', facetValue.get('value')]];
+ }
+ return this._super(name, operator, facetValue);
+ },
+ get_context: function (facet) {
+ var values = facet.values;
+ if (_.isEmpty(this.attrs.context) && values.length === 1) {
+ var c = {};
+ var v = values.at(0);
+ if (v.get('operator') !== 'ilike') {
+ c['default_' + this.attrs.name] = v.get('value');
+ }
+ return c;
+ }
+ return this._super(facet);
+ }
+});
+
+var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
+ template: 'SearchView.filters',
+ icon: "fa-filter",
+ completion_label: _lt("Filter on: %s"),
+ /**
+ * Inclusive group of filters, creates a continuous "button" with clickable
+ * sections (the normal display for filters is to be a self-contained button)
+ *
+ * @constructs instance.web.search.FilterGroup
+ * @extends instance.web.search.Input
+ *
+ * @param {Array<instance.web.search.Filter>} filters elements of the group
+ * @param {instance.web.SearchView} parent parent in which the filters are contained
+ */
+ init: function (filters, parent) {
+ // If all filters are group_by and we're not initializing a GroupbyGroup,
+ // create a GroupbyGroup instead of the current FilterGroup
+ if (!(this instanceof GroupbyGroup) &&
+ _(filters).all(function (f) {
+ if (!f.attrs.context) { return false; }
+ var c = pyeval.eval('context', f.attrs.context);
+ return !_.isEmpty(c.group_by);})) {
+ return new GroupbyGroup(filters, parent);
+ }
+ this._super(parent);
+ this.filters = filters;
+ this.searchview = parent;
+ this.searchview.query.on('add remove change reset', this.proxy('search_change'));
+ },
+ start: function () {
+ this.$el.on('click', 'a', this.proxy('toggle_filter'));
+ return $.when(null);
+ },
+ /**
+ * Handles change of the search query: any of the group's filter which is
+ * in the search query should be visually checked in the drawer
+ */
+ search_change: function () {
+ var self = this;
+ var $filters = this.$el.removeClass('selected');
+ var facet = this.searchview.query.find(_.bind(this.match_facet, this));
+ if (!facet) { return; }
+ facet.values.each(function (v) {
+ var i = _(self.filters).indexOf(v.get('value'));
+ if (i === -1) { return; }
+ $filters.filter(function () {
+ return Number($(this).data('index')) === i;
+ }).addClass('selected');
+ });
+ },
+ /**
+ * Matches the group to a facet, in order to find if the group is
+ * represented in the current search query
+ */
+ match_facet: function (facet) {
+ return facet.get('field') === this;
+ },
+ make_facet: function (values) {
+ return {
+ category: _t("Filter"),
+ icon: this.icon,
+ values: values,
+ field: this
+ };
+ },
+ make_value: function (filter) {
+ return {
+ label: filter.attrs.string || filter.attrs.help || filter.attrs.name,
+ value: filter
+ };
+ },
+ facet_for_defaults: function (defaults) {
+ var self = this;
+ var fs = _(this.filters).chain()
+ .filter(function (f) {
+ return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
+ }).map(function (f) {
+ return self.make_value(f);
+ }).value();
+ if (_.isEmpty(fs)) { return $.when(null); }
+ return $.when(this.make_facet(fs));
+ },
+ /**
+ * Fetches contexts for all enabled filters in the group
+ *
+ * @param {openerp.web.search.Facet} facet
+ * @return {*} combined contexts of the enabled filters in this group
+ */
+ get_context: function (facet) {
+ var contexts = facet.values.chain()
+ .map(function (f) { return f.get('value').attrs.context; })
+ .without('{}')
+ .reject(_.isEmpty)
+ .value();
+
+ if (!contexts.length) { return; }
+ if (contexts.length === 1) { return contexts[0]; }
+ return _.extend(new data.CompoundContext(), {
+ __contexts: contexts
+ });
+ },
+ /**
+ * Fetches group_by sequence for all enabled filters in the group
+ *
+ * @param {VS.model.SearchFacet} facet
+ * @return {Array} enabled filters in this group
+ */
+ get_groupby: function (facet) {
+ return facet.values.chain()
+ .map(function (f) { return f.get('value').attrs.context; })
+ .without('{}')
+ .reject(_.isEmpty)
+ .value();
+ },
+ /**
+ * Handles domains-fetching for all the filters within it: groups them.
+ *
+ * @param {VS.model.SearchFacet} facet
+ * @return {*} combined domains of the enabled filters in this group
+ */
+ get_domain: function (facet) {
+ var domains = facet.values.chain()
+ .map(function (f) { return f.get('value').attrs.domain; })
+ .without('[]')
+ .reject(_.isEmpty)
+ .value();
+
+ if (!domains.length) { return; }
+ if (domains.length === 1) { return domains[0]; }
+ for (var i=domains.length; --i;) {
+ domains.unshift(['|']);
+ }
+ return _.extend(new data.CompoundDomain(), {
+ __domains: domains
+ });
+ },
+ toggle_filter: function (e) {
+ e.stopPropagation();
+ this.toggle(this.filters[Number($(e.target).parent().data('index'))]);
+ },
+ toggle: function (filter, options) {
+ this.searchview.query.toggle(this.make_facet([this.make_value(filter)]), options);
+ },
+ is_visible: function () {
+ return _.some(this.filters, function (filter) {
+ return !filter.attrs.invisible;
+ });
+ },
+ complete: function (item) {
+ var self = this;
+ item = item.toLowerCase();
+ var facet_values = _(this.filters).chain()
+ .filter(function (filter) { return filter.visible(); })
+ .filter(function (filter) {
+ var at = {
+ string: filter.attrs.string || '',
+ help: filter.attrs.help || '',
+ name: filter.attrs.name || ''
+ };
+ var include = _.str.include;
+ return include(at.string.toLowerCase(), item)
+ || include(at.help.toLowerCase(), item)
+ || include(at.name.toLowerCase(), item);
+ })
+ .map(this.make_value)
+ .value();
+ if (_(facet_values).isEmpty()) { return $.when(null); }
+ return $.when(_.map(facet_values, function (facet_value) {
+ return {
+ label: _.str.sprintf(self.completion_label.toString(),
+ _.escape(facet_value.label)),
+ facet: self.make_facet([facet_value])
+ };
+ }));
+ }
+});
+
+var GroupbyGroup = FilterGroup.extend({
+ icon: 'fa-bars',
+ completion_label: _lt("Group by: %s"),
+ init: function (filters, parent) {
+ this._super(filters, parent);
+ this.searchview = parent;
+ // Not flanders: facet unicity is handled through the
+ // (category, field) pair of facet attributes. This is all well and
+ // good for regular filter groups where a group matches a facet, but for
+ // groupby we want a single facet. So cheat: add an attribute on the
+ // view which proxies to the first GroupbyGroup, so it can be used
+ // for every GroupbyGroup and still provides the various methods needed
+ // by the search view. Use weirdo name to avoid risks of conflicts
+ if (!this.searchview._s_groupby) {
+ this.searchview._s_groupby = {
+ help: "See GroupbyGroup#init",
+ get_context: this.proxy('get_context'),
+ get_domain: this.proxy('get_domain'),
+ get_groupby: this.proxy('get_groupby')
+ };
+ }
+ },
+ match_facet: function (facet) {
+ return facet.get('field') === this.searchview._s_groupby;
+ },
+ make_facet: function (values) {
+ return {
+ category: _t("GroupBy"),
+ icon: this.icon,
+ values: values,
+ field: this.searchview._s_groupby
+ };
+ }
+});
+
+var Filter = Input.extend(/** @lends instance.web.search.Filter# */{
+ template: 'SearchView.filter',
+ /**
+ * Implementation of the OpenERP filters (button with a context and/or
+ * a domain sent as-is to the search view)
+ *
+ * Filters are only attributes holder, the actual work (compositing
+ * domains and contexts, converting between facets and filters) is
+ * performed by the filter group.
+ *
+ * @constructs instance.web.search.Filter
+ * @extends instance.web.search.Input
+ *
+ * @param node
+ * @param parent
+ */
+ init: function (node, parent) {
+ this._super(parent);
+ this.load_attrs(node.attrs);
+ },
+ facet_for: function () { return $.when(null); },
+ get_context: function () { },
+ get_domain: function () { },
+});
+
+/**
+ * Registry of search fields, called by :js:class:`instance.web.SearchView` to
+ * find and instantiate its field widgets.
+ */
+
+core.search_widgets_registry
+ .add('char', CharField)
+ .add('text', CharField)
+ .add('html', CharField)
+ .add('boolean', BooleanField)
+ .add('integer', IntegerField)
+ .add('id', IntegerField)
+ .add('float', FloatField)
+ .add('selection', SelectionField)
+ .add('datetime', DateTimeField)
+ .add('date', DateField)
+ .add('many2one', ManyToOneField)
+ .add('many2many', CharField)
+ .add('one2many', CharField);
+
+
+return {
+ FilterGroup: FilterGroup,
+ Filter: Filter,
+ Field: Field,
+ GroupbyGroup: GroupbyGroup,
+};
+
+});
diff --git a/addons/web/static/src/js/views/search_menus.js b/addons/web/static/src/js/views/search_menus.js
new file mode 100644
index 00000000000..56df4657c48
--- /dev/null
+++ b/addons/web/static/src/js/views/search_menus.js
@@ -0,0 +1,437 @@
+odoo.define('web.FavoriteMenu', ['web.core', 'web.Model','web.pyeval', 'web.session', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+
+return Widget.extend({
+ template: 'SearchView.FavoriteMenu',
+ events: {
+ 'click li': function (event) {
+ event.stopImmediatePropagation();
+ },
+ 'click .oe-save-search a': function () {
+ this.toggle_save_menu();
+ },
+ 'click .oe-save-name button': 'save_favorite',
+ 'hidden.bs.dropdown': 'close_menus',
+ },
+ init: function (parent, query, target_model, action_id) {
+ this._super.apply(this,arguments);
+ this.searchview = parent;
+ this.query = query;
+ this.target_model = target_model;
+ this.model = new Model('ir.filters');
+ this.filters = {};
+ this.$filters = {};
+ this.action_id = action_id;
+ },
+ start: function () {
+ var self = this;
+ this.$save_search = this.$('.oe-save-search');
+ this.$save_name = this.$('.oe-save-name');
+ this.$inputs = this.$save_name.find('input');
+ this.$divider = this.$('.divider');
+ this.$inputs.eq(0).val(this.searchview.getParent().title);
+ var $shared_filter = this.$inputs.eq(1),
+ $default_filter = this.$inputs.eq(2);
+ $shared_filter.click(function () {$default_filter.prop('checked', false);});
+ $default_filter.click(function () {$shared_filter.prop('checked', false);});
+
+ this.query
+ .on('remove', function (facet) {
+ if (facet.get('is_custom_filter')) {
+ self.clear_selection();
+ }
+ })
+ .on('reset', this.proxy('clear_selection'));
+ if (!this.action_id) {
+ this.prepare_dropdown_menu([]);
+ return $.when();
+ }
+ return this.model.call('get_filters', [this.target_model, this.action_id])
+ .done(this.proxy('prepare_dropdown_menu'));
+ },
+ prepare_dropdown_menu: function (filters) {
+ filters.map(this.append_filter.bind(this));
+ },
+ toggle_save_menu: function (is_open) {
+ this.$save_search
+ .toggleClass('closed-menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
+ .toggleClass('open-menu', is_open);
+ this.$save_name.toggle(is_open);
+ if (this.$save_search.hasClass('open-menu')) {
+ this.$save_name.find('input').first().focus();
+ }
+ },
+ close_menus: function () {
+ this.toggle_save_menu(false);
+ },
+ save_favorite: function () {
+ var self = this,
+ filter_name = this.$inputs[0].value,
+ default_filter = this.$inputs[1].checked,
+ shared_filter = this.$inputs[2].checked;
+ if (!filter_name.length){
+ this.do_warn(_t("Error"), _t("Filter name is required."));
+ this.$inputs.first().focus();
+ return;
+ }
+ if (_.chain(this.filters)
+ .pluck('name')
+ .contains(filter_name).value()) {
+ this.do_warn(_t("Error"), _t("Filter with same name already exists."));
+ this.$inputs.first().focus();
+ return;
+ }
+ var search = this.searchview.build_search_data(),
+ view_manager = this.findAncestor(function (a) {
+ // HORRIBLE HACK. PLEASE SAVE ME FROM MYSELF (BUT IN A PAINLESS WAY IF POSSIBLE)
+ return 'active_view' in a;
+ }),
+ view_context = view_manager ? view_manager.active_view.controller.get_context() : {},
+ results = pyeval.sync_eval_domains_and_contexts({
+ domains: search.domains,
+ contexts: search.contexts.concat(view_context || []),
+ group_by_seq: search.groupbys || [],
+ });
+ if (!_.isEmpty(results.group_by)) {
+ results.context.group_by = results.group_by;
+ }
+ // Don't save user_context keys in the custom filter, otherwise end
+ // up with e.g. wrong uid or lang stored *and used in subsequent
+ // reqs*
+ var ctx = results.context;
+ _(_.keys(session.user_context)).each(function (key) {
+ delete ctx[key];
+ });
+ var filter = {
+ name: filter_name,
+ user_id: shared_filter ? false : session.uid,
+ model_id: this.searchview.dataset.model,
+ context: results.context,
+ domain: results.domain,
+ is_default: default_filter,
+ action_id: this.action_id,
+ };
+ return this.model.call('create_or_replace', [filter]).done(function (id) {
+ filter.id = id;
+ self.toggle_save_menu(false);
+ self.$save_name.find('input').val('').prop('checked', false);
+ self.append_filter(filter);
+ self.toggle_filter(filter, true);
+ });
+ },
+ get_default_filter: function () {
+ var personal_filter = _.find(this.filters, function (filter) {
+ return filter.user_id && filter.is_default;
+ });
+ if (personal_filter) {
+ return personal_filter;
+ }
+ return _.find(this.filters, function (filter) {
+ return !filter.user_id && filter.is_default;
+ });
+ },
+ /**
+ * Generates a mapping key (in the filters and $filter mappings) for the
+ * filter descriptor object provided (as returned by ``get_filters``).
+ *
+ * The mapping key is guaranteed to be unique for a given (user_id, name)
+ * pair.
+ *
+ * @param {Object} filter
+ * @param {String} filter.name
+ * @param {Number|Pair<Number, String>} [filter.user_id]
+ * @return {String} mapping key corresponding to the filter
+ */
+ key_for: function (filter) {
+ var user_id = filter.user_id,
+ action_id = filter.action_id,
+ uid = (user_id instanceof Array) ? user_id[0] : user_id,
+ act_id = (action_id instanceof Array) ? action_id[0] : action_id;
+ return _.str.sprintf('(%s)(%s)%s', uid, act_id, filter.name);
+ },
+ /**
+ * Generates a :js:class:`~instance.web.search.Facet` descriptor from a
+ * filter descriptor
+ *
+ * @param {Object} filter
+ * @param {String} filter.name
+ * @param {Object} [filter.context]
+ * @param {Array} [filter.domain]
+ * @return {Object}
+ */
+ facet_for: function (filter) {
+ return {
+ category: _t("Custom Filter"),
+ icon: 'fa-star',
+ field: {
+ get_context: function () { return filter.context; },
+ get_groupby: function () { return [filter.context]; },
+ get_domain: function () { return filter.domain; }
+ },
+ _id: filter.id,
+ is_custom_filter: true,
+ values: [{label: filter.name, value: null}]
+ };
+ },
+ clear_selection: function () {
+ this.$('li.selected').removeClass('selected');
+ },
+ append_filter: function (filter) {
+ var self = this,
+ key = this.key_for(filter),
+ $filter;
+
+ this.$divider.show();
+ if (key in this.$filters) {
+ $filter = this.$filters[key];
+ } else {
+ this.filters[key] = filter;
+ $filter = $('<li></li>')
+ .insertBefore(this.$divider)
+ .toggleClass('oe_searchview_custom_default', filter.is_default)
+ .append($('<a>').text(filter.name));
+
+ this.$filters[key] = $filter;
+ this.$filters[key].addClass(filter.user_id ? 'oe_searchview_custom_private'
+ : 'oe_searchview_custom_public');
+ $('<span>')
+ .addClass('fa fa-trash-o remove-filter')
+ .click(function (event) {
+ event.stopImmediatePropagation();
+ self.remove_filter(filter, $filter, key);
+ })
+ .appendTo($filter);
+ }
+ this.$filters[key].unbind('click').click(function () {
+ self.toggle_filter(filter);
+ });
+ },
+ toggle_filter: function (filter, preventSearch) {
+ var current = this.query.find(function (facet) {
+ return facet.get('_id') === filter.id;
+ });
+ if (current) {
+ this.query.remove(current);
+ this.$filters[this.key_for(filter)].removeClass('selected');
+ return;
+ }
+ this.query.reset([this.facet_for(filter)], {
+ preventSearch: preventSearch || false});
+ this.$filters[this.key_for(filter)].addClass('selected');
+ },
+ remove_filter: function (filter, $filter, key) {
+ var self = this;
+ var global_warning = _t("This filter is global and will be removed for everybody if you continue."),
+ warning = _t("Are you sure that you want to remove this filter?");
+ if (!confirm(filter.user_id ? warning : global_warning)) {
+ return;
+ }
+ this.model.call('unlink', [filter.id]).done(function () {
+ $filter.remove();
+ delete self.$filters[key];
+ delete self.filters[key];
+ if (_.isEmpty(self.filters)) {
+ self.$divider.hide();
+ }
+ });
+ },
+});
+
+});
+
+odoo.define('web.FilterMenu', ['web.Widget', 'web.search_filters', 'web.search_inputs'], function (require) {
+
+var search_filters = require('web.search_filters'),
+ search_inputs = require('web.search_inputs'),
+ Widget = require('web.Widget');
+
+return Widget.extend({
+ template: 'SearchView.FilterMenu',
+ events: {
+ 'click .oe-add-filter': function () {
+ this.toggle_custom_filter_menu();
+ },
+ 'click li': function (event) {event.stopImmediatePropagation();},
+ 'hidden.bs.dropdown': function () {
+ this.toggle_custom_filter_menu(false);
+ },
+ 'click .oe-add-condition': 'append_proposition',
+ 'click .oe-apply-filter': 'commit_search',
+ },
+ init: function (parent, filters, fields_def) {
+ this._super(parent);
+ this.filters = filters || [];
+ this.searchview = parent;
+ this.propositions = [];
+ this.fields_def = fields_def.then(function (data) {
+ var fields = {
+ id: { string: 'ID', type: 'id', searchable: true }
+ };
+ _.each(data, function(field_def, field_name) {
+ if (field_def.selectable !== false && field_name !== 'id') {
+ fields[field_name] = field_def;
+ }
+ });
+ return fields;
+ });
+ },
+ start: function () {
+ var self = this;
+ this.$menu = this.$('.filters-menu');
+ this.$add_filter = this.$('.oe-add-filter');
+ this.$apply_filter = this.$('.oe-apply-filter');
+ this.$add_filter_menu = this.$('.oe-add-filter-menu');
+ _.each(this.filters, function (group) {
+ if (group.is_visible()) {
+ group.insertBefore(self.$add_filter);
+ $('<li class="divider">').insertBefore(self.$add_filter);
+ }
+ });
+ this.append_proposition().then(function (prop) {
+ prop.$el.hide();
+ });
+ },
+ update_max_height: function () {
+ var max_height = $(window).height() - this.$menu[0].getBoundingClientRect().top - 10;
+ this.$menu.css('max-height', max_height);
+ },
+ toggle_custom_filter_menu: function (is_open) {
+ this.$add_filter
+ .toggleClass('closed-menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
+ .toggleClass('open-menu', is_open);
+ this.$add_filter_menu.toggle(is_open);
+ if (this.$add_filter.hasClass('closed-menu') && (!this.propositions.length)) {
+ this.append_proposition();
+ }
+ this.$('.oe-filter-condition').toggle(is_open);
+ this.update_max_height();
+ },
+ append_proposition: function () {
+ var self = this;
+ return this.fields_def.then(function (fields) {
+ var prop = new search_filters.ExtendedSearchProposition(self, fields);
+ self.propositions.push(prop);
+ prop.insertBefore(self.$add_filter_menu);
+ self.$apply_filter.prop('disabled', false);
+ self.update_max_height();
+ return prop;
+ });
+ },
+ remove_proposition: function (prop) {
+ this.propositions = _.without(this.propositions, prop);
+ if (!this.propositions.length) {
+ this.$apply_filter.prop('disabled', true);
+ }
+ prop.destroy();
+ },
+ commit_search: function () {
+ var filters = _.invoke(this.propositions, 'get_filter'),
+ filters_widgets = _.map(filters, function (filter) {
+ return new search_inputs.Filter(filter, this);
+ }),
+ filter_group = new search_inputs.FilterGroup(filters_widgets, this.searchview),
+ facets = filters_widgets.map(function (filter) {
+ return filter_group.make_facet([filter_group.make_value(filter)]);
+ });
+ filter_group.insertBefore(this.$add_filter);
+ $('<li class="divider">').insertBefore(this.$add_filter);
+ this.searchview.query.add(facets, {silent: true});
+ this.searchview.query.trigger('reset');
+
+ _.invoke(this.propositions, 'destroy');
+ this.propositions = [];
+ this.append_proposition();
+ this.toggle_custom_filter_menu(false);
+ },
+});
+
+});
+
+odoo.define('web.GroupByMenu', ['web.Widget', 'web.core', 'web.search_inputs'], function (require) {
+
+var Widget = require('web.Widget'),
+ core = require('web.core'),
+ search_inputs = require('web.search_inputs');
+
+var QWeb = core.qweb;
+
+return Widget.extend({
+ template: 'SearchView.GroupByMenu',
+ events: {
+ 'click li': function (event) {
+ event.stopImmediatePropagation();
+ },
+ 'hidden.bs.dropdown': function () {
+ this.toggle_add_menu(false);
+ },
+ 'click .add-custom-group a': function () {
+ this.toggle_add_menu();
+ },
+ },
+ init: function (parent, groups, fields_def) {
+ this._super(parent);
+ this.groups = groups || [];
+ this.groupable_fields = {};
+ this.searchview = parent;
+ this.fields_def = fields_def.then(this.proxy('get_groupable_fields'));
+ },
+ start: function () {
+ var self = this;
+ this.$menu = this.$('.group-by-menu');
+ var divider = this.$menu.find('.divider');
+ _.invoke(this.groups, 'insertBefore', divider);
+ if (this.groups.length) {
+ divider.show();
+ }
+ this.$add_group = this.$menu.find('.add-custom-group');
+ this.fields_def.then(function () {
+ self.$menu.append(QWeb.render('GroupByMenuSelector', self));
+ self.$add_group_menu = self.$('.oe-add-group');
+ self.$group_selector = self.$('.oe-group-selector');
+ self.$('.oe-select-group').click(function () {
+ self.toggle_add_menu(false);
+ var field = self.$group_selector.find(':selected').data('name');
+ self.add_groupby_to_menu(field);
+ });
+ });
+ },
+ get_groupable_fields: function (fields) {
+ var self = this,
+ groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
+
+ _.each(fields, function (field, name) {
+ if (field.store && _.contains(groupable_types, field.type)) {
+ self.groupable_fields[name] = field;
+ }
+ });
+ },
+ toggle_add_menu: function (is_open) {
+ this.$add_group
+ .toggleClass('closed-menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
+ .toggleClass('open-menu', is_open);
+ this.$add_group_menu.toggle(is_open);
+ if (this.$add_group.hasClass('open-menu')) {
+ this.$group_selector.focus();
+ }
+ },
+ add_groupby_to_menu: function (field_name) {
+ var filter = new search_inputs.Filter({attrs:{
+ context:"{'group_by':'" + field_name + "''}",
+ name: this.groupable_fields[field_name].string,
+ }}, this.searchview);
+ var group = new search_inputs.FilterGroup([filter], this.searchview),
+ divider = this.$('.divider').show();
+ group.insertBefore(divider);
+ group.toggle(filter);
+ },
+});
+
+});
diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js
new file mode 100644
index 00000000000..a459dfe666b
--- /dev/null
+++ b/addons/web/static/src/js/views/search_view.js
@@ -0,0 +1,945 @@
+odoo.define('web.SearchView', ['web.AutoComplete', 'web.core', 'web.FavoriteMenu', 'web.FilterMenu', 'web.GroupByMenu', 'web.Model', 'web.pyeval', 'web.search_inputs', 'web.utils', 'web.Widget'], function (require) {
+"use strict";
+
+var AutoComplete = require('web.AutoComplete');
+var core = require('web.core');
+var FavoriteMenu = require('web.FavoriteMenu');
+var FilterMenu = require('web.FilterMenu');
+var GroupByMenu = require('web.GroupByMenu');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var search_inputs = require('web.search_inputs');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+var Backbone = window.Backbone;
+
+var FacetValue = Backbone.Model.extend({});
+
+var FacetValues = Backbone.Collection.extend({
+ model: FacetValue
+});
+
+var Facet = Backbone.Model.extend({
+ initialize: function (attrs) {
+ var values = attrs.values;
+ delete attrs.values;
+
+ Backbone.Model.prototype.initialize.apply(this, arguments);
+
+ this.values = new FacetValues(values || []);
+ this.values.on('add remove change reset', function (_, options) {
+ this.trigger('change', this, options);
+ }, this);
+ },
+ get: function (key) {
+ if (key !== 'values') {
+ return Backbone.Model.prototype.get.call(this, key);
+ }
+ return this.values.toJSON();
+ },
+ set: function (key, value) {
+ if (key !== 'values') {
+ return Backbone.Model.prototype.set.call(this, key, value);
+ }
+ this.values.reset(value);
+ },
+ toJSON: function () {
+ var out = {};
+ var attrs = this.attributes;
+ for(var att in attrs) {
+ if (!attrs.hasOwnProperty(att) || att === 'field') {
+ continue;
+ }
+ out[att] = attrs[att];
+ }
+ out.values = this.values.toJSON();
+ return out;
+ }
+});
+
+var SearchQuery = Backbone.Collection.extend({
+ model: Facet,
+ initialize: function () {
+ Backbone.Collection.prototype.initialize.apply(
+ this, arguments);
+ this.on('change', function (facet) {
+ if(!facet.values.isEmpty()) { return; }
+
+ this.remove(facet, {silent: true});
+ }, this);
+ },
+ add: function (values, options) {
+ options = options || {};
+
+ if (!values) {
+ values = [];
+ } else if (!(values instanceof Array)) {
+ values = [values];
+ }
+
+ _(values).each(function (value) {
+ var model = this._prepareModel(value, options);
+ var previous = this.detect(function (facet) {
+ return facet.get('category') === model.get('category') &&
+ facet.get('field') === model.get('field');
+ });
+ if (previous) {
+ previous.values.add(model.get('values'), _.omit(options, 'at', 'merge'));
+ return;
+ }
+ Backbone.Collection.prototype.add.call(this, model, options);
+ }, this);
+ // warning: in backbone 1.0+ add is supposed to return the added models,
+ // but here toggle may delegate to add and return its value directly.
+ // return value of neither seems actually used but should be tested
+ // before change, probably
+ return this;
+ },
+ toggle: function (value, options) {
+ options = options || {};
+
+ var facet = this.detect(function (facet) {
+ return facet.get('category') === value.category
+ && facet.get('field') === value.field;
+ });
+ if (!facet) {
+ return this.add(value, options);
+ }
+
+ var changed = false;
+ _(value.values).each(function (val) {
+ var already_value = facet.values.detect(function (v) {
+ return v.get('value') === val.value
+ && v.get('label') === val.label;
+ });
+ // toggle value
+ if (already_value) {
+ facet.values.remove(already_value, {silent: true});
+ } else {
+ facet.values.add(val, {silent: true});
+ }
+ changed = true;
+ });
+ // "Commit" changes to values array as a single call, so observers of
+ // change event don't get misled by intermediate incomplete toggling
+ // states
+ facet.trigger('change', facet);
+ return this;
+ }
+});
+
+var InputView = Widget.extend({
+ template: 'SearchView.InputView',
+ events: {
+ focus: function () { this.trigger('focused', this); },
+ blur: function () { this.$el.text(''); this.trigger('blurred', this); },
+ keydown: 'onKeydown',
+ paste: 'onPaste',
+ },
+ getSelection: function () {
+ this.el.normalize();
+ // get Text node
+ var root = this.el.childNodes[0];
+ if (!root || !root.textContent) {
+ // if input does not have a child node, or the child node is an
+ // empty string, then the selection can only be (0, 0)
+ return {start: 0, end: 0};
+ }
+ var range = window.getSelection().getRangeAt(0);
+ // In Firefox, depending on the way text is selected (drag, double- or
+ // triple-click) the range may start or end on the parent of the
+ // selected text node‽ Check for this condition and fixup the range
+ // note: apparently with C-a this can go even higher?
+ if (range.startContainer === this.el && range.startOffset === 0) {
+ range.setStart(root, 0);
+ }
+ if (range.endContainer === this.el && range.endOffset === 1) {
+ range.setEnd(root, root.length);
+ }
+ utils.assert(range.startContainer === root,
+ "selection should be in the input view");
+ utils.assert(range.endContainer === root,
+ "selection should be in the input view");
+ return {
+ start: range.startOffset,
+ end: range.endOffset
+ };
+ },
+ onKeydown: function (e) {
+ this.el.normalize();
+ var sel;
+ switch (e.which) {
+ // Do not insert newline, but let it bubble so searchview can use it
+ case $.ui.keyCode.ENTER:
+ e.preventDefault();
+ break;
+
+ // FIXME: may forget content if non-empty but caret at index 0, ok?
+ case $.ui.keyCode.BACKSPACE:
+ sel = this.getSelection();
+ if (sel.start === 0 && sel.start === sel.end) {
+ e.preventDefault();
+ var preceding = this.getParent().siblingSubview(this, -1);
+ if (preceding && (preceding instanceof FacetView)) {
+ preceding.model.destroy();
+ }
+ }
+ break;
+
+ // let left/right events propagate to view if caret is at input border
+ // and not a selection
+ case $.ui.keyCode.LEFT:
+ sel = this.getSelection();
+ if (sel.start !== 0 || sel.start !== sel.end) {
+ e.stopPropagation();
+ }
+ break;
+ case $.ui.keyCode.RIGHT:
+ sel = this.getSelection();
+ var len = this.$el.text().length;
+ if (sel.start !== len || sel.start !== sel.end) {
+ e.stopPropagation();
+ }
+ break;
+ }
+ },
+ setCursorAtEnd: function () {
+ this.el.normalize();
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var range = document.createRange();
+ // in theory, range.selectNodeContents should work here. In practice,
+ // MSIE9 has issues from time to time, instead of selecting the inner
+ // text node it would select the reference node instead (e.g. in demo
+ // data, company news, copy across the "Company News" link + the title,
+ // from about half the link to half the text, paste in search box then
+ // hit the left arrow key, getSelection would blow up).
+ //
+ // Explicitly selecting only the inner text node (only child node
+ // since we've normalized the parent) avoids the issue
+ range.selectNode(this.el.childNodes[0]);
+ range.collapse(false);
+ sel.addRange(range);
+ },
+ onPaste: function () {
+ this.el.normalize();
+ // In MSIE and Webkit, it is possible to get various representations of
+ // the clipboard data at this point e.g.
+ // window.clipboardData.getData('Text') and
+ // event.clipboardData.getData('text/plain') to ensure we have a plain
+ // text representation of the object (and probably ensure the object is
+ // pastable as well, so nobody puts an image in the search view)
+ // (nb: since it's not possible to alter the content of the clipboard
+ // — at least in Webkit — to ensure only textual content is available,
+ // using this would require 1. getting the text data; 2. manually
+ // inserting the text data into the content; and 3. cancelling the
+ // paste event)
+ //
+ // But Firefox doesn't support the clipboard API (as of FF18)
+ // although it correctly triggers the paste event (Opera does not even
+ // do that) => implement lowest-denominator system where onPaste
+ // triggers a followup "cleanup" pass after the data has been pasted
+ setTimeout(function () {
+ // Read text content (ignore pasted HTML)
+ var data = this.$el.text();
+ if (!data)
+ return;
+ // paste raw text back in
+ this.$el.empty().text(data);
+ this.el.normalize();
+ // Set the cursor at the end of the text, so the cursor is not lost
+ // in some kind of error-spawning limbo.
+ this.setCursorAtEnd();
+ }.bind(this), 0);
+ }
+});
+
+var FacetView = Widget.extend({
+ template: 'SearchView.FacetView',
+ events: {
+ 'focus': function () { this.trigger('focused', this); },
+ 'blur': function () { this.trigger('blurred', this); },
+ 'click': function (e) {
+ if ($(e.target).is('.oe_facet_remove')) {
+ this.model.destroy();
+ return false;
+ }
+ this.$el.focus();
+ e.stopPropagation();
+ },
+ 'keydown': function (e) {
+ var keys = $.ui.keyCode;
+ switch (e.which) {
+ case keys.BACKSPACE:
+ case keys.DELETE:
+ this.model.destroy();
+ return false;
+ }
+ }
+ },
+ init: function (parent, model) {
+ this._super(parent);
+ this.model = model;
+ this.model.on('change', this.model_changed, this);
+ },
+ destroy: function () {
+ this.model.off('change', this.model_changed, this);
+ this._super();
+ },
+ start: function () {
+ var self = this;
+ var $e = this.$('> span:last-child');
+ return $.when(this._super()).then(function () {
+ return $.when.apply(null, self.model.values.map(function (value) {
+ return new FacetValueView(self, value).appendTo($e);
+ }));
+ });
+ },
+ model_changed: function () {
+ this.$el.text(this.$el.text() + '*');
+ }
+});
+
+var FacetValueView = Widget.extend({
+ template: 'SearchView.FacetView.Value',
+ init: function (parent, model) {
+ this._super(parent);
+ this.model = model;
+ this.model.on('change', this.model_changed, this);
+ },
+ destroy: function () {
+ this.model.off('change', this.model_changed, this);
+ this._super();
+ },
+ model_changed: function () {
+ this.$el.text(this.$el.text() + '*');
+ }
+});
+
+var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
+ template: "SearchView",
+ events: {
+ // focus last input if view itself is clicked
+ 'click': function (e) {
+ if (e.target === this.$('.oe_searchview_facets')[0]) {
+ this.$('.oe_searchview_input:last').focus();
+ }
+ },
+ // search button
+ 'click div.oe_searchview_search': function (e) {
+ e.stopImmediatePropagation();
+ this.do_search();
+ },
+ 'click .oe_searchview_unfold_drawer': function (e) {
+ e.stopImmediatePropagation();
+ $(e.target).toggleClass('fa-caret-down fa-caret-up');
+ localStorage.visible_search_menu = (localStorage.visible_search_menu !== 'true');
+ this.toggle_buttons();
+ },
+ 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) {
+ switch(e.which) {
+ case $.ui.keyCode.LEFT:
+ this.focusPreceding(e.target);
+ e.preventDefault();
+ break;
+ case $.ui.keyCode.RIGHT:
+ if (!this.autocomplete.is_expandable()) {
+ this.focusFollowing(e.target);
+ }
+ e.preventDefault();
+ break;
+ }
+ },
+ 'autocompleteopen': function () {
+ this.$el.autocomplete('widget').css('z-index', 9999);
+ },
+ },
+ /**
+ * @constructs instance.web.SearchView
+ * @extends instance.web.Widget
+ *
+ * @param parent
+ * @param dataset
+ * @param view_id
+ * @param defaults
+ * @param {Object} [options]
+ * @param {Boolean} [options.hidden=false] hide the search view
+ * @param {Boolean} [options.disable_custom_filters=false] do not load custom filters from ir.filters
+ */
+ init: function(parent, dataset, view_id, defaults, options) {
+ this.options = _.defaults(options || {}, {
+ hidden: false,
+ disable_custom_filters: false,
+ });
+ this._super(parent);
+ this.query = undefined;
+ this.dataset = dataset;
+ this.view_id = view_id;
+ this.search_fields = [];
+ this.filters = [];
+ this.groupbys = [];
+ this.visible_filters = (localStorage.visible_search_menu === 'true');
+ this.input_subviews = []; // for user input in searchbar
+ this.defaults = defaults || {};
+ this.headless = this.options.hidden && _.isEmpty(this.defaults);
+ this.$buttons = this.options.$buttons;
+
+ this.filter_menu = undefined;
+ this.groupby_menu = undefined;
+ this.favorite_menu = undefined;
+ this.action_id = this.options && this.options.action && this.options.action.id;
+ },
+ start: function() {
+ if (this.headless) {
+ this.$el.hide();
+ }
+ this.toggle_visibility(false);
+ this.$facets_container = this.$('div.oe_searchview_facets');
+ this.setup_global_completion();
+ this.query = new SearchQuery()
+ .on('add change reset remove', this.proxy('do_search'))
+ .on('change', this.proxy('renderChangedFacets'))
+ .on('add reset remove', this.proxy('renderFacets'));
+ var load_view = this.dataset._model.fields_view_get({
+ view_id: this.view_id,
+ view_type: 'search',
+ context: this.dataset.get_context(),
+ });
+ this.$('.oe_searchview_unfold_drawer')
+ .toggleClass('fa-caret-down', !this.visible_filters)
+ .toggleClass('fa-caret-up', this.visible_filters);
+ return this.alive($.when(this._super(), this.alive(load_view).then(this.view_loaded.bind(this))));
+ },
+ view_loaded: function (r) {
+ var custom_filters_ready;
+ this.fields_view_get = r;
+ this.view_id = this.view_id || r.view_id;
+ this.prepare_search_inputs();
+ if (this.$buttons) {
+
+ var fields_def = new Model(this.dataset.model).call('fields_get', {
+ context: this.dataset.context
+ });
+
+ this.groupby_menu = new GroupByMenu(this, this.groupbys, fields_def);
+ this.filter_menu = new FilterMenu(this, this.filters, fields_def);
+ this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id);
+
+ this.filter_menu.appendTo(this.$buttons);
+ this.groupby_menu.appendTo(this.$buttons);
+ custom_filters_ready = this.favorite_menu.appendTo(this.$buttons);
+ }
+ return $.when(custom_filters_ready).then(this.proxy('set_default_filters'));
+ },
+ set_default_filters: function () {
+ var self = this,
+ default_custom_filter = this.$buttons && this.favorite_menu.get_default_filter();
+ if (!self.options.disable_custom_filters && default_custom_filter) {
+ return this.favorite_menu.toggle_filter(default_custom_filter, true);
+ }
+ if (!_.isEmpty(this.defaults)) {
+ var inputs = this.search_fields.concat(this.filters, this.groupbys),
+ defaults = _.invoke(inputs, 'facet_for_defaults', this.defaults);
+ return $.when.apply(null, defaults).then(function () {
+ self.query.reset(_(arguments).compact(), {preventSearch: true});
+ });
+ }
+ this.query.reset([], {preventSearch: true});
+ return $.when();
+ },
+ /**
+ * Performs the search view collection of widget data.
+ *
+ * If the collection went well (all fields are valid), then triggers
+ * :js:func:`instance.web.SearchView.on_search`.
+ *
+ * If at least one field failed its validation, triggers
+ * :js:func:`instance.web.SearchView.on_invalid` instead.
+ *
+ * @param [_query]
+ * @param {Object} [options]
+ */
+ do_search: function (_query, options) {
+ if (options && options.preventSearch) {
+ return;
+ }
+ var search = this.build_search_data();
+ this.trigger('search_data', search.domains, search.contexts, search.groupbys);
+ },
+ /**
+ * Extract search data from the view's facets.
+ *
+ * Result is an object with 3 (own) properties:
+ *
+ * domains
+ * Array of domains
+ * contexts
+ * Array of contexts
+ * groupbys
+ * Array of domains, in groupby order rather than view order
+ *
+ * @return {Object}
+ */
+ build_search_data: function () {
+ var domains = [], contexts = [], groupbys = [];
+
+ this.query.each(function (facet) {
+ var field = facet.get('field');
+ var domain = field.get_domain(facet);
+ if (domain) {
+ domains.push(domain);
+ }
+ var context = field.get_context(facet);
+ if (context) {
+ contexts.push(context);
+ }
+ var group_by = field.get_groupby(facet);
+ if (group_by) {
+ groupbys.push.apply(groupbys, group_by);
+ }
+ });
+ return {
+ domains: domains,
+ contexts: contexts,
+ groupbys: groupbys,
+ };
+ },
+ toggle_visibility: function (is_visible) {
+ this.$el.toggle(!this.headless && is_visible);
+ if (this.$buttons) {
+ this.$buttons.toggle(!this.headless && is_visible && this.visible_filters);
+ }
+ if (!this.headless && is_visible) {
+ this.$('div.oe_searchview_input').last().focus();
+ }
+ },
+ toggle_buttons: function (is_visible) {
+ this.visible_filters = is_visible || !this.visible_filters;
+ if (this.$buttons) {
+ this.$buttons.toggle(this.visible_filters);
+ }
+ },
+ /**
+ * Sets up search view's view-wide auto-completion widget
+ */
+ setup_global_completion: function () {
+ var self = this;
+ this.autocomplete = new AutoComplete(this, {
+ source: this.proxy('complete_global_search'),
+ select: this.proxy('select_completion'),
+ delay: 0,
+ get_search_string: function () {
+ return self.$('div.oe_searchview_input').text();
+ },
+ });
+ this.autocomplete.appendTo(this.$el);
+ },
+ /**
+ * Provide auto-completion result for req.term (an array to `resp`)
+ *
+ * @param {Object} req request to complete
+ * @param {String} req.term searched term to complete
+ * @param {Function} resp response callback
+ */
+ complete_global_search: function (req, resp) {
+ var inputs = this.search_fields.concat(this.filters, this.groupbys);
+ $.when.apply(null, _(inputs).chain()
+ .filter(function (input) { return input.visible(); })
+ .invoke('complete', req.term)
+ .value()).then(function () {
+ resp(_(arguments).chain()
+ .compact()
+ .flatten(true)
+ .value());
+ });
+ },
+ /**
+ * Action to perform in case of selection: create a facet (model)
+ * and add it to the search collection
+ *
+ * @param {Object} e selection event, preventDefault to avoid setting value on object
+ * @param {Object} ui selection information
+ * @param {Object} ui.item selected completion item
+ */
+ select_completion: function (e, ui) {
+ e.preventDefault();
+ var input_index = _(this.input_subviews).indexOf(
+ this.subviewForRoot(
+ this.$('div.oe_searchview_input:focus')[0]));
+ this.query.add(ui.item.facet, {at: input_index / 2});
+ },
+ subviewForRoot: function (subview_root) {
+ return _(this.input_subviews).detect(function (subview) {
+ return subview.$el[0] === subview_root;
+ });
+ },
+ siblingSubview: function (subview, direction, wrap_around) {
+ var index = _(this.input_subviews).indexOf(subview) + direction;
+ if (wrap_around && index < 0) {
+ index = this.input_subviews.length - 1;
+ } else if (wrap_around && index >= this.input_subviews.length) {
+ index = 0;
+ }
+ return this.input_subviews[index];
+ },
+ focusPreceding: function (subview_root) {
+ return this.siblingSubview(
+ this.subviewForRoot(subview_root), -1, true)
+ .$el.focus();
+ },
+ focusFollowing: function (subview_root) {
+ return this.siblingSubview(
+ this.subviewForRoot(subview_root), +1, true)
+ .$el.focus();
+ },
+ /**
+ * @param {openerp.web.search.SearchQuery | undefined} Undefined if event is change
+ * @param {openerp.web.search.Facet}
+ * @param {Object} [options]
+ */
+ renderFacets: function (collection, model, options) {
+ var self = this;
+ var started = [];
+ _.invoke(this.input_subviews, 'destroy');
+ this.input_subviews = [];
+
+ var i = new InputView(this);
+ started.push(i.appendTo(this.$facets_container));
+ this.input_subviews.push(i);
+ this.query.each(function (facet) {
+ var f = new FacetView(this, facet);
+ started.push(f.appendTo(self.$facets_container));
+ self.input_subviews.push(f);
+
+ var i = new InputView(this);
+ started.push(i.appendTo(self.$facets_container));
+ self.input_subviews.push(i);
+ }, this);
+ _.each(this.input_subviews, function (childView) {
+ childView.on('focused', self, self.proxy('childFocused'));
+ childView.on('blurred', self, self.proxy('childBlurred'));
+ });
+
+ $.when.apply(null, started).then(function () {
+ if (options && options.focus_input === false) return;
+ var input_to_focus;
+ // options.at: facet inserted at given index, focus next input
+ // otherwise just focus last input
+ if (!options || typeof options.at !== 'number') {
+ input_to_focus = _.last(self.input_subviews);
+ } else {
+ input_to_focus = self.input_subviews[(options.at + 1) * 2];
+ }
+ input_to_focus.$el.focus();
+ });
+ },
+ childFocused: function () {
+ this.$el.addClass('active');
+ },
+ childBlurred: function () {
+ this.$el.val('').removeClass('active').trigger('blur');
+ this.autocomplete.close();
+ },
+ /**
+ * Call the renderFacets method with the correct arguments.
+ * This is due to the fact that change events are called with two arguments
+ * (model, options) while add, reset and remove events are called with
+ * (collection, model, options) as arguments
+ */
+ renderChangedFacets: function (model, options) {
+ this.renderFacets(undefined, model, options);
+ },
+ // it should parse the arch field of the view, instantiate the corresponding
+ // filters/fields, and put them in the correct variables:
+ // * this.search_fields is a list of all the fields,
+ // * this.filters: groups of filters
+ // * this.group_by: group_bys
+ prepare_search_inputs: function () {
+ var self = this,
+ arch = this.fields_view_get.arch;
+
+ var filters = [].concat.apply([], _.map(arch.children, function (item) {
+ return item.tag !== 'group' ? eval_item(item) : item.children.map(eval_item);
+ }));
+ function eval_item (item) {
+ var category = 'filters';
+ if (item.attrs.context) {
+ try {
+ var context = pyeval.eval('context', item.attrs.context);
+ if (context.group_by) {
+ category = 'group_by';
+ }
+ } catch (e) {}
+ }
+ return {
+ item: item,
+ category: category,
+ };
+ }
+ var current_group = [],
+ current_category = 'filters',
+ categories = {filters: this.filters, group_by: this.groupbys};
+
+ _.each(filters.concat({category:'filters', item: 'separator'}), function (filter) {
+ if (filter.item.tag === 'filter' && filter.category === current_category) {
+ return current_group.push(new search_inputs.Filter(filter.item, self));
+ }
+ if (current_group.length) {
+ var group = new search_inputs.FilterGroup(current_group, self);
+ categories[current_category].push(group);
+ current_group = [];
+ }
+ if (filter.item.tag === 'field') {
+ var attrs = filter.item.attrs,
+ field = self.fields_view_get.fields[attrs.name],
+ Obj = core.search_widgets_registry.get_any([attrs.widget, field.type]);
+ if (Obj) {
+ self.search_fields.push(new (Obj) (filter.item, field, self));
+ }
+ }
+ if (filter.item.tag === 'filter') {
+ current_group.push(new search_inputs.Filter(filter.item, self));
+ }
+ current_category = filter.category;
+ });
+ },
+});
+
+_.extend(SearchView, {
+ SearchQuery: SearchQuery,
+ Facet: Facet,
+});
+
+return SearchView;
+
+});
+
+odoo.define('web.AutoComplete', ['web.Widget'], function (require) {
+"use strict";
+
+var Widget = require('web.Widget');
+
+return Widget.extend({
+ template: "SearchView.autocomplete",
+
+ // Parameters for autocomplete constructor:
+ //
+ // parent: this is used to detect keyboard events
+ //
+ // options.source: function ({term:query}, callback). This function will be called to
+ // obtain the search results corresponding to the query string. It is assumed that
+ // options.source will call callback with the results.
+ // options.delay: delay in millisecond before calling source. Useful if you don't want
+ // to make too many rpc calls
+ // options.select: function (ev, {item: {facet:facet}}). Autocomplete widget will call
+ // that function when a selection is made by the user
+ // options.get_search_string: function (). This function will be called by autocomplete
+ // to obtain the current search string.
+ init: function (parent, options) {
+ this._super(parent);
+ this.$input = parent.$el;
+ this.source = options.source;
+ this.delay = options.delay;
+ this.select = options.select;
+ this.get_search_string = options.get_search_string;
+
+ this.current_result = null;
+
+ this.searching = true;
+ this.search_string = null;
+ this.current_search = null;
+ },
+ start: function () {
+ var self = this;
+ this.$input.on('keyup', function (ev) {
+ if (ev.which === $.ui.keyCode.RIGHT) {
+ self.searching = true;
+ ev.preventDefault();
+ return;
+ }
+ // ENTER is caugth at KeyUp rather than KeyDown to avoid firing
+ // before all regular keystrokes have been processed
+ if (ev.which === $.ui.keyCode.ENTER) {
+ if (self.current_result && self.get_search_string().length) {
+ self.select_item(ev);
+ }
+ return;
+ }
+ var search_string = self.get_search_string();
+ if (self.search_string !== search_string) {
+ if (search_string.length) {
+ self.search_string = search_string;
+ setTimeout(function () { self.initiate_search(search_string);}, self.delay);
+ } else {
+ self.close();
+ }
+ }
+ });
+ this.$input.on('keypress', function (ev) {
+ self.search_string = self.get_search_string() + String.fromCharCode(ev.which);
+ if (self.search_string.length) {
+ self.searching = true;
+ var search_string = self.search_string;
+ setTimeout(function () { self.initiate_search(search_string);}, self.delay);
+ } else {
+ self.close();
+ }
+ });
+ this.$input.on('keydown', function (ev) {
+ switch (ev.which) {
+ // TAB and direction keys are handled at KeyDown because KeyUp
+ // is not guaranteed to fire.
+ // See e.g. https://github.com/aef-/jquery.masterblaster/issues/13
+ case $.ui.keyCode.TAB:
+ if (self.current_result && self.get_search_string().length) {
+ self.select_item(ev);
+ }
+ break;
+ case $.ui.keyCode.DOWN:
+ self.move('down');
+ self.searching = false;
+ ev.preventDefault();
+ break;
+ case $.ui.keyCode.UP:
+ self.move('up');
+ self.searching = false;
+ ev.preventDefault();
+ break;
+ case $.ui.keyCode.RIGHT:
+ self.searching = false;
+ var current = self.current_result;
+ if (current && current.expand && !current.expanded) {
+ self.expand();
+ self.searching = true;
+ }
+ ev.preventDefault();
+ break;
+ case $.ui.keyCode.ESCAPE:
+ self.close();
+ self.searching = false;
+ break;
+ }
+ });
+ },
+ initiate_search: function (query) {
+ if (query === this.search_string && query !== this.current_search) {
+ this.search(query);
+ }
+ },
+ search: function (query) {
+ var self = this;
+ this.current_search = query;
+ this.source({term:query}, function (results) {
+ if (results.length) {
+ self.render_search_results(results);
+ self.focus_element(self.$('li:first-child'));
+ } else {
+ self.close();
+ }
+ });
+ },
+ render_search_results: function (results) {
+ var self = this;
+ var $list = this.$('ul');
+ $list.empty();
+ var render_separator = false;
+ results.forEach(function (result) {
+ if (result.is_separator) {
+ if (render_separator)
+ $list.append($('<li>').addClass('oe-separator'));
+ render_separator = false;
+ } else {
+ var $item = self.make_list_item(result).appendTo($list);
+ result.$el = $item;
+ render_separator = true;
+ }
+ });
+ this.show();
+ },
+ make_list_item: function (result) {
+ var self = this;
+ var $li = $('<li>')
+ .hover(function () {self.focus_element($li);})
+ .mousedown(function (ev) {
+ if (ev.button === 0) { // left button
+ self.select(ev, {item: {facet: result.facet}});
+ self.close();
+ } else {
+ ev.preventDefault();
+ }
+ })
+ .data('result', result);
+ if (result.expand) {
+ var $expand = $('<span class="oe-expand">').text('â–¶').appendTo($li);
+ $expand.mousedown(function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (result.expanded)
+ self.fold();
+ else
+ self.expand();
+ });
+ result.expanded = false;
+ }
+ if (result.indent) $li.addClass('oe-indent');
+ $li.append($('<span>').html(result.label));
+ return $li;
+ },
+ expand: function () {
+ var self = this;
+ this.current_result.expand(this.get_search_string()).then(function (results) {
+ (results || [{label: '(no result)'}]).reverse().forEach(function (result) {
+ result.indent = true;
+ var $li = self.make_list_item(result);
+ self.current_result.$el.after($li);
+ });
+ self.current_result.expanded = true;
+ self.current_result.$el.find('span.oe-expand').html('â–¼');
+ });
+ },
+ fold: function () {
+ var $next = this.current_result.$el.next();
+ while ($next.hasClass('oe-indent')) {
+ $next.remove();
+ $next = this.current_result.$el.next();
+ }
+ this.current_result.expanded = false;
+ this.current_result.$el.find('span.oe-expand').html('â–¶');
+ },
+ focus_element: function ($li) {
+ this.$('li').removeClass('oe-selection-focus');
+ $li.addClass('oe-selection-focus');
+ this.current_result = $li.data('result');
+ },
+ select_item: function (ev) {
+ if (this.current_result.facet) {
+ this.select(ev, {item: {facet: this.current_result.facet}});
+ this.close();
+ }
+ },
+ show: function () {
+ this.$el.show();
+ },
+ close: function () {
+ this.current_search = null;
+ this.search_string = null;
+ this.searching = true;
+ this.$el.hide();
+ },
+ move: function (direction) {
+ var $next;
+ if (direction === 'down') {
+ $next = this.$('li.oe-selection-focus').nextAll(':not(.oe-separator)').first();
+ if (!$next.length) $next = this.$('li:first-child');
+ } else {
+ $next = this.$('li.oe-selection-focus').prevAll(':not(.oe-separator)').first();
+ if (!$next.length) $next = this.$('li:not(.oe-separator)').last();
+ }
+ this.focus_element($next);
+ },
+ is_expandable: function () {
+ return !!this.$('.oe-selection-focus .oe-expand').length;
+ },
+});
+});
diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/views/tree_view.js
index 45cc183511a..7c3950a5407 100644
--- a/addons/web/static/src/js/view_tree.js
+++ b/addons/web/static/src/js/views/tree_view.js
@@ -1,16 +1,20 @@
+odoo.define('web.TreeView', ['web.core', 'web.data', 'web.formats', 'web.pyeval', 'web.session', 'web.View'], function (require) {
+"use strict";
/*---------------------------------------------------------
- * OpenERP web library
+ * Odoo Tree view
*---------------------------------------------------------*/
-(function() {
+var core = require('web.core');
+var data = require('web.data');
+var formats = require('web.formats');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+var View = require('web.View');
-var instance = openerp;
-openerp.web.view_tree = {};
-var QWeb = instance.web.qweb,
- _lt = instance.web._lt;
+var _lt = core._lt;
+var QWeb = core.qweb;
-instance.web.views.add('tree', 'instance.web.TreeView');
-instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeView# */{
+var TreeView = View.extend(/** @lends instance.web.TreeView# */{
display_name: _lt('Tree'),
view_type: 'tree',
/**
@@ -68,7 +72,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
// field name in OpenERP is kinda stupid: this is the name of the field
// holding the ids to the children of the current node, why call it
// field_parent?
- this.children_field = fields_view['field_parent'];
+ this.children_field = fields_view.field_parent;
this.fields_view = fields_view;
_(this.fields_view.arch.children).each(function (field) {
if (field.attrs.modifiers) {
@@ -137,7 +141,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
color_for: function (record) {
if (!this.colors) { return ''; }
var context = _.extend({}, record, {
- uid: this.session.uid,
+ uid: session.uid,
current_date: moment().format('YYYY-MM-DD')
// TODO: time, datetime, relativedelta
});
@@ -203,7 +207,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
'fields_view': self.fields_view.arch.children,
'fields': self.fields,
'level': $curr_node.data('level') || 0,
- 'render': instance.web.format_value,
+ 'render': formats.format_value,
'color_for': self.color_for,
'row_parent_id': id
});
@@ -224,8 +228,8 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
active_id: id,
active_ids: [id]
};
- var ctx = instance.web.pyeval.eval(
- 'context', new instance.web.CompoundContext(
+ var ctx = pyeval.eval(
+ 'context', new data.CompoundContext(
this.dataset.get_context(), local_context));
return this.rpc('/web/treeview/action', {
id: id,
@@ -234,7 +238,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
}).then(function (actions) {
if (!actions.length) { return; }
var action = actions[0][2];
- var c = new instance.web.CompoundContext(local_context).set_eval_context(ctx);
+ var c = new data.CompoundContext(local_context).set_eval_context(ctx);
if (action.context) {
c.add(action.context);
}
@@ -267,4 +271,10 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
this._super();
}
});
-})();
+
+core.view_registry.add('tree', TreeView);
+
+return TreeView;
+
+});
+
diff --git a/addons/web/static/src/js/watch.js b/addons/web/static/src/js/watch.js
deleted file mode 100644
index efa80a1d529..00000000000
--- a/addons/web/static/src/js/watch.js
+++ /dev/null
@@ -1,58 +0,0 @@
-(function() {
-/*
- * object.watch polyfill
- *
- * 2012-04-03
- *
- * By Eli Grey, http://eligrey.com
- * Public Domain.
- * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
- */
-
-// object.watch
-if (!Object.prototype.watch) {
- Object.defineProperty(Object.prototype, "watch", {
- enumerable: false
- , configurable: true
- , writable: false
- , value: function (prop, handler) {
- var
- oldval = this[prop]
- , newval = oldval
- , getter = function () {
- return newval;
- }
- , setter = function (val) {
- oldval = newval;
- return newval = handler.call(this, prop, oldval, val);
- }
- ;
-
- if (delete this[prop]) { // can't watch constants
- Object.defineProperty(this, prop, {
- get: getter
- , set: setter
- , enumerable: true
- , configurable: true
- });
- }
- }
- });
-}
-
-// object.unwatch
-if (!Object.prototype.unwatch) {
- Object.defineProperty(Object.prototype, "unwatch", {
- enumerable: false
- , configurable: true
- , writable: false
- , value: function (prop) {
- var val = this[prop];
- delete this[prop]; // remove accessors
- this[prop] = val;
- }
- });
-}
-
-})();
-
diff --git a/addons/web/static/src/js/web_client.js b/addons/web/static/src/js/web_client.js
new file mode 100644
index 00000000000..e4d943ce6bf
--- /dev/null
+++ b/addons/web/static/src/js/web_client.js
@@ -0,0 +1,368 @@
+odoo.define('web.WebClient', ['web.ActionManager', 'web.core', 'web.crash_manager', 'web.data', 'web.Loading', 'web.Menu', 'web.Model', 'web.Notification', 'web.session', 'web.SystrayMenu', 'web.UserMenu', 'web.utils', 'web.Widget'], function (require) {
+"use strict";
+
+var ActionManager = require('web.ActionManager');
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var data = require('web.data');
+var Loading = require('web.Loading');
+var Menu = require('web.Menu');
+var Model = require('web.Model');
+var Notification = require('web.Notification');
+var session = require('web.session');
+var SystrayMenu = require('web.SystrayMenu');
+var UserMenu = require('web.UserMenu');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+
+var WebClient = Widget.extend({
+ events: {
+ 'click .oe_logo_edit_admin': 'logo_edit',
+ 'click .oe_logo img': 'on_logo_click',
+ },
+
+ init: function(parent, client_options) {
+ this.client_options = {};
+ this._super(parent);
+ this.origin = undefined;
+ if (client_options) {
+ _.extend(this.client_options, client_options);
+ }
+ this._current_state = null;
+ this.menu_dm = new utils.DropMisordered();
+ this.action_mutex = new utils.Mutex();
+ this.set('title_part', {"zopenerp": "Odoo"});
+ },
+ start: function() {
+ var self = this;
+ this.on("change:title_part", this, this._title_changed);
+ this._title_changed();
+
+ core.bus.on('web_client_toggle_bars', this, function () {
+ this.toggle_bars.apply(this, arguments);
+ });
+
+ return session.session_bind(this.origin).then(function() {
+ self.bind_events();
+ return self.show_common();
+ }).then(function() {
+ if (session.session_is_valid()) {
+ self.show_application();
+ }
+ if (self.client_options.action) {
+ self.action_manager.do_action(self.client_options.action);
+ delete(self.client_options.action);
+ }
+ self.trigger('web_client_ready');
+ });
+ },
+ bind_events: function() {
+ var self = this;
+ $('.oe_systray').show();
+ this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
+ $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
+ });
+ this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
+ ev.preventDefault();
+ var $toggle = $(this);
+ var doc_width = $(document).width();
+ var $menu = $toggle.siblings('.oe_dropdown_menu');
+ $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
+ var state = $menu.is('.oe_opened');
+ setTimeout(function() {
+ // Do not alter propagation
+ $toggle.add($menu).toggleClass('oe_opened', !state);
+ if (!state) {
+ // Move $menu if outside window's edge
+ var offset = $menu.offset();
+ var menu_width = $menu.width();
+ var x = doc_width - offset.left - menu_width - 2;
+ if (x < 0) {
+ $menu.offset({ left: offset.left + x }).width(menu_width);
+ }
+ }
+ }, 0);
+ });
+ core.bus.on('click', this, function(ev) {
+ $('.tooltip').remove();
+ if (!$(ev.target).is('input[type=file]')) {
+ self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
+ }
+ });
+ core.bus.on('set_full_screen', this, function (full_screen) {
+ this.set_content_full_screen(full_screen);
+ });
+ },
+ on_logo_click: function(ev){
+ if (!this.has_uncommitted_changes()) {
+ return;
+ } else {
+ ev.preventDefault();
+ }
+ },
+ show_common: function() {
+ var self = this;
+ session.on('error', crash_manager, crash_manager.rpc_error);
+ self.notification = new Notification(this);
+ self.notification.appendTo(self.$('.openerp'));
+ self.loading = new Loading(self);
+ self.loading.appendTo(self.$('.openerp_webclient_container'));
+ self.action_manager = new ActionManager(self);
+ self.action_manager.replace(self.$('.oe_application'));
+
+ core.bus.on('display_notification_warning', this, function (title, message) {
+ self.notification.warn(title, message);
+ });
+ window.onerror = function (message, file, line) {
+ crash_manager.show_error({
+ type: _t("Client Error"),
+ message: message,
+ data: {debug: file + ':' + line}
+ });
+ };
+
+ },
+ toggle_bars: function(value) {
+ this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
+ },
+ has_uncommitted_changes: function() {
+ var $e = $.Event('clear_uncommitted_changes');
+ core.bus.trigger('clear_uncommitted_changes', $e);
+ return $e.isDefaultPrevented();
+ },
+ /**
+ Sets the first part of the title of the window, dedicated to the current action.
+ */
+ set_title: function(title) {
+ this.set_title_part("action", title);
+ },
+ /**
+ Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
+ a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
+ title of the window separated by '-'.
+ */
+ set_title_part: function(part, title) {
+ var tmp = _.clone(this.get("title_part"));
+ tmp[part] = title;
+ this.set("title_part", tmp);
+ },
+ _title_changed: function() {
+ var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
+ var tmp = "";
+ _.each(parts, function(part) {
+ var str = this.get("title_part")[part];
+ if (str) {
+ tmp = tmp ? tmp + " - " + str : str;
+ }
+ }, this);
+ document.title = tmp;
+ },
+ show_application: function() {
+ var self = this;
+ self.toggle_bars(true);
+
+ self.update_logo();
+
+ // Menu is rendered server-side thus we don't want the widget to create any dom
+ self.menu = new Menu(self);
+ self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
+ self.menu.on('menu_click', this, this.on_menu_action);
+
+ // Create the user menu (rendered client-side)
+ self.user_menu = new UserMenu(self);
+ var user_menu_loaded = self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
+ self.user_menu.on('user_logout', self, self.on_logout);
+ self.user_menu.do_update();
+
+ // Create the systray menu (rendered server-side)
+ self.systray_menu = new SystrayMenu(self);
+ self.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
+ var systray_menu_loaded = self.systray_menu.start();
+
+ // Start the menu once both systray and user menus are rendered
+ // to prevent overflows while loading
+ $.when(systray_menu_loaded, user_menu_loaded).done(function() {
+ self.menu.start();
+ });
+
+ self.bind_hashchange();
+ self.set_title();
+ self.check_timezone();
+ if (self.client_options.action_post_login) {
+ self.action_manager.do_action(self.client_options.action_post_login);
+ delete(self.client_options.action_post_login);
+ }
+ },
+ update_logo: function() {
+ var company = session.company_id;
+ var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
+ this.$('.oe_logo img').attr('src', '').attr('src', img);
+ this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.uid === 1);
+ },
+ logo_edit: function(ev) {
+ var self = this;
+ ev.preventDefault();
+ self.alive(new Model("res.users").get_func("read")(session.uid, ["company_id"])).then(function(res) {
+ self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
+ result.res_id = res.company_id[0];
+ result.target = "new";
+ result.views = [[false, 'form']];
+ result.flags = {
+ action_buttons: true,
+ headless: true,
+ };
+ self.action_manager.do_action(result);
+ var form = self.action_manager.dialog_widget.views.form.controller;
+ form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
+ form.on('record_saved', self, function() {
+ self.action_manager.dialog_stop();
+ self.update_logo();
+ });
+ });
+ });
+ return false;
+ },
+ check_timezone: function() {
+ var self = this;
+ return self.alive(new Model('res.users').call('read', [[session.uid], ['tz_offset']])).then(function(result) {
+ var user_offset = result[0]['tz_offset'];
+ var offset = -(new Date().getTimezoneOffset());
+ // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
+ var browser_offset = (offset < 0) ? "-" : "+";
+ browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
+ browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
+ if (browser_offset !== user_offset) {
+ var $icon = $(QWeb.render('WebClient.timezone_systray'));
+ $icon.on('click', function() {
+ var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
+ user_timezone: session.user_context.tz || 'UTC',
+ user_offset: user_offset,
+ browser_offset: browser_offset,
+ }), true);
+ notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
+ notification.close();
+ }).find('a').on('click', function() {
+ notification.close();
+ self.user_menu.on_menu_settings();
+ return false;
+ });
+ });
+ $icon.prependTo(window.$('.oe_systray'));
+ }
+ });
+ },
+ destroy_content: function() {
+ _.each(_.clone(this.getChildren()), function(el) {
+ el.destroy();
+ });
+ this.$el.children().remove();
+ },
+ do_reload: function() {
+ var self = this;
+ return this.session.session_reload().then(function () {
+ session.load_modules(true).then(
+ self.menu.proxy('do_reload')); });
+ },
+ do_notify: function() {
+ var n = this.notification;
+ return n.notify.apply(n, arguments);
+ },
+ do_warn: function() {
+ var n = this.notification;
+ return n.warn.apply(n, arguments);
+ },
+ on_logout: function() {
+ var self = this;
+ if (!this.has_uncommitted_changes()) {
+ self.action_manager.do_action('logout');
+ }
+ },
+ bind_hashchange: function() {
+ var self = this;
+ $(window).bind('hashchange', this.on_hashchange);
+
+ var state = $.bbq.getState(true);
+ if (_.isEmpty(state) || state.action == "login") {
+ self.menu.is_bound.done(function() {
+ new Model("res.users").call("read", [session.uid, ["action_id"]]).done(function(data) {
+ if(data.action_id) {
+ self.action_manager.do_action(data.action_id[0]);
+ self.menu.open_action(data.action_id[0]);
+ } else {
+ var first_menu_id = self.menu.$el.find("a:first").data("menu");
+ if(first_menu_id) {
+ self.menu.menu_click(first_menu_id);
+ } }
+ });
+ });
+ } else {
+ $(window).trigger('hashchange');
+ }
+ },
+ on_hashchange: function(event) {
+ var self = this;
+ var stringstate = event.getState(false);
+ if (!_.isEqual(this._current_state, stringstate)) {
+ var state = event.getState(true);
+ if(!state.action && state.menu_id) {
+ self.menu.is_bound.done(function() {
+ self.menu.menu_click(state.menu_id);
+ });
+ } else {
+ state._push_me = false; // no need to push state back...
+ this.action_manager.do_load_state(state, !!this._current_state);
+ }
+ }
+ this._current_state = stringstate;
+ },
+ do_push_state: function(state) {
+ this.set_title(state.title);
+ delete state.title;
+ var url = '#' + $.param(state);
+ this._current_state = $.deparam($.param(state), false); // stringify all values
+ $.bbq.pushState(url);
+ this.trigger('state_pushed', state);
+ },
+ on_menu_action: function(options) {
+ var self = this;
+ return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
+ .then(function (result) {
+ return self.action_mutex.exec(function() {
+ if (options.needaction) {
+ result.context = new data.CompoundContext(result.context, {
+ search_default_message_unread: true,
+ search_disable_custom_filters: true,
+ });
+ }
+ var completed = $.Deferred();
+ $.when(self.action_manager.do_action(result, {
+ clear_breadcrumbs: true,
+ action_menu_id: self.menu.current_menu,
+ })).fail(function() {
+ self.menu.open_menu(options.previous_menu_id);
+ }).always(function() {
+ completed.resolve();
+ });
+ setTimeout(function() {
+ completed.resolve();
+ }, 2000);
+ // We block the menu when clicking on an element until the action has correctly finished
+ // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
+ return completed;
+ });
+ });
+ },
+ set_content_full_screen: function(fullscreen) {
+ $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
+ this.$('.oe_webclient').toggleClass(
+ 'oe_content_full_screen', fullscreen);
+ },
+});
+
+return WebClient;
+
+});
diff --git a/addons/web/static/src/js/widgets/change_password.js b/addons/web/static/src/js/widgets/change_password.js
new file mode 100644
index 00000000000..d1b7e98c74e
--- /dev/null
+++ b/addons/web/static/src/js/widgets/change_password.js
@@ -0,0 +1,47 @@
+odoo.define('web.ChangePassword', ['web.core', 'web.Dialog', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+
+var ChangePassword = Widget.extend({
+ template: "ChangePassword",
+ start: function() {
+ var self = this;
+ this.getParent().dialog_title = _t("Change Password");
+ var $button = self.$el.find('.oe_form_button');
+ $button.appendTo(this.getParent().$buttons);
+ $button.eq(2).click(function(){
+ self.$el.parents('.modal').modal('hide');
+ });
+ $button.eq(0).click(function(){
+ self.rpc("/web/session/change_password",{
+ 'fields': $("form[name=change_password_form]").serializeArray()
+ }).done(function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ } else {
+ self.do_action('logout');
+ }
+ });
+ });
+ },
+ display_error: function (error) {
+ return new Dialog(this, {
+ size: 'medium',
+ title: error.title,
+ buttons: [
+ {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
+ ]
+ }, $('<div>').html(error.error)).open();
+ },
+});
+
+core.action_registry.add("change_password", ChangePassword);
+
+return ChangePassword;
+});
diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index b36a927307a..73ec37cdd17 100644
--- a/addons/web/static/src/js/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -1,12 +1,16 @@
+odoo.define('web.DataExport', ['web.core', 'web.crash_manager', 'web.data', 'web.Dialog', 'web.framework'], function (require) {
+"use strict";
-(function() {
+var core = require('web.core');
+var crash_manager = require('web.crash_manager');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
-var instance = openerp;
-openerp.web.data_export = {};
+var QWeb = core.qweb;
+var _t = core._t;
-var QWeb = instance.web.qweb,
- _t = instance.web._t;
-instance.web.DataExport = instance.web.Dialog.extend({
+var DataExport = Dialog.extend({
template: 'ExportTreeView',
dialog_title: {toString: function () { return _t("Export Data"); }},
events: {
@@ -42,7 +46,7 @@ instance.web.DataExport = instance.web.Dialog.extend({
this._super(parent, options);
this.records = {};
this.dataset = dataset;
- this.exports = new instance.web.DataSetSearch(
+ this.exports = new data.DataSetSearch(
this, 'ir.exports', this.dataset.get_context());
},
start: function() {
@@ -82,16 +86,16 @@ instance.web.DataExport = instance.web.Dialog.extend({
this.show_exports_list());
},
on_click_move_up: function () {
- var prev_row = this.$el.find('#fields_list option:selected').first().prev();
+ var prev_row = this.$('#fields_list option:selected').first().prev();
if(prev_row.length){
- var selected_rows = self.$('#fields_list option:selected').detach();
+ var selected_rows = this.$('#fields_list option:selected').detach();
prev_row.before(selected_rows);
}
},
on_click_move_down: function () {
- var next_row = this.$el.find('#fields_list option:selected').last().next();
+ var next_row = this.$('#fields_list option:selected').last().next();
if(next_row.length){
- var selected_rows = self.$('#fields_list option:selected').detach();
+ var selected_rows = this.$('#fields_list option:selected').detach();
next_row.after(selected_rows);
}
},
@@ -413,9 +417,8 @@ instance.web.DataExport = instance.web.Dialog.extend({
exported_fields.unshift({name: 'id', label: 'External ID'});
var export_format = this.$el.find("#export_format").val();
- var c = instance.webclient.crashmanager;
- instance.web.blockUI();
+ framework.blockUI();
this.session.get_file({
url: '/web/export/' + export_format,
data: {data: JSON.stringify({
@@ -426,8 +429,8 @@ instance.web.DataExport = instance.web.Dialog.extend({
context: this.dataset.context,
import_compat: !!this.$el.find("#import_compat").val(),
})},
- complete: instance.web.unblockUI,
- error: c.rpc_error.bind(c),
+ complete: framework.unblockUI,
+ error: crash_manager.rpc_error.bind(crash_manager),
});
},
close: function() {
@@ -435,4 +438,5 @@ instance.web.DataExport = instance.web.Dialog.extend({
}
});
-})();
+return DataExport;
+});
diff --git a/addons/web/static/src/js/widgets/database_manager.js b/addons/web/static/src/js/widgets/database_manager.js
new file mode 100644
index 00000000000..9bd5b4d05c3
--- /dev/null
+++ b/addons/web/static/src/js/widgets/database_manager.js
@@ -0,0 +1,217 @@
+odoo.define('web.DatabaseManager', ['web.core', 'web.Dialog', 'web.framework', 'web.web_client', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+var DataBaseManager = Widget.extend({
+ init: function(parent) {
+ this._super(parent);
+ $.validator.addMethod('matches', function (s, _, re) {
+ return new RegExp(re).test(s);
+ }, _t("Invalid database name"));
+ },
+ start: function() {
+ var self = this;
+ $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
+ var fetch_db = this.rpc("/web/database/get_list", {}).then(
+ function(result) {
+ self.db_list = result;
+ },
+ function (_, ev) {
+ ev.preventDefault();
+ self.db_list = null;
+ });
+ var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
+ self.lang_list = result;
+ });
+ return $.when(fetch_db, fetch_langs).always(self.do_render);
+ },
+ do_render: function() {
+ var self = this;
+ core.bus.trigger('web_client_toggle_bars', true);
+ self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
+ $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
+ $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
+ $('ul.oe_secondary_submenu > li:first').addClass('active');
+ $('ul.oe_secondary_submenu > li').bind('click', function (event) {
+ var menuitem = $(this);
+ menuitem.addClass('active').siblings().removeClass('active');
+ var form_id =menuitem.find('a').attr('href');
+ $(form_id).show().siblings().hide();
+ event.preventDefault();
+ });
+ $('#back-to-login').click(self.do_exit);
+ self.$el.find("td").addClass("oe_form_group_cell");
+ self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
+ self.$el.find("label").addClass("oe_form_label");
+ self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
+ self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
+ self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
+ self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
+ self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
+ self.$el.find("form[name=change_pwd_form]").validate({
+ messages: {
+ old_pwd: _t("Please enter your previous password"),
+ new_pwd: _t("Please enter your new password"),
+ confirm_pwd: {
+ required: _t("Please confirm your new password"),
+ equalTo: _t("The confirmation does not match the password")
+ }
+ },
+ submitHandler: self.do_change_password
+ });
+ },
+ destroy: function () {
+ this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
+ this._super();
+ },
+ /**
+ * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
+ * from unblocking the UI
+ */
+ blockUI: function () {
+ framework.blockUI();
+ $.unblockUI = function () {};
+ },
+ /**
+ * Reinstates $.unblockUI so third parties can play with blockUI, and
+ * unblocks the UI
+ */
+ unblockUI: function () {
+ $.unblockUI = framework.unblockUI;
+ framework.unblockUI();
+ },
+ /**
+ * Displays an error dialog resulting from the various RPC communications
+ * failing over themselves
+ *
+ * @param {Object} error error description
+ * @param {String} error.title title of the error dialog
+ * @param {String} error.error message of the error dialog
+ */
+ display_error: function (error) {
+ return new Dialog(this, {
+ size: 'medium',
+ title: error.title,
+ buttons: [
+ {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
+ ]
+ }, $('<div>').html(error.error)).open();
+ },
+ do_create: function(form) {
+ var fields = $(form).serializeArray();
+ this.rpc("/web/database/create", {'fields': fields}).done(function(result) {
+ if (result) {
+ framework.redirect('/web');
+ } else {
+ alert("Failed to create database");
+ }
+ });
+ },
+ do_duplicate: function(form) {
+ var self = this;
+ var fields = $(form).serializeArray();
+ self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
+ self.start();
+ });
+ },
+ do_drop: function(form) {
+ var self = this;
+ var $form = $(form),
+ fields = $form.serializeArray(),
+ $db_list = $form.find('[name=drop_db]'),
+ db = $db_list.val();
+ if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
+ return;
+ }
+ self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
+ self.start();
+ });
+ },
+ do_backup: function(form) {
+ var self = this;
+ self.blockUI();
+ self.session.get_file({
+ form: form,
+ success: function () {
+ self.do_notify(_t("Backed"), _t("Database backed up successfully"));
+ },
+ error: function(error){
+ if (error && error[1]) {
+ self.display_error(error[1][0]);
+ }
+ },
+ complete: function() {
+ self.unblockUI();
+ }
+ });
+ },
+ do_restore: function(form) {
+ var self = this;
+ self.blockUI();
+ $(form).ajaxSubmit({
+ url: '/web/database/restore',
+ type: 'POST',
+ resetForm: true,
+ success: function (body) {
+ // If empty body, everything went fine
+ if (!body) { return; }
+
+ if (body.indexOf('403 Forbidden') !== -1) {
+ self.display_error({
+ title: _t("Access Denied"),
+ error: _t("Incorrect super-administrator password")
+ });
+ } else {
+ self.display_error({
+ title: _t("Restore Database"),
+ error: _t("Could not restore the database")
+ });
+ }
+ },
+ complete: function() {
+ self.unblockUI();
+ self.do_notify(_t("Restored"), _t("Database restored successfully"));
+ }
+ });
+ },
+ do_change_password: function(form) {
+ var self = this;
+ self.rpc("/web/database/change_password", {
+ 'fields': $(form).serializeArray()
+ }).done(function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ self.unblockUI();
+ self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
+ });
+ },
+ do_exit: function () {
+ this.$el.remove();
+ framework.redirect('/web');
+ }
+});
+
+core.action_registry.add("database_manager", DataBaseManager);
+
+return DataBaseManager;
+
+});
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
new file mode 100644
index 00000000000..7e242cf1d6e
--- /dev/null
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -0,0 +1,114 @@
+odoo.define('web.datepicker', ['web.core', 'web.formats', 'web.time', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var formats = require('web.formats');
+var time = require('web.time');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+
+var DateTimeWidget = Widget.extend({
+ template: "web.datepicker",
+ type_of_date: "datetime",
+ events: {
+ 'dp.change .oe_datepicker_main': 'change_datetime',
+ 'dp.show .oe_datepicker_main': 'set_datetime_default',
+ 'keypress .oe_datepicker_master': 'change_datetime',
+ },
+ init: function(parent) {
+ this._super(parent);
+ this.name = parent.name;
+ },
+ start: function() {
+ var l10n = _t.database.parameters;
+ var options = {
+ pickTime: true,
+ useSeconds: true,
+ startDate: moment({ y: 1900 }),
+ endDate: moment().add(200, "y"),
+ calendarWeeks: true,
+ icons : {
+ time: 'fa fa-clock-o',
+ date: 'fa fa-calendar',
+ up: 'fa fa-chevron-up',
+ down: 'fa fa-chevron-down'
+ },
+ language : moment.locale(),
+ format : time.strftime_to_moment_format(l10n.date_format +' '+ l10n.time_format),
+ };
+ this.$input = this.$el.find('input.oe_datepicker_master');
+ if (this.type_of_date === 'date') {
+ options.pickTime = false;
+ options.useSeconds = false;
+ options.format = time.strftime_to_moment_format(l10n.date_format);
+ }
+ this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
+ this.set_readonly(false);
+ this.set({'value': false});
+ },
+ set_value: function(value_) {
+ this.set({'value': value_});
+ this.$input.val(value_ ? this.format_client(value_) : '');
+ },
+ get_value: function() {
+ return this.get('value');
+ },
+ set_value_from_ui_: function() {
+ var value_ = this.$input.val() || false;
+ this.set_value(this.parse_client(value_));
+ },
+ set_readonly: function(readonly) {
+ this.readonly = readonly;
+ this.$input.prop('readonly', this.readonly);
+ },
+ is_valid_: function() {
+ var value_ = this.$input.val();
+ if (value_ === "") {
+ return true;
+ } else {
+ try {
+ this.parse_client(value_);
+ return true;
+ } catch(e) {
+ return false;
+ }
+ }
+ },
+ parse_client: function(v) {
+ return formats.parse_value(v, {"widget": this.type_of_date});
+ },
+ format_client: function(v) {
+ return formats.format_value(v, {"widget": this.type_of_date});
+ },
+ set_datetime_default: function(){
+ //when opening datetimepicker the date and time by default should be the one from
+ //the input field if any or the current day otherwise
+ if (this.type_of_date === 'datetime') {
+ var value = moment().second(0);
+ if (this.$input.val().length !== 0 && this.is_valid_()){
+ value = this.$input.val();
+ }
+ this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
+ }
+ },
+ change_datetime: function(e) {
+ if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
+ this.set_value_from_ui_();
+ this.trigger("datetime_changed");
+ }
+ },
+ commit_value: function () {
+ this.change_datetime();
+ },
+});
+
+var DateWidget = DateTimeWidget.extend({
+ type_of_date: "date"
+});
+
+return {
+ DateTimeWidget: DateTimeWidget,
+ DateWidget: DateWidget,
+};
+});
diff --git a/addons/web/static/src/js/widgets/loading.js b/addons/web/static/src/js/widgets/loading.js
new file mode 100644
index 00000000000..75ac1a8b4b8
--- /dev/null
+++ b/addons/web/static/src/js/widgets/loading.js
@@ -0,0 +1,66 @@
+odoo.define('web.Loading', ['web.core', 'web.framework', 'web.session', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var framework = require('web.framework');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+
+var Loading = Widget.extend({
+ template: _t("Loading"),
+ init: function(parent) {
+ this._super(parent);
+ this.count = 0;
+ this.blocked_ui = false;
+ session.on("request", this, this.request_call);
+ session.on("response", this, this.response_call);
+ session.on("response_failed", this, this.response_call);
+ },
+ destroy: function() {
+ this.on_rpc_event(-this.count);
+ this._super();
+ },
+ request_call: function() {
+ this.on_rpc_event(1);
+ },
+ response_call: function() {
+ this.on_rpc_event(-1);
+ },
+ on_rpc_event : function(increment) {
+ var self = this;
+ if (!this.count && increment === 1) {
+ // Block UI after 3s
+ this.long_running_timer = setTimeout(function () {
+ self.blocked_ui = true;
+ framework.blockUI();
+ }, 3000);
+ }
+
+ this.count += increment;
+ if (this.count > 0) {
+ if (session.debug) {
+ this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
+ } else {
+ this.$el.text(_t("Loading"));
+ }
+ this.$el.show();
+ this.getParent().$el.addClass('oe_wait');
+ } else {
+ this.count = 0;
+ clearTimeout(this.long_running_timer);
+ // Don't unblock if blocked by somebody else
+ if (self.blocked_ui) {
+ this.blocked_ui = false;
+ framework.unblockUI();
+ }
+ this.$el.fadeOut();
+ this.getParent().$el.removeClass('oe_wait');
+ }
+ }
+});
+
+return Loading;
+});
+
diff --git a/addons/web/static/src/js/widgets/notification.js b/addons/web/static/src/js/widgets/notification.js
new file mode 100644
index 00000000000..d99ef75b324
--- /dev/null
+++ b/addons/web/static/src/js/widgets/notification.js
@@ -0,0 +1,44 @@
+odoo.define('web.Notification', ['web.Widget'], function (require) {
+"use strict";
+
+var Widget = require('web.Widget');
+
+var Notification = Widget.extend({
+ template: 'Notification',
+ init: function() {
+ this._super.apply(this, arguments);
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$el.notify({
+ speed: 500,
+ expires: 2500
+ });
+ },
+ notify: function(title, text, sticky) {
+ sticky = !!sticky;
+ var opts = {};
+ if (sticky) {
+ opts.expires = false;
+ }
+ return this.$el.notify('create', {
+ title: title,
+ text: text
+ }, opts);
+ },
+ warn: function(title, text, sticky) {
+ sticky = !!sticky;
+ var opts = {};
+ if (sticky) {
+ opts.expires = false;
+ }
+ return this.$el.notify('create', 'oe_notification_alert', {
+ title: title,
+ text: text
+ }, opts);
+ }
+});
+
+return Notification;
+
+});
diff --git a/addons/web/static/src/js/widgets/sidebar.js b/addons/web/static/src/js/widgets/sidebar.js
new file mode 100644
index 00000000000..7005ec2012e
--- /dev/null
+++ b/addons/web/static/src/js/widgets/sidebar.js
@@ -0,0 +1,215 @@
+odoo.define('web.Sidebar', ['web.core', 'web.data', 'web.Dialog', 'web.framework', 'web.pyeval', 'web.session', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+var Sidebar = Widget.extend({
+ init: function(parent) {
+ var self = this;
+ this._super(parent);
+ this.sections = [
+ { 'name' : 'print', 'label' : _t('Print'), },
+ { 'name' : 'other', 'label' : _t('More'), }
+ ];
+ this.items = {
+ 'print' : [],
+ 'other' : []
+ };
+ this.fileupload_id = _.uniqueId('oe_fileupload');
+ $(window).on(this.fileupload_id, function() {
+ var args = [].slice.call(arguments).slice(1);
+ self.do_attachement_update(self.dataset, self.model_id,args);
+ framework.unblockUI();
+ });
+ },
+ start: function() {
+ var self = this;
+ this._super(this);
+ this.redraw();
+ this.$el.on('click','.dropdown-menu li a', function(event) {
+ var section = $(this).data('section');
+ var index = $(this).data('index');
+ var item = self.items[section][index];
+ if (item.callback) {
+ item.callback.apply(self, [item]);
+ } else if (item.action) {
+ self.on_item_action_clicked(item);
+ } else if (item.url) {
+ return true;
+ }
+ event.preventDefault();
+ });
+ },
+ redraw: function() {
+ var self = this;
+ self.$el.html(QWeb.render('Sidebar', {widget: self}));
+
+ // Hides Sidebar sections when item list is empty
+ this.$('.oe_form_dropdown_section').each(function() {
+ $(this).toggle(!!$(this).find('li').length);
+ });
+ self.$("[title]").tooltip({
+ delay: { show: 500, hide: 0}
+ });
+ },
+ /**
+ * For each item added to the section:
+ *
+ * ``label``
+ * will be used as the item's name in the sidebar, can be html
+ *
+ * ``action``
+ * descriptor for the action which will be executed, ``action`` and
+ * ``callback`` should be exclusive
+ *
+ * ``callback``
+ * function to call when the item is clicked in the sidebar, called
+ * with the item descriptor as its first argument (so information
+ * can be stored as additional keys on the object passed to
+ * ``add_items``)
+ *
+ * ``classname`` (optional)
+ * ``@class`` set on the sidebar serialization of the item
+ *
+ * ``title`` (optional)
+ * will be set as the item's ``@title`` (tooltip)
+ *
+ * @param {String} section_code
+ * @param {Array<{label, action | callback[, classname][, title]}>} items
+ */
+ add_items: function(section_code, items) {
+ if (items) {
+ this.items[section_code].unshift.apply(this.items[section_code],items);
+ this.redraw();
+ }
+ },
+ add_toolbar: function(toolbar) {
+ var self = this;
+ _.each(['print','action','relate'], function(type) {
+ var items = toolbar[type];
+ if (items) {
+ for (var i = 0; i < items.length; i++) {
+ items[i] = {
+ label: items[i]['name'],
+ action: items[i],
+ classname: 'oe_sidebar_' + type
+ };
+ }
+ self.add_items(type=='print' ? 'print' : 'other', items);
+ }
+ });
+ },
+ on_item_action_clicked: function(item) {
+ var self = this;
+ self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) {
+ var ids = self.getParent().get_selected_ids();
+ var domain;
+ if (self.getParent().get_active_domain) {
+ domain = self.getParent().get_active_domain();
+ }
+ else {
+ domain = $.Deferred().resolve(undefined);
+ }
+ if (ids.length === 0) {
+ new Dialog(this, { title: _t("Warning"), size: 'medium',}, $("<div />").text(_t("You must choose at least one record."))).open();
+ return false;
+ }
+ var dataset = self.getParent().dataset;
+ var active_ids_context = {
+ active_id: ids[0],
+ active_ids: ids,
+ active_model: dataset.model,
+ };
+
+ $.when(domain).done(function (domain) {
+ if (domain !== undefined) {
+ active_ids_context.active_domain = domain;
+ }
+ var c = pyeval.eval('context',
+ new data.CompoundContext(
+ sidebar_eval_context, active_ids_context));
+
+ self.rpc("/web/action/load", {
+ action_id: item.action.id,
+ context: new data.CompoundContext(
+ dataset.get_context(), active_ids_context).eval()
+ }).done(function(result) {
+ result.context = new data.CompoundContext(
+ result.context || {}, active_ids_context)
+ .set_eval_context(c);
+ result.flags = result.flags || {};
+ result.flags.new_window = true;
+ self.do_action(result, {
+ on_close: function() {
+ // reload view
+ self.getParent().reload();
+ },
+ });
+ });
+ });
+ });
+ },
+ do_attachement_update: function(dataset, model_id, args) {
+ this.dataset = dataset;
+ this.model_id = model_id;
+ if (args && args[0].error) {
+ this.do_warn(_t('Uploading Error'), args[0].error);
+ }
+ if (!model_id) {
+ this.on_attachments_loaded([]);
+ } else {
+ var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
+ var ds = new data.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
+ ds.read_slice(['name', 'url', 'type', 'create_uid', 'create_date', 'write_uid', 'write_date'], {}).done(this.on_attachments_loaded);
+ }
+ },
+ on_attachments_loaded: function(attachments) {
+ var self = this;
+ var prefix = session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
+ _.each(attachments,function(a) {
+ a.label = a.name;
+ if(a.type === "binary") {
+ a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime());
+ }
+ });
+ self.items.files = attachments;
+ self.redraw();
+ this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
+ this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
+ },
+ on_attachment_changed: function(e) {
+ var $e = $(e.target);
+ if ($e.val() !== '') {
+ this.$el.find('form.oe_form_binary_form').submit();
+ $e.parent().find('input[type=file]').prop('disabled', true);
+ $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
+ this.$('.oe_sidebar_add_attachment a').text(_t('Uploading...'));
+ framework.blockUI();
+ }
+ },
+ on_attachment_delete: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var self = this;
+ var $e = $(e.currentTarget);
+ if (confirm(_t("Do you really want to delete this attachment ?"))) {
+ (new data.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).done(function() {
+ self.do_attachement_update(self.dataset, self.model_id);
+ });
+ }
+ }
+});
+
+return Sidebar;
+
+});
+
diff --git a/addons/web/static/src/js/widgets/systray_menu.js b/addons/web/static/src/js/widgets/systray_menu.js
new file mode 100644
index 00000000000..eb218a51bc7
--- /dev/null
+++ b/addons/web/static/src/js/widgets/systray_menu.js
@@ -0,0 +1,38 @@
+odoo.define('web.SystrayMenu', ['web.Widget'], function (require) {
+"use strict";
+
+var Widget = require('web.Widget');
+
+var SystrayMenu = Widget.extend({
+ /**
+ * This widget renders the systray menu. It creates and renders widgets
+ * pushed in instance.web.SystrayItems.
+ */
+ init: function(parent) {
+ this._super(parent);
+ this.items = [];
+ this.load = $.Deferred();
+ },
+ start: function() {
+ var self = this;
+ self._super.apply(this, arguments);
+ self.load_items();
+ return $.when.apply($, self.items).done(function () {
+ self.load.resolve();
+ });
+ },
+ load_items: function() {
+ var self = this;
+ _.each(SystrayMenu.Items, function(widgetCls) {
+ var cur_systray_item = new widgetCls(self);
+ self.items.push(cur_systray_item.appendTo(self.$el));
+ });
+ },
+});
+
+SystrayMenu.Items = [];
+
+return SystrayMenu;
+
+});
+
diff --git a/addons/web/static/src/js/widgets/user_menu.js b/addons/web/static/src/js/widgets/user_menu.js
new file mode 100644
index 00000000000..450b0191486
--- /dev/null
+++ b/addons/web/static/src/js/widgets/user_menu.js
@@ -0,0 +1,112 @@
+odoo.define('web.UserMenu', ['web.core', 'web.Dialog', 'web.framework', 'web.Model', 'web.session', 'web.Widget'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
+var Model = require('web.Model');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+var SystrayMenu = Widget.extend({
+ template: "UserMenu",
+ init: function(parent) {
+ this._super(parent);
+ this.update_promise = $.Deferred().resolve();
+ },
+ start: function() {
+ var self = this;
+ this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
+ ev.preventDefault();
+ var f = self['on_menu_' + $(this).data('menu')];
+ if (f) {
+ f($(this));
+ }
+ });
+ this.$el.parent().show();
+ return this._super.apply(this, arguments);
+ },
+ do_update: function () {
+ var self = this;
+ var fct = function() {
+ var $avatar = self.$el.find('.oe_topbar_avatar');
+ $avatar.attr('src', $avatar.data('default-src'));
+ if (!session.uid)
+ return;
+ var func = new Model("res.users").get_func("read");
+ return self.alive(func(session.uid, ["name", "company_id"])).then(function(res) {
+ var topbar_name = res.name;
+ if(session.debug)
+ topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db);
+ if(res.company_id[0] > 1)
+ topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
+ self.$el.find('.oe_topbar_name').text(topbar_name);
+ if (!session.debug) {
+ topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db);
+ }
+ var avatar_src = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: session.uid});
+ $avatar.attr('src', avatar_src);
+
+ core.bus.trigger('resize'); // Re-trigger the reflow logic
+ });
+ };
+ this.update_promise = this.update_promise.then(fct, fct);
+ },
+ on_menu_help: function() {
+ window.open('http://help.odoo.com', '_blank');
+ },
+ on_menu_logout: function() {
+ this.trigger('user_logout');
+ },
+ on_menu_settings: function() {
+ var self = this;
+ if (!this.getParent().has_uncommitted_changes()) {
+ self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
+ result.res_id = session.uid;
+ self.getParent().action_manager.do_action(result);
+ });
+ }
+ },
+ on_menu_account: function() {
+ if (!this.getParent().has_uncommitted_changes()) {
+ var P = new Model('ir.config_parameter');
+ P.call('get_param', ['database.uuid']).then(function(dbuuid) {
+ var state = {
+ 'd': session.db,
+ 'u': window.location.protocol + '//' + window.location.host,
+ };
+ var params = {
+ response_type: 'token',
+ client_id: dbuuid || '',
+ state: JSON.stringify(state),
+ scope: 'userinfo',
+ };
+ framework.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params));
+ }).fail(function(result, ev){
+ ev.preventDefault();
+ framework.redirect('https://accounts.odoo.com/web');
+ });
+ }
+ },
+ on_menu_about: function() {
+ var self = this;
+ self.rpc("/web/webclient/version_info", {}).done(function(res) {
+ var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
+ $help.find('a.oe_activate_debug_mode').click(function (e) {
+ e.preventDefault();
+ window.location = $.param.querystring( window.location.href, 'debug');
+ });
+ new Dialog(this, {
+ size: 'medium',
+ dialogClass: 'oe_act_window',
+ title: _t("About"),
+ }, $help).open();
+ });
+ },
+});
+
+return SystrayMenu;
+});
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index bd701dc7b90..2d514dc2830 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -473,7 +473,7 @@
<div class="oe-view-manager-header container-fluid">
<div class="row">
<div class="col-md-6 oe-header-title">
- <div t-if="widget.session.debug" class="oe_debug_view btn-group btn-group-sm"/>
+ <div t-if="widget.debug" class="oe_debug_view btn-group btn-group-sm"/>
<ol class="oe-view-title breadcrumb" t-if="widget.flags.display_title !== false">
</ol>
</div>
@@ -523,7 +523,7 @@
<li><a data-action="fields">View Fields</a></li>
<li><a data-action="fvg">Fields View Get</a></li>
<li><a data-action="manage_filters">Manage Filters</a></li>
- <t t-if="view_manager.session.uid === 1">
+ <t t-if="uid === 1">
<li><a data-action="translate">Technical Translation</a></li>
<li><a data-action="manage_views">Manage Views</a></li>
<li><a data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.fields_view.type)"/> View</a></li>