summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorVidhin Mehta (OpenERP) <vme@tinyerp.com>2011-09-14 11:07:46 +0530
committerVidhin Mehta (OpenERP) <vme@tinyerp.com>2011-09-14 11:07:46 +0530
commitb6076c3cc3ca5141349b99821268b8fd5e148b77 (patch)
treebb875d3ef5127cf5b8c35de6475de14e5477dbbd /addons/web/static/src/js
parent243e1bdb0079ea9f214696ead8228d464ecb2cfc (diff)
parentc7c2b5846d3775780bbaa9a84a93ad755bdb8756 (diff)
[Merge] upto revno 1024.
bzr revid: vme@tinyerp.com-20110914053746-ri6d5s341mm6tc7n
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/boot.js69
-rw-r--r--addons/web/static/src/js/chrome.js978
-rw-r--r--addons/web/static/src/js/core.js992
-rw-r--r--addons/web/static/src/js/data.js833
-rw-r--r--addons/web/static/src/js/data_export.js391
-rw-r--r--addons/web/static/src/js/dates.js138
-rw-r--r--addons/web/static/src/js/form.js2588
-rw-r--r--addons/web/static/src/js/formats.js197
-rw-r--r--addons/web/static/src/js/list-editable.js336
-rw-r--r--addons/web/static/src/js/list.js1523
-rw-r--r--addons/web/static/src/js/search.js1220
-rw-r--r--addons/web/static/src/js/view_editor.js147
-rw-r--r--addons/web/static/src/js/view_help.js15
-rw-r--r--addons/web/static/src/js/view_tree.js216
-rw-r--r--addons/web/static/src/js/views.js829
15 files changed, 10472 insertions, 0 deletions
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js
new file mode 100644
index 00000000000..38f8eaa1e7c
--- /dev/null
+++ b/addons/web/static/src/js/boot.js
@@ -0,0 +1,69 @@
+/*---------------------------------------------------------
+ * OpenERP Web Boostrap Code
+ *---------------------------------------------------------*/
+
+/**
+ * @name openerp
+ * @namespace openerp
+ */
+(function() {
+ if (this.openerp)
+ return;
+ var session_counter = 0;
+
+ var openerp = this.openerp = {
+ // Per session namespace
+ // openerp.<module> will map to
+ // openerp.sessions.sessionname.<module> using a closure
+ sessions: {},
+ /**
+ * OpenERP instance constructor
+ *
+ * @param {Boolean} skip_init if true, skips the built-in initialization
+ */
+ init: function(skip_init) {
+ var new_instance = {
+ // links to the global openerp
+ _openerp: openerp,
+ // Only web will be loaded, the rest will be by loaded by
+ // openerp.web.Connection on the first connection
+ _modules_loaded: false,
+ // this unique id will be replaced by hostname_databasename by
+ // openerp.web.Connection on the first connection
+ _session_id: "session" + session_counter++,
+ web: {},
+ web_mobile: {}
+ };
+ openerp.sessions[new_instance._session_id] = new_instance;
+ if (!skip_init){
+ openerp.web(new_instance);
+ }
+ return new_instance;
+ }
+ };
+})();
+
+/*---------------------------------------------------------
+ * OpenERP Web web module split
+ *---------------------------------------------------------*/
+
+/**
+ * @namespace
+ */
+openerp.web = function(instance) {
+ openerp.web.core(instance);
+ if (openerp.web.dates) {
+ openerp.web.dates(instance);
+ }
+ openerp.web.formats(instance);
+ openerp.web.chrome(instance);
+ openerp.web.data(instance);
+ var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export","view_editor"];
+ for(var i=0; i<files.length; i++) {
+ if(openerp.web[files[i]]) {
+ openerp.web[files[i]](instance);
+ }
+ }
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
new file mode 100644
index 00000000000..a04456cfaa5
--- /dev/null
+++ b/addons/web/static/src/js/chrome.js
@@ -0,0 +1,978 @@
+/*---------------------------------------------------------
+ * OpenERP Web chrome
+ *---------------------------------------------------------*/
+
+openerp.web.chrome = function(openerp) {
+var QWeb = openerp.web.qweb;
+
+openerp.web.Notification = openerp.web.Widget.extend(/** @lends openerp.web.Notification# */{
+ /**
+ * @constructs openerp.web.Notification
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param element_id
+ */
+ init: function(parent, element_id) {
+ this._super(parent, element_id);
+ this.$element.notify({
+ speed: 500,
+ expires: 1500
+ });
+ },
+ notify: function(title, text) {
+ this.$element.notify('create', {
+ title: title,
+ text: text
+ });
+ },
+ warn: function(title, text) {
+ this.$element.notify('create', 'oe_notification_alert', {
+ title: title,
+ text: text
+ });
+ }
+});
+
+openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# */{
+ dialog_title: "",
+ identifier_prefix: 'dialog',
+ /**
+ * @constructs openerp.web.Dialog
+ * @extends openerp.web.OldWidget
+ *
+ * @param parent
+ * @param dialog_options
+ */
+ init: function (parent, dialog_options) {
+ var self = this;
+ this._super(parent);
+ this.dialog_options = {
+ modal: true,
+ width: 'auto',
+ min_width: 0,
+ max_width: '100%',
+ height: 'auto',
+ min_height: 0,
+ max_height: '100%',
+ autoOpen: false,
+ buttons: {},
+ beforeClose: function () { self.on_close(); }
+ };
+ for (var f in this) {
+ if (f.substr(0, 10) == 'on_button_') {
+ this.dialog_options.buttons[f.substr(10)] = this[f];
+ }
+ }
+ if (dialog_options) {
+ this.set_options(dialog_options);
+ }
+ },
+ set_options: function(options) {
+ options = options || {};
+ options.width = this.get_width(options.width || this.dialog_options.width);
+ options.min_width = this.get_width(options.min_width || this.dialog_options.min_width);
+ options.max_width = this.get_width(options.max_width || this.dialog_options.max_width);
+ options.height = this.get_height(options.height || this.dialog_options.height);
+ options.min_height = this.get_height(options.min_height || this.dialog_options.min_height);
+ options.max_height = this.get_height(options.max_height || this.dialog_options.max_width);
+
+ if (options.width !== 'auto') {
+ if (options.width > options.max_width) options.width = options.max_width;
+ if (options.width < options.min_width) options.width = options.min_width;
+ }
+ if (options.height !== 'auto') {
+ if (options.height > options.max_height) options.height = options.max_height;
+ if (options.height < options.min_height) options.height = options.min_height;
+ }
+ if (!options.title && this.dialog_title) {
+ options.title = this.dialog_title;
+ }
+ _.extend(this.dialog_options, options);
+ },
+ get_width: function(val) {
+ return this.get_size(val.toString(), $(window.top).width());
+ },
+ get_height: function(val) {
+ return this.get_size(val.toString(), $(window.top).height());
+ },
+ get_size: function(val, available_size) {
+ if (val === 'auto') {
+ return val;
+ } else if (val.slice(-1) == "%") {
+ return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
+ } else {
+ return parseInt(val, 10);
+ }
+ },
+ start: function () {
+ this.$dialog = $('<div id="' + this.element_id + '"></div>').dialog(this.dialog_options);
+ this._super();
+ return this;
+ },
+ open: function(dialog_options) {
+ // TODO fme: bind window on resize
+ if (this.template) {
+ this.$element.html(this.render());
+ }
+ this.set_options(dialog_options);
+ this.$dialog.dialog(this.dialog_options).dialog('open');
+ return this;
+ },
+ close: function() {
+ // Closes the dialog but leave it in a state where it could be opened again.
+ this.$dialog.dialog('close');
+ },
+ on_close: function() {
+ },
+ stop: function () {
+ // Destroy widget
+ this.close();
+ this.$dialog.dialog('destroy');
+ }
+});
+
+openerp.web.CrashManager = openerp.web.Dialog.extend(/** @lends openerp.web.CrashManager# */{
+ identifier_prefix: 'dialog_crash',
+ /**
+ * @constructs opener.web.CrashManager
+ * @extends openerp.web.Dialog
+ *
+ * @param parent
+ */
+ init: function(parent) {
+ this._super(parent);
+ this.session.on_rpc_error.add(this.on_rpc_error);
+ },
+ on_button_Ok: function() {
+ this.close();
+ },
+ on_rpc_error: function(error) {
+ this.error = error;
+ if (error.data.fault_code) {
+ var split = ("" + error.data.fault_code).split('\n')[0].split(' -- ');
+ if (split.length > 1) {
+ error.type = split.shift();
+ error.data.fault_code = error.data.fault_code.substr(error.type.length + 4);
+ }
+ }
+ if (error.code === 200 && error.type) {
+ this.dialog_title = "OpenERP " + _.capitalize(error.type);
+ this.template = 'DialogWarning';
+ this.open({
+ width: 'auto',
+ height: 'auto'
+ });
+ } else {
+ this.dialog_title = "OpenERP Error";
+ this.template = 'DialogTraceback';
+ this.open({
+ width: 'auto',
+ height: 'auto'
+ });
+ }
+ }
+});
+
+openerp.web.Loading = openerp.web.Widget.extend(/** @lends openerp.web.Loading# */{
+ /**
+ * @constructs openerp.web.Loading
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param element_id
+ */
+ init: function(parent, element_id) {
+ this._super(parent, element_id);
+ this.count = 0;
+ this.session.on_rpc_request.add_first(this.on_rpc_event, 1);
+ this.session.on_rpc_response.add_last(this.on_rpc_event, -1);
+ },
+ on_rpc_event : function(increment) {
+ this.count += increment;
+ if (this.count) {
+ //this.$element.html(QWeb.render("Loading", {}));
+ this.$element.html("Loading ("+this.count+")");
+ this.$element.show();
+ } else {
+ this.$element.fadeOut();
+ }
+ }
+});
+
+openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database# */{
+ /**
+ * @constructs openerp.web.Database
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param element_id
+ * @param option_id
+ */
+ init: function(parent, element_id, option_id) {
+ this._super(parent, element_id);
+ this.$option_id = $('#' + option_id);
+ },
+ start: function() {
+ this.$element.html(QWeb.render("Database", this));
+ this.$element.closest(".openerp")
+ .removeClass("login-mode")
+ .addClass("database_block");
+
+ var self = this;
+
+ var fetch_db = this.rpc("/web/database/get_list", {}, function(result) {
+ self.db_list = result.db_list;
+ });
+ var fetch_langs = this.rpc("/web/session/get_lang_list", {}, function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ self.lang_list = result.lang_list;
+ });
+ $.when(fetch_db, fetch_langs).then(function () {self.do_create();});
+
+ this.$element.find('#db-create').click(this.do_create);
+ this.$element.find('#db-drop').click(this.do_drop);
+ this.$element.find('#db-backup').click(this.do_backup);
+ this.$element.find('#db-restore').click(this.do_restore);
+ this.$element.find('#db-change-password').click(this.do_change_password);
+ this.$element.find('#back-to-login').click(function() {
+ self.stop();
+ });
+ },
+ stop: function () {
+ this.$option_id.empty();
+
+ this.$element
+ .find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login')
+ .unbind('click')
+ .end()
+ .closest(".openerp")
+ .addClass("login-mode")
+ .removeClass("database_block")
+ .end()
+ .empty();
+
+ },
+ /**
+ * Converts a .serializeArray() result into a dict. Does not bother folding
+ * multiple identical keys into an array, last key wins.
+ *
+ * @param {Array} array
+ */
+ to_object: function (array) {
+ var result = {};
+ _(array).each(function (record) {
+ result[record.name] = record.value;
+ });
+ return result;
+ },
+ /**
+ * Waits until the new database is done creating, then unblocks the UI and
+ * logs the user in as admin
+ *
+ * @param {Number} db_creation_id identifier for the db-creation operation, used to fetch the current installation progress
+ * @param {Object} info info fields for this database creation
+ * @param {String} info.db name of the database being created
+ * @param {String} info.password super-admin password for the database
+ */
+ wait_for_newdb: function (db_creation_id, info) {
+ var self = this;
+ self.rpc('/web/database/progress', {
+ id: db_creation_id,
+ password: info.password
+ }, function (result) {
+ var progress = result[0];
+ // I'd display a progress bar, but turns out the progress status
+ // the server report kind-of blows goats: it's at 0 for ~75% of
+ // the installation, then jumps to 75%, then jumps down to either
+ // 0 or ~40%, then back up to 75%, then terminates. Let's keep that
+ // mess hidden behind a not-very-useful but not overly weird
+ // message instead.
+ if (progress < 1) {
+ setTimeout(function () {
+ self.wait_for_newdb(db_creation_id, info);
+ }, 500);
+ return;
+ }
+
+ var admin = result[1][0];
+ setTimeout(function () {
+ self.stop();
+ self.widget_parent.do_login(
+ info.db, admin.login, admin.password);
+ $.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 $('<div>').dialog({
+ modal: true,
+ title: error.title,
+ buttons: {
+ Ok: function() {
+ $(this).dialog("close");
+ }
+ }
+ }).html(error.error);
+ },
+ do_create: function() {
+ var self = this;
+ self.$option_id.html(QWeb.render("Database.CreateDB", self));
+ self.$option_id.find("form[name=create_db_form]").validate({
+ submitHandler: function (form) {
+ var fields = $(form).serializeArray();
+ $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
+ self.rpc("/web/database/create", {'fields': fields}, function(result) {
+ if (result.error) {
+ $.unblockUI();
+ self.display_error(result);
+ return;
+ }
+ self.db_list.push(self.to_object(fields)['db_name']);
+ self.db_list.sort();
+ var form_obj = self.to_object(fields);
+ self.wait_for_newdb(result, {
+ password: form_obj['super_admin_pwd'],
+ db: form_obj['db_name']
+ });
+ });
+ }
+ });
+ },
+ do_drop: function() {
+ var self = this;
+ self.$option_id.html(QWeb.render("DropDB", self));
+ self.$option_id.find("form[name=drop_db_form]").validate({
+ submitHandler: function (form) {
+ var $form = $(form),
+ fields = $form.serializeArray(),
+ $db_list = $form.find('select[name=drop_db]'),
+ db = $db_list.val();
+
+ if (!confirm("Do you really want to delete the database: " + db + " ?")) {
+ return;
+ }
+ self.rpc("/web/database/drop", {'fields': fields}, function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ $db_list.find(':selected').remove();
+ self.db_list.splice(_.indexOf(self.db_list, db, true), 1);
+ self.notification.notify("Dropping database", "The database '" + db + "' has been dropped");
+ });
+ }
+ });
+ },
+ do_backup: function() {
+ var self = this;
+ self.$option_id
+ .html(QWeb.render("BackupDB", self))
+ .find("form[name=backup_db_form]").validate({
+ submitHandler: function (form) {
+ $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
+ self.session.get_file({
+ form: form,
+ error: function (body) {
+ var error = body.firstChild.data.split('|');
+ self.display_error({
+ title: error[0],
+ error: error[1]
+ });
+ },
+ complete: $.unblockUI
+ });
+ }
+ });
+ },
+ do_restore: function() {
+ var self = this;
+ self.$option_id.html(QWeb.render("RestoreDB", self));
+
+ self.$option_id.find("form[name=restore_db_form]").validate({
+ submitHandler: function (form) {
+ $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
+ $(form).ajaxSubmit({
+ url: '/web/database/restore',
+ type: 'POST',
+ resetForm: true,
+ success: function (body) {
+ // TODO: ui manipulations
+ // note: response objects don't work, but we have the
+ // HTTP body of the response~~
+
+ // If empty body, everything went fine
+ if (!body) { return; }
+
+ if (body.indexOf('403 Forbidden') !== -1) {
+ self.display_error({
+ title: 'Access Denied',
+ error: 'Incorrect super-administrator password'
+ })
+ } else {
+ self.display_error({
+ title: 'Restore Database',
+ error: 'Could not restore the database'
+ })
+ }
+ },
+ complete: function () {
+ $.unblockUI();
+ }
+ });
+ }
+ });
+ },
+ do_change_password: function() {
+ var self = this;
+ self.$option_id.html(QWeb.render("Change_DB_Pwd", self));
+
+ self.$option_id.find("form[name=change_pwd_form]").validate({
+ messages: {
+ old_pwd: "Please enter your previous password",
+ new_pwd: "Please enter your new password",
+ confirm_pwd: {
+ required: "Please confirm your new password",
+ equalTo: "The confirmation does not match the password"
+ }
+ },
+ submitHandler: function (form) {
+ self.rpc("/web/database/change_password", {
+ 'fields': $(form).serializeArray()
+ }, function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ self.notification.notify("Changed Password", "Password has been changed successfully");
+ });
+ }
+ });
+ }
+});
+
+openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
+ remember_creditentials: true,
+ /**
+ * @constructs openerp.web.Login
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param element_id
+ */
+ init: function(parent, element_id) {
+ this._super(parent, element_id);
+ this.has_local_storage = typeof(localStorage) != 'undefined';
+ this.selected_db = null;
+ this.selected_login = null;
+
+ if (this.has_local_storage && this.remember_creditentials) {
+ this.selected_db = localStorage.getItem('last_db_login_success');
+ this.selected_login = localStorage.getItem('last_login_login_success');
+ if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) {
+ this.selected_password = localStorage.getItem('last_password_login_success');
+ }
+ }
+ },
+ start: function() {
+ var self = this;
+ this.rpc("/web/database/get_list", {}, function(result) {
+ self.db_list = result.db_list;
+ self.display();
+ }, function() {
+ self.display();
+ });
+ },
+ display: function() {
+ var self = this;
+
+ this.$element.html(QWeb.render("Login", this));
+ this.database = new openerp.web.Database(
+ this, "oe_database", "oe_db_options");
+
+ this.$element.find('#oe-db-config').click(function() {
+ self.database.start();
+ });
+
+ this.$element.find("form").submit(this.on_submit);
+ },
+ on_login_invalid: function() {
+ this.$element.closest(".openerp").addClass("login-mode");
+ },
+ on_login_valid: function() {
+ this.$element.closest(".openerp").removeClass("login-mode");
+ },
+ on_submit: function(ev) {
+ ev.preventDefault();
+ var $e = this.$element;
+ var db = $e.find("form [name=db]").val();
+ var login = $e.find("form input[name=login]").val();
+ var password = $e.find("form input[name=password]").val();
+
+ this.do_login(db, login, password);
+ },
+ /**
+ * Performs actual login operation, and UI-related stuff
+ *
+ * @param {String} db database to log in
+ * @param {String} login user login
+ * @param {String} password user password
+ */
+ do_login: function (db, login, password) {
+ var self = this;
+ this.session.session_login(db, login, password, function() {
+ if(self.session.session_is_valid()) {
+ if (self.has_local_storage) {
+ if(self.remember_creditentials) {
+ localStorage.setItem('last_db_login_success', db);
+ localStorage.setItem('last_login_login_success', login);
+ if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) {
+ localStorage.setItem('last_password_login_success', password);
+ }
+ } else {
+ localStorage.setItem('last_db_login_success', '');
+ localStorage.setItem('last_login_login_success', '');
+ localStorage.setItem('last_password_login_success', '');
+ }
+ }
+ self.on_login_valid();
+ } else {
+ self.$element.addClass("login_invalid");
+ self.on_login_invalid();
+ }
+ });
+ },
+ do_ask_login: function(continuation) {
+ this.on_login_invalid();
+ this.$element
+ .removeClass("login_invalid");
+ this.on_login_valid.add({
+ position: "last",
+ unique: true,
+ callback: continuation || function() {}
+ });
+ },
+ on_logout: function() {
+ this.session.logout();
+ }
+});
+
+openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# */{
+ template: "Header",
+ identifier_prefix: 'oe-app-header-',
+ /**
+ * @constructs openerp.web.Header
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ */
+ init: function(parent) {
+ this._super(parent);
+ this.qs = "?" + jQuery.param.querystring();
+ this.$content = $();
+ console.debug("initializing header with id", this.element_id);
+ this.update_promise = $.Deferred().resolve();
+ },
+ start: function() {
+ this._super();
+ },
+ do_update: function () {
+ var self = this;
+ var fct = function() {
+ self.$content.remove();
+ if (!self.session.uid)
+ return;
+ var func = new openerp.web.Model(self.session, "res.users").get_func("read");
+ return func(self.session.uid, ["name", "company_id"]).pipe(function(res) {
+ self.$content = $(QWeb.render("Header-content", {widget: self, user: res}));
+ self.$content.appendTo(self.$element);
+ self.$element.find(".logout").click(self.on_logout);
+ self.$element.find("a.preferences").click(self.on_preferences);
+ self.$element.find(".about").click(self.on_about);
+ return self.shortcut_load();
+ });
+ };
+ this.update_promise = this.update_promise.pipe(fct, fct);
+ },
+ on_about: function() {
+ var self = this;
+ self.rpc("/web/webclient/version_info", {}).then(function(res) {
+ var $help = $(QWeb.render("About-Page", {version_info: res}));
+ $help.dialog({autoOpen: true,
+ modal: true, width: 960, title: "About"});
+ });
+ },
+ shortcut_load :function(){
+ var self = this,
+ sc = self.session.shortcuts,
+ shortcuts_ds = new openerp.web.DataSet(this, 'ir.ui.view_sc');
+ // TODO: better way to communicate between sections.
+ // sc.bindings, because jquery does not bind/trigger on arrays...
+ if (!sc.binding) {
+ sc.binding = {};
+ $(sc.binding).bind({
+ 'add': function (e, attrs) {
+ shortcuts_ds.create(attrs, function (out) {
+ $('<li>', {
+ 'data-shortcut-id':out.result,
+ 'data-id': attrs.res_id
+ }).text(attrs.name)
+ .appendTo(self.$element.find('.oe-shortcuts ul'));
+ attrs.id = out.result;
+ sc.push(attrs);
+ });
+ },
+ 'remove-current': function () {
+ var menu_id = self.session.active_id;
+ var $shortcut = self.$element
+ .find('.oe-shortcuts li[data-id=' + menu_id + ']');
+ var shortcut_id = $shortcut.data('shortcut-id');
+ $shortcut.remove();
+ shortcuts_ds.unlink([shortcut_id]);
+ var sc_new = _.reject(sc, function(shortcut){ return shortcut_id === shortcut.id});
+ sc.splice(0, sc.length);
+ sc.push.apply(sc, sc_new);
+ }
+ });
+ }
+ return this.rpc('/web/session/sc_list', {}, function(shortcuts) {
+ sc.splice(0, sc.length);
+ sc.push.apply(sc, shortcuts);
+
+ self.$element.find('.oe-shortcuts')
+ .html(QWeb.render('Shortcuts', {'shortcuts': shortcuts}))
+ .undelegate('li', 'click')
+
+ .delegate('li', 'click', function(e) {
+ e.stopPropagation();
+ var id = $(this).data('id');
+ self.session.active_id = id;
+ self.rpc('/web/menu/action', {'menu_id':id}, function(ir_menu_data) {
+ if (ir_menu_data.action.length){
+ self.on_action(ir_menu_data.action[0][2]);
+ }
+ });
+ });
+ });
+ },
+
+ on_action: function(action) {
+ },
+ on_preferences: function(){
+ var self = this;
+ var action_manager = new openerp.web.ActionManager(this);
+ var dataset = new openerp.web.DataSet (this,'res.users',this.context);
+ dataset.call ('action_get','',function (result){
+ self.rpc('/web/action/load', {action_id:result}, function(result){
+ action_manager.do_action(_.extend(result['result'], {
+ res_id: self.session.uid,
+ res_model: 'res.users',
+ flags: {
+ action_buttons: false,
+ search_view: false,
+ sidebar: false,
+ views_switcher: false,
+ pager: false
+ }
+ }));
+ });
+ });
+ this.dialog = new openerp.web.Dialog(this,{
+ modal: true,
+ title: 'Preferences',
+ width: 600,
+ height: 500,
+ buttons: {
+ "Change password": function(){
+ self.change_password();
+ },
+ Cancel: function(){
+ $(this).dialog('destroy');
+ },
+ Save: function(){
+ var inner_viewmanager = action_manager.inner_viewmanager;
+ inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save(function(){
+ inner_viewmanager.start();
+ });
+ $(this).dialog('destroy')
+ }
+ }
+ });
+ this.dialog.start().open();
+ action_manager.appendTo(this.dialog);
+ action_manager.render(this.dialog);
+ },
+
+ change_password :function() {
+ var self = this;
+ this.dialog = new openerp.web.Dialog(this,{
+ modal : true,
+ title : 'Change Password',
+ width : 'auto',
+ height : 'auto'
+ });
+ this.dialog.start().open();
+ this.dialog.$element.html(QWeb.render("Change_Pwd", self));
+ this.dialog.$element.find("form[name=change_password_form]").validate({
+ submitHandler: function (form) {
+ self.rpc("/web/session/change_password",{
+ 'fields': $(form).serializeArray()
+ }, function(result) {
+ if (result.error) {
+ self.display_error(result);
+ return;
+ }
+ else {
+ if (result.new_password) {
+ self.session.password = result.new_password;
+ var session = new openerp.web.Session(self.session.server, self.session.port);
+ session.start();
+ session.session_login(self.session.db, self.session.login, self.session.password)
+ }
+ }
+ self.notification.notify("Changed Password", "Password has been changed successfully");
+ self.dialog.close();
+ });
+ }
+ });
+ },
+ display_error: function (error) {
+ return $('<div>').dialog({
+ modal: true,
+ title: error.title,
+ buttons: {
+ Ok: function() {
+ $(this).dialog("close");
+ }
+ }
+ }).html(error.error);
+ },
+ on_logout: function() {
+ }
+});
+
+openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{
+ /**
+ * @constructs openerp.web.Menu
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param element_id
+ * @param secondary_menu_id
+ */
+ init: function(parent, element_id, secondary_menu_id) {
+ this._super(parent, element_id);
+ this.secondary_menu_id = secondary_menu_id;
+ this.$secondary_menu = $("#" + secondary_menu_id).hide();
+ this.menu = false;
+ },
+ start: function() {
+ this.rpc("/web/menu/load", {}, this.on_loaded);
+ },
+ on_loaded: function(data) {
+ this.data = data;
+ this.$element.html(QWeb.render("Menu", this.data));
+ for (var i = 0; i < this.data.data.children.length; i++) {
+ var v = { menu : this.data.data.children[i] };
+ this.$secondary_menu.append(QWeb.render("Menu.secondary", v));
+ }
+ this.$secondary_menu.find("div.menu_accordion").accordion({
+ animated : false,
+ autoHeight : false,
+ icons : false
+ });
+ this.$secondary_menu.find("div.submenu_accordion").accordion({
+ animated : false,
+ autoHeight : false,
+ active: false,
+ collapsible: true,
+ header: 'h4'
+ });
+
+ this.$element.add(this.$secondary_menu).find("a").click(this.on_menu_click);
+ },
+ on_menu_click: function(ev, id) {
+ id = id || 0;
+ var $menu, $parent, $secondary;
+
+ if (id) {
+ // We can manually activate a menu with it's id (for hash url mapping)
+ $menu = this.$element.find('a[data-menu=' + id + ']');
+ if (!$menu.length) {
+ $menu = this.$secondary_menu.find('a[data-menu=' + id + ']');
+ }
+ } else {
+ $menu = $(ev.currentTarget);
+ id = $menu.data('menu');
+ }
+ if (this.$secondary_menu.has($menu).length) {
+ $secondary = $menu.parents('.menu_accordion');
+ $parent = this.$element.find('a[data-menu=' + $secondary.data('menu-parent') + ']');
+ } else {
+ $parent = $menu;
+ $secondary = this.$secondary_menu.find('.menu_accordion[data-menu-parent=' + $menu.attr('data-menu') + ']');
+ }
+
+ this.$secondary_menu.find('.menu_accordion').hide();
+ // TODO: ui-accordion : collapse submenus and expand the good one
+ $secondary.show();
+
+ if (id) {
+ this.session.active_id = id;
+ this.rpc('/web/menu/action', {'menu_id': id},
+ this.on_menu_action_loaded);
+ }
+
+ $('.active', this.$element.add(this.$secondary_menu.show())).removeClass('active');
+ $parent.addClass('active');
+ $menu.addClass('active');
+ $menu.parent('h4').addClass('active');
+
+ return !$menu.is(".leaf");
+ },
+ on_menu_action_loaded: function(data) {
+ var self = this;
+ if (data.action.length) {
+ var action = data.action[0][2];
+ self.on_action(action);
+ }
+ },
+ on_action: function(action) {
+ }
+});
+
+openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClient */{
+ /**
+ * @constructs openerp.web.WebClient
+ * @extends openerp.web.Widget
+ *
+ * @param element_id
+ */
+ init: function(element_id) {
+ this._super(null, element_id);
+ openerp.webclient = this;
+
+ QWeb.add_template("/web/static/src/xml/base.xml");
+ var params = {};
+ if(jQuery.param != undefined && jQuery.deparam(jQuery.param.querystring()).kitten != undefined) {
+ this.$element.addClass("kitten-mode-activated");
+ }
+ this.$element.html(QWeb.render("Interface", params));
+
+ this.session = new openerp.web.Session();
+ this.loading = new openerp.web.Loading(this,"oe_loading");
+ this.crashmanager = new openerp.web.CrashManager(this);
+ this.crashmanager.start();
+
+ // Do you autorize this ? will be replaced by notify() in controller
+ openerp.web.Widget.prototype.notification = new openerp.web.Notification(this, "oe_notification");
+
+
+ this.header = new openerp.web.Header(this);
+ this.login = new openerp.web.Login(this, "oe_login");
+ this.header.on_logout.add(this.login.on_logout);
+ this.header.on_action.add(this.on_menu_action);
+
+ this.session.on_session_invalid.add(this.login.do_ask_login);
+ this.session.on_session_valid.add_last(this.header.do_update);
+ this.session.on_session_invalid.add_last(this.header.do_update);
+ this.session.on_session_valid.add_last(this.on_logged);
+
+ this.menu = new openerp.web.Menu(this, "oe_menu", "oe_secondary_menu");
+ this.menu.on_action.add(this.on_menu_action);
+ },
+ start: function() {
+ this.header.appendTo($("#oe_header"));
+ this.session.start();
+ this.login.start();
+ this.menu.start();
+ console.debug("The openerp client has been initialized.");
+ },
+ on_logged: function() {
+ if(this.action_manager)
+ this.action_manager.stop();
+ this.action_manager = new openerp.web.ActionManager(this);
+ this.action_manager.appendTo($("#oe_app"));
+
+ // if using saved actions, load the action and give it to action manager
+ var parameters = jQuery.deparam(jQuery.param.querystring());
+ if (parameters["s_action"] != undefined) {
+ var key = parseInt(parameters["s_action"], 10);
+ var self = this;
+ this.rpc("/web/session/get_session_action", {key:key}, function(action) {
+ self.action_manager.do_action(action);
+ });
+ } else if (openerp._modules_loaded) { // TODO: find better option than this
+ this.load_url_state()
+ } else {
+ this.session.on_modules_loaded.add({
+ callback: $.proxy(this, 'load_url_state'),
+ unique: true,
+ position: 'last'
+ })
+ }
+ },
+ /**
+ * Loads state from URL if any, or checks if there is a home action and
+ * loads that, assuming we're at the index
+ */
+ load_url_state: function () {
+ var self = this;
+ // TODO: add actual loading if there is url state to unpack, test on window.location.hash
+
+ // not logged in
+ if (!this.session.uid) { return; }
+ var ds = new openerp.web.DataSetSearch(this, 'res.users');
+ ds.read_ids([this.session.uid], ['action_id'], function (users) {
+ var home_action = users[0].action_id;
+ if (!home_action) {
+ self.default_home();
+ return;
+ }
+ self.execute_home_action(home_action[0], ds);
+ })
+ },
+ default_home: function () {
+ },
+ /**
+ * Bundles the execution of the home action
+ *
+ * @param {Number} action action id
+ * @param {openerp.web.DataSet} dataset action executor
+ */
+ execute_home_action: function (action, dataset) {
+ var self = this;
+ this.rpc('/web/action/load', {
+ action_id: action,
+ context: dataset.get_context()
+ }, function (meh) {
+ var action = meh.result;
+ action.context = _.extend(action.context || {}, {
+ active_id: false,
+ active_ids: [false],
+ active_model: dataset.model
+ });
+ self.action_manager.do_action(action);
+ });
+ },
+ on_menu_action: function(action) {
+ this.action_manager.do_action(action);
+ },
+ do_about: function() {
+ }
+});
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js
new file mode 100644
index 00000000000..19a915c0f78
--- /dev/null
+++ b/addons/web/static/src/js/core.js
@@ -0,0 +1,992 @@
+/*---------------------------------------------------------
+ * OpenERP Web core
+ *--------------------------------------------------------*/
+
+if (!console.debug) {
+ console.debug = console.log;
+}
+
+openerp.web.core = function(openerp) {
+openerp.web.qweb = new QWeb2.Engine();
+openerp.web.qweb.debug = (window.location.search.indexOf('?debug') !== -1);
+/**
+ * John Resig Class with factory improvement
+ */
+(function() {
+ var initializing = false,
+ fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
+ // The web Class implementation (does nothing)
+ /**
+ * Extended version of John Resig's Class pattern
+ *
+ * @class
+ */
+ openerp.web.Class = function(){};
+
+ /**
+ * Subclass an existing class
+ *
+ * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
+ */
+ openerp.web.Class.extend = function(prop) {
+ var _super = this.prototype;
+
+ // Instantiate a web class (but only create the instance,
+ // don't run the init constructor)
+ initializing = true;
+ var prototype = new this();
+ initializing = false;
+
+ // Copy the properties over onto the new prototype
+ for (var name in prop) {
+ // Check if we're overwriting an existing function
+ prototype[name] = typeof prop[name] == "function" &&
+ typeof _super[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() {
+ // All construction is actually done in the init method
+ if (!initializing && this.init) {
+ var ret = this.init.apply(this, arguments);
+ if (ret) { return ret; }
+ }
+ return this;
+ }
+ Class.include = function (properties) {
+ for (var name in properties) {
+ 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 = arguments.callee;
+
+ return Class;
+ };
+})();
+
+openerp.web.callback = function(obj, method) {
+ var callback = function() {
+ var args = Array.prototype.slice.call(arguments);
+ var r;
+ for(var i = 0; i < callback.callback_chain.length; i++) {
+ var c = callback.callback_chain[i];
+ if(c.unique) {
+ callback.callback_chain.splice(i, 1);
+ i -= 1;
+ }
+ r = c.callback.apply(c.self, c.args.concat(args));
+ // TODO special value to stop the chain
+ // openerp.web.callback_stop
+ }
+ return r;
+ };
+ callback.callback_chain = [];
+ callback.add = function(f) {
+ if(typeof(f) == 'function') {
+ f = { callback: f, args: Array.prototype.slice.call(arguments, 1) };
+ }
+ f.self = f.self || null;
+ f.args = f.args || [];
+ f.unique = !!f.unique;
+ if(f.position == 'last') {
+ callback.callback_chain.push(f);
+ } else {
+ callback.callback_chain.unshift(f);
+ }
+ return callback;
+ };
+ callback.add_first = function(f) {
+ return callback.add.apply(null,arguments);
+ };
+ callback.add_last = function(f) {
+ return callback.add({
+ callback: f,
+ args: Array.prototype.slice.call(arguments, 1),
+ position: "last"
+ });
+ };
+
+ return callback.add({
+ callback: method,
+ self:obj,
+ args:Array.prototype.slice.call(arguments, 2)
+ });
+};
+
+/**
+ * Generates an inherited class that replaces all the methods by null methods (methods
+ * that does nothing and always return undefined).
+ *
+ * @param {Class} claz
+ * @param {Object} add Additional functions to override.
+ * @return {Class}
+ */
+openerp.web.generate_null_object_class = function(claz, add) {
+ var newer = {};
+ var copy_proto = function(prototype) {
+ for (var name in prototype) {
+ if(typeof prototype[name] == "function") {
+ newer[name] = function() {};
+ }
+ }
+ if (prototype.prototype)
+ copy_proto(prototype.prototype);
+ };
+ copy_proto(claz.prototype);
+ newer.init = openerp.web.Widget.prototype.init;
+ var tmpclass = claz.extend(newer);
+ return tmpclass.extend(add || {});
+};
+
+/**
+ * web error for lookup failure
+ *
+ * @class
+ */
+openerp.web.NotFound = openerp.web.Class.extend( /** @lends openerp.web.NotFound# */ {
+});
+openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.KeyNotFound# */ {
+ /**
+ * Thrown when a key could not be found in a mapping
+ *
+ * @constructs openerp.web.KeyNotFound
+ * @extends openerp.web.NotFound
+ * @param {String} key the key which could not be found
+ */
+ init: function (key) {
+ this.key = key;
+ },
+ toString: function () {
+ return "The key " + this.key + " was not found";
+ }
+});
+openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ {
+ /**
+ * Thrown when an object path does not designate a valid class or object
+ * in the openerp hierarchy.
+ *
+ * @constructs openerp.web.ObjectNotFound
+ * @extends openerp.web.NotFound
+ * @param {String} path the invalid object path
+ */
+ init: function (path) {
+ this.path = path;
+ },
+ toString: function () {
+ return "Could not find any object of path " + this.path;
+ }
+});
+openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ {
+ /**
+ * 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 openerp root to the
+ * object pointed to (e.g. ``"openerp.web.Session"`` for an OpenERP
+ * session object).
+ *
+ * @constructs openerp.web.Registry
+ * @param {Object} mapping a mapping of keys to object-paths
+ */
+ init: function (mapping) {
+ this.map = mapping || {};
+ },
+ /**
+ * Retrieves the object matching the provided key string.
+ *
+ * @param {String} key the key to fetch the object for
+ * @returns {Class} the stored class, to initialize
+ *
+ * @throws {openerp.web.KeyNotFound} if the object was not in the mapping
+ * @throws {openerp.web.ObjectNotFound} if the object path was invalid
+ */
+ get_object: function (key) {
+ var path_string = this.map[key];
+ if (path_string === undefined) {
+ throw new openerp.web.KeyNotFound(key);
+ }
+
+ var object_match = openerp;
+ 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) {
+ throw new openerp.web.ObjectNotFound(path_string);
+ }
+ }
+ return object_match;
+ },
+ /**
+ * 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
+ *
+ * @throws {openerp.web.KeyNotFound} if none of the keys was in the mapping
+ * @trows {openerp.web.ObjectNotFound} if a found object path was invalid
+ */
+ get_any: function (keys) {
+ for (var i=0; i<keys.length; ++i) {
+ try {
+ return this.get_object(keys[i]);
+ } catch (e) {
+ if (e instanceof openerp.web.KeyNotFound) {
+ continue;
+ }
+ throw e;
+ }
+ }
+ throw new openerp.web.KeyNotFound(keys.join(','));
+ },
+ /**
+ * 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 {openerp.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)
+ *
+ * @param {Object} [mapping={}] a mapping of keys to object-paths
+ */
+ clone: function (mapping) {
+ return new openerp.web.Registry(
+ _.extend({}, this.map, mapping || {}));
+ }
+});
+
+openerp.web.CallbackEnabled = openerp.web.Class.extend(/** @lends openerp.web.CallbackEnabled# */{
+ /**
+ * @constructs openerp.web.CallbackEnabled
+ * @extends openerp.web.Class
+ */
+ init: function() {
+ // Transform on_* method into openerp.web.callbacks
+ for (var name in this) {
+ if(typeof(this[name]) == "function") {
+ this[name].debug_name = name;
+ // bind ALL function to this not only on_and _do ?
+ if((/^on_|^do_/).test(name)) {
+ this[name] = openerp.web.callback(this, this[name]);
+ }
+ }
+ }
+ }
+});
+
+openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web.Session# */{
+ /**
+ * @constructs openerp.web.Session
+ * @extends openerp.web.CallbackEnabled
+ *
+ * @param {String} [server] JSON-RPC endpoint hostname
+ * @param {String} [port] JSON-RPC endpoint port
+ */
+ init: function(server, port) {
+ this._super();
+ this.server = (server == undefined) ? location.hostname : server;
+ this.port = (port == undefined) ? location.port : port;
+ this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp";
+ this.debug = (window.location.search.indexOf('?debug') !== -1);
+ this.session_id = false;
+ this.uid = false;
+ this.user_context= {};
+ this.db = false;
+ this.module_list = [];
+ this.module_loaded = {"web": true};
+ this.context = {};
+ this.shortcuts = [];
+ this.active_id = null;
+ },
+ start: function() {
+ this.session_restore();
+ },
+ /**
+ * 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 {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, success_callback, error_callback) {
+ var self = this;
+ // Construct a JSON-RPC2 request, method is currently unused
+ params.session_id = this.session_id;
+
+ // Call using the rpc_mode
+ var deferred = $.Deferred();
+ this.rpc_ajax(url, {
+ jsonrpc: "2.0",
+ method: "call",
+ params: params,
+ id: _.uniqueId('browser-client-')
+ }).then(function () {deferred.resolve.apply(deferred, arguments);},
+ function(error) {deferred.reject(error, $.Event());});
+ return deferred.fail(function() {
+ deferred.fail(function(error, event) {
+ if (!event.isDefaultPrevented()) {
+ self.on_rpc_error(error, event);
+ }
+ });
+ }).then(success_callback, error_callback).promise();
+ },
+ /**
+ * Raw JSON-RPC call
+ *
+ * @returns {jQuery.Deferred} ajax-webd deferred object
+ */
+ rpc_ajax: function(url, payload) {
+ var self = this;
+ this.on_rpc_request();
+ // url can be an $.ajax option object
+ if (_.isString(url)) {
+ url = {
+ url: url
+ }
+ }
+ var ajax = _.extend({
+ type: "POST",
+ url: url,
+ dataType: 'json',
+ contentType: 'application/json',
+ data: JSON.stringify(payload),
+ processData: false
+ }, url);
+ var deferred = $.Deferred();
+ $.ajax(ajax).done(function(response, textStatus, jqXHR) {
+ self.on_rpc_response();
+ if (!response.error) {
+ deferred.resolve(response["result"], textStatus, jqXHR);
+ return;
+ }
+ if (response.error.data.type !== "session_invalid") {
+ deferred.reject(response.error);
+ return;
+ }
+ self.uid = false;
+ self.on_session_invalid(function() {
+ self.rpc(url, payload.params,
+ function() {
+ deferred.resolve.apply(deferred, arguments);
+ },
+ function(error, event) {
+ event.preventDefault();
+ deferred.reject.apply(deferred, arguments);
+ });
+ });
+ }).fail(function(jqXHR, textStatus, errorThrown) {
+ self.on_rpc_response();
+ var error = {
+ code: -32098,
+ message: "XmlHttpRequestError " + errorThrown,
+ data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] }
+ };
+ deferred.reject(error);
+ });
+ return deferred.promise();
+ },
+ on_rpc_request: function() {
+ },
+ on_rpc_response: function() {
+ },
+ on_rpc_error: function(error) {
+ },
+ /**
+ * The session is validated either by login or by restoration of a previous session
+ */
+ on_session_valid: function() {
+ if(!openerp._modules_loaded)
+ this.load_modules();
+ },
+ on_session_invalid: function(contination) {
+ },
+ session_is_valid: function() {
+ return this.uid;
+ },
+ session_login: function(db, login, password, success_callback) {
+ var self = this;
+ var params = { db: db, login: login, password: password };
+ this.rpc("/web/session/login", params, function(result) {
+ self.session_id = result.session_id;
+ self.uid = result.uid;
+ self.user_context = result.context;
+ self.db = result.db;
+ self.session_save();
+ self.on_session_valid();
+ return true;
+ }).then(success_callback);
+ },
+ /**
+ * Reloads uid and session_id from local storage, if they exist
+ */
+ session_restore: function () {
+ var self = this;
+ this.session_id = this.get_cookie('session_id');
+ return this.rpc("/web/session/get_session_info", {}).then(function(result) {
+ self.uid = result.uid;
+ self.user_context = result.context;
+ self.db = result.db;
+ if (self.uid)
+ self.on_session_valid();
+ else
+ self.on_session_invalid();
+ });
+ },
+ /**
+ * Saves the session id and uid locally
+ */
+ session_save: function () {
+ this.set_cookie('session_id', this.session_id);
+ },
+ logout: function() {
+ this.set_cookie('session_id', '');
+ this.reload_client();
+ },
+ reload_client: function() {
+ window.location.reload();
+ },
+ /**
+ * Fetches a cookie stored by an openerp session
+ *
+ * @private
+ * @param name the cookie's name
+ */
+ get_cookie: function (name) {
+ var nameEQ = this.element_id + '|' + 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) {
+ return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
+ }
+ }
+ 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) {
+ ttl = ttl || 24*60*60*365;
+ document.cookie = [
+ this.element_id + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
+ '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;
+ this.rpc('/web/session/modules', {}, function(result) {
+ self.module_list = result;
+ var lang = self.user_context.lang;
+ var params = { mods: ["web"].concat(result), lang: lang};
+ self.rpc('/web/webclient/translations',params).then(function(transs) {
+ openerp.web._t.database.set_bundle(transs);
+ var modules = self.module_list.join(',');
+ var file_list = ["/web/static/lib/datejs/globalization/" +
+ self.user_context.lang.replace("_", "-") + ".js"
+ ];
+ if(self.debug) {
+ self.rpc('/web/webclient/csslist', {"mods": modules}, self.do_load_css);
+ self.rpc('/web/webclient/jslist', {"mods": modules}, function(files) {
+ self.do_load_js(file_list.concat(files));
+ });
+ } else {
+ self.do_load_css(["/web/webclient/css?mods="+modules]);
+ self.do_load_js(file_list.concat(["/web/webclient/js?mods="+modules]));
+ }
+ openerp._modules_loaded = true;
+ });
+ });
+ },
+ do_load_css: function (files) {
+ var self = this;
+ _.each(files, function (file) {
+ $('head').append($('<link>', {
+ 'href': file + (self.debug ? '?debug=' + (new Date().getTime()) : ''),
+ 'rel': 'stylesheet',
+ 'type': 'text/css'
+ }));
+ });
+ },
+ do_load_js: function(files) {
+ var self = this;
+ if(files.length != 0) {
+ var file = files.shift();
+ var tag = document.createElement('script');
+ tag.type = 'text/javascript';
+ tag.src = file + (this.debug ? '?debug=' + (new Date().getTime()) : '');
+ tag.onload = tag.onreadystatechange = function() {
+ if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
+ return;
+ tag.onload_done = true;
+ self.do_load_js(files);
+ };
+ $('head').append(tag);
+ self.do_load_js(files);
+ } else {
+ this.on_modules_loaded();
+ }
+ },
+ 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;
+ openerp[mod] = {};
+ // init module mod
+ if(openerp._openerp[mod] != undefined) {
+ openerp._openerp[mod](openerp);
+ 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;
+
+ 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 () {
+ if (options.error) { options.error(this.contentDocument.body); }
+ complete();
+ });
+
+ if (options.form) {
+ $form = $(options.form);
+ } else {
+ remove_form = true;
+ $form = $('<form>', {
+ action: options.url,
+ method: 'POST'
+ }).appendTo(document.body);
+ }
+
+ _(_.extend({}, options.data || {},
+ {session_id: this.session_id, token: token}))
+ .each(function (value, key) {
+ $('<input type="hidden" name="' + key + '">')
+ .val(value)
+ .appendTo($form_data);
+ });
+
+ $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 = _.sprintf("%s=;expires=%s;path=/",
+ cookie_name, new Date().toGMTString());
+ if (options.success) { options.success(); }
+ complete();
+ return;
+ }
+ };
+ timer = setTimeout(waitLoop, CHECK_INTERVAL);
+ }
+});
+
+openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp.web.SessionAware# */{
+ /**
+ * Utility class that any class is allowed to extend to easy common manipulations.
+ *
+ * It provides rpc calls, callback on all methods preceded by "on_" or "do_" and a
+ * logging facility.
+ *
+ * @constructs openerp.web.SessionAware
+ * @extends openerp.web.CallbackEnabled
+ *
+ * @param {openerp.web.Session} session
+ */
+ init: function(session) {
+ this._super();
+ this.session = session;
+ },
+ /**
+ * Performs a JSON-RPC call
+ *
+ * @param {String} url endpoint url
+ * @param {Object} data RPC parameters
+ * @param {Function} success RPC call success callback
+ * @param {Function} error RPC call error callback
+ * @returns {jQuery.Deferred} deferred object for the RPC call
+ */
+ rpc: function(url, data, success, error) {
+ return this.session.rpc(url, data, success, error);
+ }
+});
+
+openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widget# */{
+ /**
+ * 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,
+ /**
+ * The prefix used to generate an id automatically. Should be redefined in
+ * subclasses. If it is not defined, a generic identifier will be used.
+ *
+ * @type string
+ */
+ identifier_prefix: 'generic-identifier-',
+ /**
+ * Construct the widget and set its parent if a parent is given.
+ *
+ * @constructs openerp.web.Widget
+ * @extends openerp.web.SessionAware
+ *
+ * @param {openerp.web.Widget} parent Binds the current instance to the given Widget instance.
+ * When that widget is destroyed by calling stop(), the current instance will be
+ * destroyed too. Can be null.
+ * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
+ * to bind the current Widget to an already existing part of the DOM, which is not compatible
+ * with the DOM insertion methods provided by the current implementation of Widget. So
+ * for new components this argument should not be provided any more.
+ */
+ init: function(parent, /** @deprecated */ element_id) {
+ this._super((parent || {}).session);
+ // if given an element_id, try to get the associated DOM element and save
+ // a reference in this.$element. Else just generate a unique identifier.
+ this.element_id = element_id;
+ this.element_id = this.element_id || _.uniqueId(this.identifier_prefix);
+ var tmp = document.getElementById(this.element_id);
+ this.$element = tmp ? $(tmp) : undefined;
+
+ this.widget_parent = parent;
+ this.widget_children = [];
+ if(parent && parent.widget_children) {
+ parent.widget_children.push(this);
+ }
+ // useful to know if the widget was destroyed and should not be used anymore
+ this.widget_is_stopped = false;
+ },
+ /**
+ * Render 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._render_and_insert(function(t) {
+ self.$element.appendTo(t);
+ }, target);
+ },
+ /**
+ * Render 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._render_and_insert(function(t) {
+ self.$element.prependTo(t);
+ }, target);
+ },
+ /**
+ * Render 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._render_and_insert(function(t) {
+ self.$element.insertAfter(t);
+ }, target);
+ },
+ /**
+ * Render 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._render_and_insert(function(t) {
+ self.$element.insertBefore(t);
+ }, target);
+ },
+ _render_and_insert: function(insertion, target) {
+ var rendered = this.render();
+ this.$element = $(rendered);
+ if (target instanceof openerp.web.Widget)
+ target = target.$element;
+ insertion(target);
+ this.on_inserted(this.$element, this);
+ return this.start();
+ },
+ on_inserted: function(element, widget) {},
+ /**
+ * Renders the widget using QWeb, `this.template` must be defined.
+ * The context given to QWeb contains the "widget" key that references `this`.
+ *
+ * @param {Object} additional Additional context arguments to pass to the template.
+ */
+ render: function (additional) {
+ return openerp.web.qweb.render(this.template, _.extend({widget: this}, additional || {}));
+ },
+ /**
+ * Method called after rendering. Mostly used to bind actions, perform asynchronous
+ * calls, etc...
+ *
+ * By convention, the method should return a promise to inform the caller when
+ * this widget has been initialized.
+ *
+ * @returns {jQuery.Deferred}
+ */
+ start: function() {
+ if (!this.$element) {
+ var tmp = document.getElementById(this.element_id);
+ this.$element = tmp ? $(tmp) : undefined;
+ }
+ return $.Deferred().done().promise();
+ },
+ /**
+ * Destroys the current widget, also destory all its children before destroying itself.
+ */
+ stop: function() {
+ _.each(_.clone(this.widget_children), function(el) {
+ el.stop();
+ });
+ if(this.$element != null) {
+ this.$element.remove();
+ }
+ if (this.widget_parent && this.widget_parent.widget_children) {
+ this.widget_parent.widget_children = _.without(this.widget_parent.widget_children, this);
+ }
+ this.widget_parent = null;
+ this.widget_is_stopped = true;
+ },
+ /**
+ * Inform the action manager to do an action. Of course, this suppose 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(action, on_finished) {
+ if (this.widget_parent) {
+ return this.widget_parent.do_action(action, on_finished);
+ }
+ return false;
+ },
+ do_notify: function() {
+ if (this.widget_parent) {
+ return this.widget_parent.do_notify.apply(this,arguments);
+ }
+ return false;
+ },
+ do_warn: function() {
+ if (this.widget_parent) {
+ return this.widget_parent.do_warn.apply(this,arguments);
+ }
+ return false;
+ },
+ rpc: function(url, data, success, error) {
+ var def = $.Deferred().then(success, error);
+ var self = this;
+ this._super(url, data). then(function() {
+ if (!self.widget_is_stopped)
+ def.resolve.apply(def, arguments);
+ }, function() {
+ if (!self.widget_is_stopped)
+ def.reject.apply(def, arguments);
+ });
+ return def.promise();
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.Widget
+ * @deprecated
+ * For retro compatibility only, the only difference with is that render() uses
+ * directly ``this`` instead of context with a ``widget`` key.
+ */
+openerp.web.OldWidget = openerp.web.Widget.extend(/** @lends openerp.web.OldWidget# */{
+ render: function (additional) {
+ return openerp.web.qweb.render(this.template, _.extend(_.extend({}, this), additional || {}));
+ }
+});
+
+openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.web.TranslationDataBase# */{
+ /**
+ * @constructs openerp.web.TranslationDataBase
+ * @extends openerp.web.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).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) {
+ if (self.db[message.id] === undefined) {
+ 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) {
+ if (this.db[key])
+ return this.db[key];
+ return undefined;
+ }
+});
+
+openerp.web._t = new openerp.web.TranslationDataBase().build_translation_function();
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js
new file mode 100644
index 00000000000..1eb57241fa6
--- /dev/null
+++ b/addons/web/static/src/js/data.js
@@ -0,0 +1,833 @@
+
+openerp.web.data = function(openerp) {
+
+/**
+ * Serializes the sort criterion array of a dataset into a form which can be
+ * consumed by OpenERP's RPC APIs.
+ *
+ * @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
+ */
+openerp.web.serialize_sort = function (criterion) {
+ return _.map(criterion,
+ function (criteria) {
+ if (criteria[0] === '-') {
+ return criteria.slice(1) + ' DESC';
+ }
+ return criteria + ' ASC';
+ }).join(', ');
+};
+
+openerp.web.DataGroup = openerp.web.Widget.extend( /** @lends openerp.web.DataGroup# */{
+ /**
+ * Management interface between views and grouped collections of OpenERP
+ * records.
+ *
+ * The root DataGroup is instantiated with the relevant information
+ * (a session, a model, a domain, a context and a group_by sequence), the
+ * domain and context may be empty. It is then interacted with via
+ * :js:func:`~openerp.web.DataGroup.list`, which is used to read the
+ * content of the current grouping level.
+ *
+ * @constructs openerp.web.DataGroup
+ * @extends openerp.web.Widget
+ *
+ * @param {openerp.web.Session} session Current OpenERP session
+ * @param {String} model name of the model managed by this DataGroup
+ * @param {Array} domain search domain for this DataGroup
+ * @param {Object} context context of the DataGroup's searches
+ * @param {Array} group_by sequence of fields by which to group
+ * @param {Number} [level=0] nesting level of the group
+ */
+ init: function(parent, model, domain, context, group_by, level) {
+ this._super(parent, null);
+ if (group_by) {
+ if (group_by.length || context['group_by_no_leaf']) {
+ return new openerp.web.ContainerDataGroup( this, model, domain, context, group_by, level);
+ } else {
+ return new openerp.web.GrouplessDataGroup( this, model, domain, context, level);
+ }
+ }
+
+ this.model = model;
+ this.context = context;
+ this.domain = domain;
+
+ this.level = level || 0;
+ },
+ cls: 'DataGroup'
+});
+openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.ContainerDataGroup# */ {
+ /**
+ *
+ * @constructs openerp.web.ContainerDataGroup
+ * @extends openerp.web.DataGroup
+ *
+ * @param session
+ * @param model
+ * @param domain
+ * @param context
+ * @param group_by
+ * @param level
+ */
+ init: function (parent, model, domain, context, group_by, level) {
+ this._super(parent, model, domain, context, null, level);
+
+ this.group_by = group_by;
+ },
+ /**
+ * The format returned by ``read_group`` is absolutely dreadful:
+ *
+ * * A ``__context`` key provides future grouping levels
+ * * A ``__domain`` key provides the domain for the next search
+ * * The current grouping value is provided through the name of the
+ * current grouping name e.g. if currently grouping on ``user_id``, then
+ * the ``user_id`` value for this group will be provided through the
+ * ``user_id`` key.
+ * * Similarly, the number of items in the group (not necessarily direct)
+ * is provided via ``${current_field}_count``
+ * * Other aggregate fields are just dumped there
+ *
+ * This function slightly improves the grouping records by:
+ *
+ * * Adding a ``grouped_on`` property providing the current grouping field
+ * * Adding a ``value`` and a ``length`` properties which replace the
+ * ``$current_field`` and ``${current_field}_count`` ones
+ * * Moving aggregate values into an ``aggregates`` property object
+ *
+ * Context and domain keys remain as-is, they should not be used externally
+ * but in case they're needed...
+ *
+ * @param {Object} group ``read_group`` record
+ */
+ transform_group: function (group) {
+ var field_name = this.group_by[0];
+ // In cases where group_by_no_leaf and no group_by, the result of
+ // read_group has aggregate fields but no __context or __domain.
+ // Create default (empty) values for those so that things don't break
+ var fixed_group = _.extend(
+ {__context: {group_by: []}, __domain: []},
+ group);
+
+ var aggregates = {};
+ _(fixed_group).each(function (value, key) {
+ if (key.indexOf('__') === 0
+ || key === field_name
+ || key === field_name + '_count') {
+ return;
+ }
+ aggregates[key] = value || 0;
+ });
+
+ return {
+ __context: fixed_group.__context,
+ __domain: fixed_group.__domain,
+
+ grouped_on: field_name,
+ // if terminal group (or no group) and group_by_no_leaf => use group.__count
+ length: fixed_group[field_name + '_count'] || fixed_group.__count,
+ value: fixed_group[field_name],
+
+ openable: !(this.context['group_by_no_leaf']
+ && fixed_group.__context.group_by.length === 0),
+
+ aggregates: aggregates
+ };
+ },
+ fetch: function (fields) {
+ // internal method
+ var d = new $.Deferred();
+ var self = this;
+
+ this.rpc('/web/group/read', {
+ model: this.model,
+ context: this.context,
+ domain: this.domain,
+ fields: _.uniq(this.group_by.concat(fields)),
+ group_by_fields: this.group_by,
+ sort: openerp.web.serialize_sort(this.sort)
+ }, function () { }).then(function (response) {
+ var data_groups = _(response).map(
+ _.bind(self.transform_group, self));
+ self.groups = data_groups;
+ d.resolveWith(self, [data_groups]);
+ }, function () {
+ d.rejectWith.apply(d, [self, arguments]);
+ });
+ return d.promise();
+ },
+ /**
+ * The items of a list have the following properties:
+ *
+ * ``length``
+ * the number of records contained in the group (and all of its
+ * sub-groups). This does *not* provide the size of the "next level"
+ * of the group, unless the group is terminal (no more groups within
+ * it).
+ * ``grouped_on``
+ * the name of the field this level was grouped on, this is mostly
+ * used for display purposes, in order to know the name of the current
+ * level of grouping. The ``grouped_on`` should be the same for all
+ * objects of the list.
+ * ``value``
+ * the value which led to this group (this is the value all contained
+ * records have for the current ``grouped_on`` field name).
+ * ``aggregates``
+ * a mapping of other aggregation fields provided by ``read_group``
+ *
+ * @param {Array} fields the list of fields to aggregate in each group, can be empty
+ * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters.
+ * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter
+ */
+ list: function (fields, ifGroups, ifRecords) {
+ var self = this;
+ this.fetch(fields).then(function (group_records) {
+ ifGroups(_(group_records).map(function (group) {
+ var child_context = _.extend({}, self.context, group.__context);
+ return _.extend(
+ new openerp.web.DataGroup(
+ self, self.model, group.__domain,
+ child_context, child_context.group_by,
+ self.level + 1),
+ group, {sort: self.sort});
+ }));
+ });
+ }
+});
+openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.GrouplessDataGroup# */ {
+ /**
+ *
+ * @constructs openerp.web.GrouplessDataGroup
+ * @extends openerp.web.DataGroup
+ *
+ * @param session
+ * @param model
+ * @param domain
+ * @param context
+ * @param level
+ */
+ init: function (parent, model, domain, context, level) {
+ this._super(parent, model, domain, context, null, level);
+ },
+ list: function (fields, ifGroups, ifRecords) {
+ ifRecords(_.extend(
+ new openerp.web.DataSetSearch(this, this.model),
+ {domain: this.domain, context: this.context, _sort: this.sort}));
+ }
+});
+openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ {
+ /**
+ * A specialization of groupless data groups, relying on a single static
+ * dataset as its records provider.
+ *
+ * @constructs openerp.web.StaticDataGroup
+ * @extends openerp.web.GrouplessDataGroup
+ * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups
+ */
+ init: function (dataset) {
+ this.dataset = dataset;
+ },
+ list: function (fields, ifGroups, ifRecords) {
+ ifRecords(this.dataset);
+ }
+});
+
+openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet# */{
+ /**
+ * DateaManagement interface between views and the collection of selected
+ * OpenERP records (represents the view's state?)
+ *
+ * @constructs openerp.web.DataSet
+ * @extends openerp.web.Widget
+ *
+ * @param {String} model the OpenERP model this dataset will manage
+ */
+ init: function(source_controller, model, context) {
+ // we don't want the dataset to be a child of anything!
+ this._super(null);
+ this.session = source_controller ? source_controller.session : undefined;
+ this.model = model;
+ this.context = context || {};
+ this.index = null;
+ },
+ start: function() {
+ },
+ previous: function () {
+ this.index -= 1;
+ if (this.index < 0) {
+ this.index = this.ids.length - 1;
+ }
+ return this;
+ },
+ next: function () {
+ this.index += 1;
+ if (this.index >= this.ids.length) {
+ this.index = 0;
+ }
+ return this;
+ },
+ /**
+ * Read records.
+ *
+ * @param {Array} ids identifiers of the records to read
+ * @param {Array} fields fields to read and return, by default all fields are returned
+ * @param {Function} callback function called with read result
+ * @returns {$.Deferred}
+ */
+ read_ids: function (ids, fields, callback) {
+ return this.rpc('/web/dataset/get', {
+ model: this.model,
+ ids: ids,
+ fields: fields,
+ context: this.get_context()
+ }, callback);
+ },
+ /**
+ * Read a slice of the records represented by this DataSet, based on its
+ * domain and context.
+ *
+ * @param {Array} [fields] fields to read and return, by default all fields are returned
+ * @params {Object} options
+ * @param {Number} [options.offset=0] The index from which selected records should be returned
+ * @param {Number} [options.limit=null] The maximum number of records to return
+ * @param {Function} callback function called with read_slice result
+ * @returns {$.Deferred}
+ */
+ read_slice: function (fields, options, callback) {
+ return null;
+ },
+ /**
+ * Reads the current dataset record (from its index)
+ *
+ * @params {Array} [fields] fields to read and return, by default all fields are returned
+ * @params {Function} callback function called with read_index result
+ * @returns {$.Deferred}
+ */
+ read_index: function (fields, callback) {
+ var def = $.Deferred().then(callback);
+ if (_.isEmpty(this.ids)) {
+ def.reject();
+ } else {
+ fields = fields || false;
+ return this.read_ids([this.ids[this.index]], fields).then(function(records) {
+ def.resolve(records[0]);
+ }, function() {
+ def.reject.apply(def, arguments);
+ });
+ }
+ return def.promise();
+ },
+ /**
+ * Reads default values for the current model
+ *
+ * @param {Array} [fields] fields to get default values for, by default all defaults are read
+ * @param {Function} callback function called with default_get result
+ * @returns {$.Deferred}
+ */
+ default_get: function(fields, callback) {
+ return this.rpc('/web/dataset/default_get', {
+ model: this.model,
+ fields: fields,
+ context: this.get_context()
+ }, callback);
+ },
+ /**
+ * Creates a new record in db
+ *
+ * @param {Object} data field values to set on the new record
+ * @param {Function} callback function called with operation result
+ * @param {Function} error_callback function called in case of creation error
+ * @returns {$.Deferred}
+ */
+ create: function(data, callback, error_callback) {
+ return this.rpc('/web/dataset/create', {
+ model: this.model,
+ data: data,
+ context: this.get_context()
+ }, callback, error_callback);
+ },
+ /**
+ * Saves the provided data in an existing db record
+ *
+ * @param {Number|String} id identifier for the record to alter
+ * @param {Object} data field values to write into the record
+ * @param {Function} callback function called with operation result
+ * @param {Function} error_callback function called in case of write error
+ * @returns {$.Deferred}
+ */
+ write: function (id, data, options, callback, error_callback) {
+ var options = options || {};
+ return this.rpc('/web/dataset/save', {
+ model: this.model,
+ id: id,
+ data: data,
+ context: this.get_context(options.context)
+ }, callback, error_callback);
+ },
+ /**
+ * Deletes an existing record from the database
+ *
+ * @param {Number|String} ids identifier of the record to delete
+ * @param {Function} callback function called with operation result
+ * @param {Function} error_callback function called in case of deletion error
+ */
+ unlink: function(ids, callback, error_callback) {
+ var self = this;
+ return this.call_and_eval("unlink", [ids, this.get_context()], null, 1,
+ callback, error_callback);
+ },
+ /**
+ * Calls an arbitrary RPC method
+ *
+ * @param {String} method name of the method (on the current model) to call
+ * @param {Array} [args] arguments to pass to the method
+ * @param {Function} callback
+ * @param {Function} error_callback
+ * @returns {$.Deferred}
+ */
+ call: function (method, args, callback, error_callback) {
+ return this.rpc('/web/dataset/call', {
+ model: this.model,
+ method: method,
+ args: args || []
+ }, callback, error_callback);
+ },
+ /**
+ * Calls an arbitrary method, with more crazy
+ *
+ * @param {String} method
+ * @param {Array} [args]
+ * @param {Number} [domain_index] index of a domain to evaluate in the args array
+ * @param {Number} [context_index] index of a context to evaluate in the args array
+ * @param {Function} callback
+ * @param {Function }error_callback
+ * @returns {$.Deferred}
+ */
+ call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) {
+ return this.rpc('/web/dataset/call', {
+ model: this.model,
+ method: method,
+ domain_id: domain_index || null,
+ context_id: context_index || null,
+ args: args || []
+ }, callback, error_callback);
+ },
+ /**
+ * Calls a button method, usually returning some sort of action
+ *
+ * @param {String} method
+ * @param {Array} [args]
+ * @param {Function} callback
+ * @param {Function} error_callback
+ * @returns {$.Deferred}
+ */
+ call_button: function (method, args, callback, error_callback) {
+ return this.rpc('/web/dataset/call_button', {
+ model: this.model,
+ method: method,
+ domain_id: null,
+ context_id: 1,
+ args: args || []
+ }, callback, error_callback);
+ },
+ /**
+ * Fetches the "readable name" for records, based on intrinsic rules
+ *
+ * @param {Array} ids
+ * @param {Function} callback
+ * @returns {$.Deferred}
+ */
+ name_get: function(ids, callback) {
+ return this.call_and_eval('name_get', [ids, this.get_context()], null, 1, callback);
+ },
+ /**
+ *
+ * @param {String} name name to perform a search for/on
+ * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
+ * @param {String} [operator='ilike'] matching operator to use with the provided name value
+ * @param {Number} [limit=100] maximum number of matches to return
+ * @param {Function} callback function to call with name_search result
+ * @returns {$.Deferred}
+ */
+ name_search: function (name, domain, operator, limit, callback) {
+ return this.call_and_eval('name_search',
+ [name || '', domain || false, operator || 'ilike', this.get_context(), limit || 100],
+ 1, 3, callback);
+ },
+ /**
+ * @param name
+ * @param callback
+ */
+ name_create: function(name, callback) {
+ return this.call_and_eval('name_create', [name, this.get_context()], null, 1, callback);
+ },
+ exec_workflow: function (id, signal, callback) {
+ return this.rpc('/web/dataset/exec_workflow', {
+ model: this.model,
+ id: id,
+ signal: signal
+ }, callback);
+ },
+ get_context: function(request_context) {
+ if (request_context) {
+ return new openerp.web.CompoundContext(this.context, request_context);
+ }
+ return this.context;
+ }
+});
+openerp.web.DataSetStatic = openerp.web.DataSet.extend({
+ init: function(parent, model, context, ids) {
+ this._super(parent, model, context);
+ // all local records
+ this.ids = ids || [];
+ },
+ read_slice: function (fields, options, callback) {
+ // TODO remove fields from options
+ var self = this,
+ offset = options.offset || 0,
+ limit = options.limit || false,
+ fields = fields || false;
+ var end_pos = limit && limit !== -1 ? offset + limit : undefined;
+ return this.read_ids(this.ids.slice(offset, end_pos), fields, callback);
+ },
+ set_ids: function (ids) {
+ this.ids = ids;
+ if (this.index !== null) {
+ this.index = this.index <= this.ids.length - 1 ?
+ this.index : (this.ids.length > 0 ? this.length - 1 : 0);
+ }
+ },
+ unlink: function(ids) {
+ this.on_unlink(ids);
+ return $.Deferred().resolve({result: true});
+ },
+ on_unlink: function(ids) {
+ this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
+ }
+});
+openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{
+ /**
+ * @constructs openerp.web.DataSetSearch
+ * @extends openerp.web.DataSet
+ *
+ * @param {Object} parent
+ * @param {String} model
+ * @param {Object} context
+ * @param {Array} domain
+ */
+ init: function(parent, model, context, domain) {
+ this._super(parent, model, context);
+ this.domain = domain || [];
+ this._sort = [];
+ this.offset = 0;
+ // subset records[offset:offset+limit]
+ // is it necessary ?
+ this.ids = [];
+ },
+ /**
+ * Read a slice of the records represented by this DataSet, based on its
+ * domain and context.
+ *
+ * @params {Object} options
+ * @param {Array} [options.fields] fields to read and return, by default all fields are returned
+ * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
+ * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
+ * @param {Number} [options.offset=0] The index from which selected records should be returned
+ * @param {Number} [options.limit=null] The maximum number of records to return
+ * @param {Function} callback function called with read_slice result
+ * @returns {$.Deferred}
+ */
+ read_slice: function (fields, options, callback) {
+ var self = this;
+ var options = options || {};
+ var offset = options.offset || 0;
+ return this.rpc('/web/dataset/search_read', {
+ model: this.model,
+ fields: fields || false,
+ domain: this.get_domain(options.domain),
+ context: this.get_context(options.context),
+ sort: this.sort(),
+ offset: offset,
+ limit: options.limit || false
+ }, function (result) {
+ self.ids = result.ids;
+ self.offset = offset;
+ if (callback) {
+ callback(result.records);
+ }
+ });
+ },
+ get_domain: function (other_domain) {
+ if (other_domain) {
+ return new openerp.web.CompoundDomain(this.domain, other_domain);
+ }
+ return this.domain;
+ },
+ /**
+ * Reads or changes sort criteria on the dataset.
+ *
+ * If not provided with any argument, serializes the sort criteria to
+ * an SQL-like form usable by OpenERP's ORM.
+ *
+ * If given a field, will set that field as first sorting criteria or,
+ * if the field is already the first sorting criteria, will reverse it.
+ *
+ * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
+ * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
+ * @returns {String|undefined}
+ */
+ sort: function (field, force_reverse) {
+ if (!field) {
+ return openerp.web.serialize_sort(this._sort);
+ }
+ var reverse = force_reverse || (this._sort[0] === field);
+ this._sort.splice.apply(
+ this._sort, [0, this._sort.length].concat(
+ _.without(this._sort, field, '-' + field)));
+
+ this._sort.unshift((reverse ? '-' : '') + field);
+ return undefined;
+ },
+ unlink: function(ids, callback, error_callback) {
+ var self = this;
+ return this._super(ids, function(result) {
+ self.ids = _.without.apply(_, [self.ids].concat(ids));
+ if (this.index !== null) {
+ self.index = self.index <= self.ids.length - 1 ?
+ self.index : (self.ids.length > 0 ? self.ids.length -1 : 0);
+ }
+ if (callback)
+ callback(result);
+ }, error_callback);
+ }
+});
+openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
+ virtual_id_prefix: "one2many_v_id_",
+ virtual_id_regex: /one2many_v_id_.*/,
+ debug_mode: true,
+ init: function() {
+ this._super.apply(this, arguments);
+ this.reset_ids([]);
+ },
+ create: function(data, callback, error_callback) {
+ var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data};
+ this.to_create.push(cached);
+ this.cache.push(cached);
+ this.on_change();
+ var to_return = $.Deferred().then(callback);
+ to_return.resolve({result: cached.id});
+ return to_return.promise();
+ },
+ write: function (id, data, options, callback) {
+ var self = this;
+ var record = _.detect(this.to_create, function(x) {return x.id === id;});
+ record = record || _.detect(this.to_write, function(x) {return x.id === id;});
+ var dirty = false;
+ if (record) {
+ for (var k in data) {
+ if (record.values[k] === undefined || record.values[k] !== data[k]) {
+ dirty = true;
+ break;
+ }
+ }
+ $.extend(record.values, data);
+ } else {
+ dirty = true;
+ record = {id: id, values: data};
+ self.to_write.push(record);
+ }
+ var cached = _.detect(this.cache, function(x) {return x.id === id;});
+ $.extend(cached.values, record.values);
+ if (dirty)
+ this.on_change();
+ var to_return = $.Deferred().then(callback);
+ to_return.resolve({result: true});
+ return to_return.promise();
+ },
+ unlink: function(ids, callback, error_callback) {
+ var self = this;
+ _.each(ids, function(id) {
+ if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
+ self.to_delete.push({id: id})
+ }
+ });
+ this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
+ this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
+ this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
+ this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
+ this.on_change();
+ var to_return = $.Deferred().then(callback);
+ setTimeout(function () {to_return.resolve({result: true});}, 0);
+ return to_return.promise();
+ },
+ reset_ids: function(ids) {
+ this.set_ids(ids);
+ this.to_delete = [];
+ this.to_create = [];
+ this.to_write = [];
+ this.cache = [];
+ this.delete_all = false;
+ },
+ on_change: function() {},
+ read_ids: function (ids, fields, callback) {
+ var self = this;
+ var to_get = [];
+ _.each(ids, function(id) {
+ var cached = _.detect(self.cache, function(x) {return x.id === id;});
+ var created = _.detect(self.to_create, function(x) {return x.id === id;});
+ if (created) {
+ _.each(fields, function(x) {if (cached.values[x] === undefined) cached.values[x] = false;});
+ } else {
+ if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
+ to_get.push(id);
+ }
+ });
+ var completion = $.Deferred().then(callback);
+ var return_records = function() {
+ var records = _.map(ids, function(id) {
+ return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
+ });
+ if (self.debug_mode) {
+ if (_.include(records, undefined)) {
+ throw "Record not correctly loaded";
+ }
+ }
+ setTimeout(function () {completion.resolve(records);}, 0);
+ };
+ if(to_get.length > 0) {
+ var rpc_promise = this._super(to_get, fields, function(records) {
+ _.each(records, function(record, index) {
+ var id = to_get[index];
+ var cached = _.detect(self.cache, function(x) {return x.id === id;});
+ if (!cached) {
+ self.cache.push({id: id, values: record});
+ } else {
+ // I assume cache value is prioritary
+ _.defaults(cached.values, record);
+ }
+ });
+ return_records();
+ });
+ $.when(rpc_promise).fail(function() {completion.reject();});
+ } else {
+ return_records();
+ }
+ return completion.promise();
+ }
+});
+openerp.web.ReadOnlyDataSetSearch = openerp.web.DataSetSearch.extend({
+ create: function(data, callback, error_callback) {
+ this.on_create(data);
+ var to_return = $.Deferred().then(callback);
+ setTimeout(function () {to_return.resolve({"result": undefined});}, 0);
+ return to_return.promise();
+ },
+ on_create: function(data) {},
+ write: function (id, data, options, callback) {
+ this.on_write(id, data);
+ var to_return = $.Deferred().then(callback);
+ setTimeout(function () {to_return.resolve({"result": true});}, 0);
+ return to_return.promise();
+ },
+ on_write: function(id, data) {},
+ unlink: function(ids, callback, error_callback) {
+ this.on_unlink(ids);
+ var to_return = $.Deferred().then(callback);
+ setTimeout(function () {to_return.resolve({"result": true});}, 0);
+ return to_return.promise();
+ },
+ on_unlink: function(ids) {}
+});
+
+openerp.web.Model = openerp.web.SessionAware.extend({
+ init: function(session, model_name) {
+ this._super(session);
+ this.model_name = model_name;
+ },
+ get_func: function(method_name) {
+ var self = this;
+ return function() {
+ if (method_name == "search_read")
+ return self._search_read.apply(self, arguments);
+ return self._call(method_name, _.toArray(arguments));
+ };
+ },
+ _call: function (method, args) {
+ return this.rpc('/web/dataset/call', {
+ model: this.model_name,
+ method: method,
+ args: args
+ }).pipe(function(result) {
+ if (method == "read" && result instanceof Array && result.length > 0 && result[0]["id"]) {
+ var index = {};
+ _.each(_.range(result.length), function(i) {
+ index[result[i]["id"]] = result[i];
+ })
+ result = _.map(args[0], function(x) {return index[x];});
+ }
+ return result;
+ });
+ },
+ _search_read: function(domain, fields, offset, limit, order, context) {
+ return this.rpc('/web/dataset/search_read', {
+ model: this.model_name,
+ fields: fields,
+ offset: offset,
+ limit: limit,
+ domain: domain,
+ sort: order,
+ context: context
+ }).pipe(function(result) {
+ return result.records;
+ });;
+ }
+});
+
+openerp.web.CompoundContext = openerp.web.Class.extend({
+ init: function () {
+ this.__ref = "compound_context";
+ this.__contexts = [];
+ this.__eval_context = null;
+ var self = this;
+ _.each(arguments, function(x) {
+ self.add(x);
+ });
+ },
+ add: function (context) {
+ this.__contexts.push(context);
+ return this;
+ },
+ set_eval_context: function (eval_context) {
+ this.__eval_context = eval_context;
+ return this;
+ },
+ get_eval_context: function () {
+ return this.__eval_context;
+ }
+});
+
+openerp.web.CompoundDomain = openerp.web.Class.extend({
+ init: function () {
+ this.__ref = "compound_domain";
+ this.__domains = [];
+ this.__eval_context = null;
+ var self = this;
+ _.each(arguments, function(x) {
+ self.add(x);
+ });
+ },
+ add: function(domain) {
+ this.__domains.push(domain);
+ return this;
+ },
+ set_eval_context: function(eval_context) {
+ this.__eval_context = eval_context;
+ return this;
+ },
+ get_eval_context: function() {
+ return this.__eval_context;
+ }
+});
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js
new file mode 100644
index 00000000000..51ac2cbd568
--- /dev/null
+++ b/addons/web/static/src/js/data_export.js
@@ -0,0 +1,391 @@
+openerp.web.data_export = function(openerp) {
+var QWeb = openerp.web.qweb;
+openerp.web.DataExport = openerp.web.Dialog.extend({
+ template: 'ExportTreeView',
+ dialog_title: 'Export Data',
+ init: function(parent, dataset) {
+ this._super(parent);
+ this.records = {};
+ this.dataset = dataset;
+ this.exports = new openerp.web.DataSetSearch(
+ this, 'ir.exports', this.dataset.get_context());
+ },
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.open({
+ modal: true,
+ width: '55%',
+ height: 'auto',
+ position: 'top',
+ buttons : {
+ "Close" : function() {
+ self.close();
+ },
+ "Export To File" : function() {
+ self.on_click_export_data();
+ }
+ },
+ close: function(event, ui){ self.close();}
+ });
+ self.$element.removeClass('ui-dialog-content ui-widget-content');
+ self.$element.find('#add_field').click(function() {
+ if ($('#field-tree-structure tr.ui-selected')) {
+ var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a');
+ for (var i = 0; i < fld.length; i++) {
+ var id = $(fld[i]).attr('id').split('-')[1];
+ var string = $(fld[i]).attr('string');
+ self.add_field(id, string);
+ }
+ self.$element.find('#field-tree-structure tr').removeClass('ui-selected');
+ }
+ });
+ self.$element.find('#remove_field').click(function() {
+ self.$element.find('#fields_list option:selected').remove();
+ });
+ self.$element.find('#remove_all_field').click(function() {
+ self.$element.find('#fields_list').empty();
+ });
+ this.$element.find('#export_new_list').click(this.on_show_save_list);
+
+ var got_fields = new $.Deferred();
+ this.$element.find('#import_compat').change(function() {
+ self.$element.find('#fields_list').empty();
+ self.$element.find('#field-tree-structure').remove();
+ var import_comp = self.$element.find("#import_compat").val();
+ self.rpc("/web/export/get_fields", {
+ model: self.dataset.model,
+ import_compat: Boolean(import_comp)
+ }, function (records) {
+ got_fields.resolve();
+ self.on_show_data(records);
+ });
+ }).change();
+
+ return $.when(
+ got_fields,
+ this.rpc('/web/export/formats', {}, this.do_setup_export_formats),
+ this.show_exports_list());
+ },
+ do_setup_export_formats: function (formats) {
+ var $fmts = this.$element.find('#export_format');
+ _(formats).each(function (format) {
+ $fmts.append(new Option(format[1], format[0]));
+ });
+ },
+ show_exports_list: function() {
+ var self = this;
+ if (self.$element.find('#saved_export_list').is(':hidden')) {
+ self.$element.find('#ExistsExportList').show();
+ return;
+ }
+ return this.exports.read_slice(['name'], {
+ domain: [['resource', '=', this.dataset.model]]
+ }, function (export_list) {
+ if (!export_list.length) {
+ return;
+ }
+ self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list}));
+ self.$element.find('#saved_export_list').change(function() {
+ self.$element.find('#fields_list option').remove();
+ var export_id = self.$element.find('#saved_export_list option:selected').val();
+ if (export_id) {
+ self.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field);
+ }
+ });
+ self.$element.find('#delete_export_list').click(function() {
+ var select_exp = self.$element.find('#saved_export_list option:selected');
+ if (select_exp.val()) {
+ self.exports.unlink([parseInt(select_exp.val(), 10)]);
+ select_exp.remove();
+ if (self.$element.find('#saved_export_list option').length <= 1) {
+ self.$element.find('#ExistsExportList').hide();
+ }
+ }
+ });
+ });
+ },
+ do_load_export_field: function(field_list) {
+ var export_node = this.$element.find("#fields_list");
+ _(field_list).each(function (field) {
+ export_node.append(new Option(field.label, field.name));
+ });
+ },
+ on_show_save_list: function() {
+ var self = this;
+ var current_node = self.$element.find("#savenewlist");
+ if (!(current_node.find("label")).length) {
+ current_node.append(QWeb.render('ExportNewList'));
+ current_node.find("#add_export_list").click(function() {
+ var value = current_node.find("#savelist_name").val();
+ if (value) {
+ self.do_save_export_list(value);
+ } else {
+ alert("Pleae Enter Save Field List Name");
+ }
+ });
+ } else {
+ if (current_node.is(':hidden')) {
+ current_node.show();
+ current_node.find("#savelist_name").val("");
+ } else {
+ current_node.hide();
+ }
+ }
+ },
+ do_save_export_list: function(value) {
+ var self = this;
+ var fields = self.get_fields();
+ if (!fields.length) {
+ return;
+ }
+ this.exports.create({
+ name: value,
+ resource: this.dataset.model,
+ export_fields: _(fields).map(function (field) {
+ return [0, 0, {name: field}];
+ })
+ }, function (export_list_id) {
+ if (!export_list_id.result) {
+ return;
+ }
+ self.$element.find("#saved_export_list").append(
+ new Option(value, export_list_id.result));
+ if (self.$element.find("#saved_export_list").is(":hidden")) {
+ self.show_exports_list();
+ }
+ });
+ this.on_show_save_list();
+ this.$element.find("#fields_list option").remove();
+ },
+ on_click: function(id, record) {
+ var self = this;
+ if (!record['children']) {
+ return;
+ }
+ var model = record['params']['model'],
+ prefix = record['params']['prefix'],
+ name = record['params']['name'];
+
+ if (!record.loaded) {
+ var import_comp = self.$element.find("#import_compat").val();
+ self.rpc("/web/export/get_fields", {
+ model: model,
+ prefix: prefix,
+ parent_name: name,
+ import_compat: Boolean(import_comp),
+ parent_field_type : record['field_type']
+ }, function(results) {
+ record.loaded = true;
+ self.on_show_data(results, record.id);
+ });
+ } else {
+ self.showcontent(record.id);
+ }
+ },
+ on_show_data: function(result, after) {
+ var self = this;
+ var imp_cmpt = Boolean(self.$element.find("#import_compat").val());
+
+ if (after) {
+ var current_tr = self.$element.find("tr[id='treerow-" + after + "']");
+ current_tr.addClass('open');
+ current_tr.find('img').attr('src','/web/static/src/img/collapse.gif');
+ current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result}));
+ } else {
+ self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result}));
+ }
+ _.each(result, function(record) {
+ self.records[record.id] = record.value;
+ if (record.required) {
+ var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
+ required_fld.addClass("oe_export_requiredfield");
+ }
+ self.$element.find("img[id='parentimg-" + record.id +"']").click(function() {
+ self.on_click(this.id, record);
+ });
+
+ self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) {
+ if (e.shiftKey) {
+ var frst_click, scnd_click = '';
+ if (self.row_index == 0) {
+ self.row_index = this.rowIndex;
+ frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1];
+ $(frst_click).addClass("ui-selected");
+ } else {
+ if (this.rowIndex >=self.row_index) {
+ for (var i = (self.row_index-1); i < this.rowIndex; i++) {
+ scnd_click = self.$element.find("tr[id^='treerow-']")[i];
+ if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) {
+ $(scnd_click).addClass("ui-selected");
+ }
+ }
+ } else {
+ for (var i = (self.row_index-1); i >= (this.rowIndex-1); i--) {
+ scnd_click = self.$element.find("tr[id^='treerow-']")[i];
+ if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) {
+ $(scnd_click).addClass("ui-selected");
+ }
+ }
+ }
+ }
+ }
+ self.row_index = this.rowIndex;
+
+ self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function() {
+ self.row_index = 0;
+ });
+ var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column');
+ if ($(o2m_selection).hasClass("oe_export_readonlyfield")) {
+ return false;
+ }
+ if (e.ctrlKey) {
+ if ($(this).hasClass('ui-selected')) {
+ $(this).removeClass('ui-selected').find('a').blur();
+ } else {
+ $(this).addClass('ui-selected').find('a').focus();
+ }
+ } else if (!e.shiftKey) {
+ self.$element.find("tr.ui-selected")
+ .removeClass("ui-selected").find('a').blur();
+ $(this).addClass("ui-selected").find('a').focus();
+ }
+ return false;
+ });
+
+ self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) {
+ var keyCode = e.keyCode || e.which;
+ var arrow = {left: 37, up: 38, right: 39, down: 40 };
+ switch (keyCode) {
+ case arrow.left:
+ if ($(this).hasClass('open')) {
+ self.on_click(this.id, record);
+ }
+ break;
+ case arrow.right:
+ if (!$(this).hasClass('open')) {
+ self.on_click(this.id, record);
+ }
+ break;
+ case arrow.up:
+ var elem = this;
+ $(elem).removeClass("ui-selected");
+ while (!$(elem).prev().is(":visible")) {
+ elem = $(elem).prev();
+ }
+ if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) {
+ $(elem).prev().addClass("ui-selected");
+ }
+ $(elem).prev().find('a').focus();
+ break;
+ case arrow.down:
+ var elem = this;
+ $(elem).removeClass("ui-selected");
+ while(!$(elem).next().is(":visible")) {
+ elem = $(elem).next();
+ }
+ if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) {
+ $(elem).next().addClass("ui-selected");
+ }
+ $(elem).next().find('a').focus();
+ break;
+ }
+ });
+ self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function() {
+ var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column');
+ if (!$o2m_selection.hasClass("oe_export_readonlyfield")) {
+ self.add_field(record.id, $(this).find("a").attr("string"));
+ }
+ });
+ });
+ self.$element.find('#fields_list').mouseover(function(event) {
+ if (event.relatedTarget) {
+ if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) {
+ var field_id = event.relatedTarget.attributes["id"]["value"];
+ if (field_id && field_id.split("-")[0] === 'export') {
+ if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) {
+ self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]);
+ }
+ }
+ }
+ }
+ });
+ },
+ showcontent: function(id) {
+ // show & hide the contents
+ var $this = this.$element.find("tr[id='treerow-" + id + "']");
+ var is_open = $this.hasClass('open');
+ $this.toggleClass('open');
+
+ var first_child = $this.find('img');
+ if (is_open) {
+ first_child.attr('src', '/web/static/src/img/expand.gif');
+ } else {
+ first_child.attr('src', '/web/static/src/img/collapse.gif');
+ }
+ var child_field = this.$element.find("tr[id^='treerow-" + id +"/']");
+ var child_len = (id.split("/")).length + 1;
+ for (var i = 0; i < child_field.length; i++) {
+ var $child = $(child_field[i]);
+ if (is_open) {
+ $child.hide();
+ } else if (child_len == (child_field[i].id.split("/")).length) {
+ if ($child.hasClass('open')) {
+ $child.removeClass('open');
+ $child.find('img').attr('src', '/web/static/src/img/expand.gif');
+ }
+ $child.show();
+ }
+ }
+ },
+ add_field: function(field_id, string) {
+ var field_list = this.$element.find('#fields_list');
+ if (this.$element.find("#fields_list option[value='" + field_id + "']")
+ && !this.$element.find("#fields_list option[value='" + field_id + "']").length) {
+ field_list.append(new Option(string, field_id));
+ }
+ },
+ get_fields: function() {
+ var export_field = [];
+ this.$element.find("#fields_list option").each(function() {
+ export_field.push($(this).val());
+ });
+ if (!export_field.length) {
+ alert('Please select fields to save export list...');
+ }
+ return export_field;
+ },
+ on_click_export_data: function() {
+ $.blockUI(this.$element);
+ var exported_fields = [], self = this;
+ this.$element.find("#fields_list option").each(function() {
+ var fieldname = self.records[$(this).val()];
+ exported_fields.push({name: fieldname, label: $(this).text()});
+ });
+ if (_.isEmpty(exported_fields)) {
+ alert('Please select fields to export...');
+ return;
+ }
+
+ exported_fields.unshift({name: 'id', label: 'External ID'});
+ var export_format = this.$element.find("#export_format").val();
+ this.session.get_file({
+ url: '/web/export/' + export_format,
+ data: {data: JSON.stringify({
+ model: this.dataset.model,
+ fields: exported_fields,
+ ids: this.dataset.ids,
+ domain: this.dataset.domain,
+ import_compat: Boolean(
+ this.$element.find("#import_compat").val())
+ })},
+ complete: $.unblockUI
+ });
+ },
+ close: function() {
+ $(this.$dialog).remove();
+ this._super();
+ }
+});
+
+};
diff --git a/addons/web/static/src/js/dates.js b/addons/web/static/src/js/dates.js
new file mode 100644
index 00000000000..036cf8cd90b
--- /dev/null
+++ b/addons/web/static/src/js/dates.js
@@ -0,0 +1,138 @@
+
+openerp.web.dates = function(openerp) {
+
+/**
+ * Converts a string to a Date javascript object using OpenERP's
+ * datetime string format (exemple: '2011-12-01 15:12:35').
+ *
+ * The timezone is assumed to be UTC (standard for OpenERP 6.1)
+ * and will be converted to the browser's timezone.
+ *
+ * @param {String} str A string representing a datetime.
+ * @returns {Date}
+ */
+openerp.web.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 "'" + str + "' is not a valid datetime";
+ }
+ var obj = Date.parse(res[1] + " GMT");
+ if (! obj) {
+ throw "'" + str + "' is not a valid datetime";
+ }
+ return obj;
+};
+
+/**
+ * Converts a string to a Date javascript object using OpenERP's
+ * date string format (exemple: '2011-12-01').
+ *
+ * @param {String} str A string representing a date.
+ * @returns {Date}
+ */
+openerp.web.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 "'" + str + "' is not a valid date";
+ }
+ var obj = Date.parse(str);
+ if (! obj) {
+ throw "'" + str + "' is not a valid date";
+ }
+ return obj;
+};
+
+/**
+ * Converts a string to a Date javascript object using OpenERP's
+ * time string format (exemple: '15:12:35').
+ *
+ * @param {String} str A string representing a time.
+ * @returns {Date}
+ */
+openerp.web.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 "'" + str + "' is not a valid time";
+ }
+ var obj = Date.parse(res[1]);
+ if (! obj) {
+ throw "'" + str + "' is not a valid time";
+ }
+ return obj;
+};
+
+/*
+ * 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 zpad = function(str, size) {
+ str = "" + str;
+ return new Array(size - str.length + 1).join('0') + str;
+};
+
+/**
+ * Converts a Date javascript object to a string using OpenERP's
+ * datetime string format (exemple: '2011-12-01 15:12:35').
+ *
+ * The timezone 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.web.datetime_to_str = function(obj) {
+ if (!obj) {
+ return false;
+ }
+ return zpad(obj.getUTCFullYear(),4) + "-" + zpad(obj.getUTCMonth() + 1,2) + "-"
+ + zpad(obj.getUTCDate(),2) + " " + zpad(obj.getUTCHours(),2) + ":"
+ + zpad(obj.getUTCMinutes(),2) + ":" + zpad(obj.getUTCSeconds(),2);
+};
+
+/**
+ * Converts a Date javascript object to a string using OpenERP's
+ * date string format (exemple: '2011-12-01').
+ *
+ * @param {Date} obj
+ * @returns {String} A string representing a date.
+ */
+openerp.web.date_to_str = function(obj) {
+ if (!obj) {
+ return false;
+ }
+ return zpad(obj.getFullYear(),4) + "-" + zpad(obj.getMonth() + 1,2) + "-"
+ + zpad(obj.getDate(),2);
+};
+
+/**
+ * Converts a Date javascript object to a string using OpenERP's
+ * time string format (exemple: '15:12:35').
+ *
+ * @param {Date} obj
+ * @returns {String} A string representing a time.
+ */
+openerp.web.time_to_str = function(obj) {
+ if (!obj) {
+ return false;
+ }
+ return zpad(obj.getHours(),2) + ":" + zpad(obj.getMinutes(),2) + ":"
+ + zpad(obj.getSeconds(),2);
+};
+
+};
diff --git a/addons/web/static/src/js/form.js b/addons/web/static/src/js/form.js
new file mode 100644
index 00000000000..c4540d591a2
--- /dev/null
+++ b/addons/web/static/src/js/form.js
@@ -0,0 +1,2588 @@
+openerp.web.form = function (openerp) {
+
+var _t = openerp.web._t;
+var QWeb = openerp.web.qweb;
+
+openerp.web.views.add('form', 'openerp.web.FormView');
+openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{
+ /**
+ * 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",
+ /**
+ * @constructs openerp.web.FormView
+ * @extends openerp.web.View
+ *
+ * @param {openerp.web.Session} session the current openerp session
+ * @param {String} element_id this view's root element id
+ * @param {openerp.web.DataSet} dataset the dataset this view will work with
+ * @param {String} view_id the identifier of the OpenERP view object
+ *
+ * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
+ */
+ init: function(parent, element_id, dataset, view_id, options) {
+ this._super(parent, element_id);
+ this.set_default_options(options);
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id;
+ this.fields_view = {};
+ this.widgets = {};
+ this.widgets_counter = 0;
+ this.fields = {};
+ this.datarecord = {};
+ this.ready = false;
+ this.show_invalid = true;
+ this.dirty = false;
+ this.default_focus_field = null;
+ this.default_focus_button = null;
+ this.registry = openerp.web.form.widgets;
+ this.has_been_loaded = $.Deferred();
+ this.$form_header = null;
+ this.translatable_fields = [];
+ _.defaults(this.options, {"always_show_new_button": true});
+ },
+ start: function() {
+ if (this.embedded_view) {
+ var def = $.Deferred().then(this.on_loaded);
+ var self = this;
+ setTimeout(function() {def.resolve(self.embedded_view);}, 0);
+ return def.promise();
+ } else {
+ var context = new openerp.web.CompoundContext(this.dataset.get_context());
+ return this.rpc("/web/view/load", {
+ "model": this.model,
+ "view_id": this.view_id,
+ "view_type": "form",
+ toolbar: this.options.sidebar,
+ context: context
+ }, this.on_loaded);
+ }
+ },
+ stop: function() {
+ if (this.sidebar) {
+ this.sidebar.attachments.stop();
+ this.sidebar.stop();
+ }
+ _.each(this.widgets, function(w) {
+ w.stop();
+ });
+ },
+ on_loaded: function(data) {
+ var self = this;
+ this.fields_view = data;
+ var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
+
+ this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this }));
+ _.each(this.widgets, function(w) {
+ w.start();
+ });
+ this.$form_header = this.$element.find('#' + this.element_id + '_header');
+ this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
+ var action = $(this).data('pager-action');
+ self.on_pager_action(action);
+ });
+
+ this.$form_header.find('button.oe_form_button_save').click(this.do_save);
+ this.$form_header.find('button.oe_form_button_save_edit').click(this.do_save_edit);
+ this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel);
+ this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
+
+ if (this.session.debug) {
+ this.$form_header.find('button.oe_get_xml_view').click(function() {
+ $('<xmp>' + openerp.web.json_node_to_xml(self.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600});
+ });
+ }
+
+ if (this.options.sidebar && this.options.sidebar_id) {
+ this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
+ this.sidebar.start();
+ this.sidebar.do_unfold();
+ this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this);
+ this.sidebar.add_toolbar(this.fields_view.toolbar);
+ this.set_common_sidebar_sections(this.sidebar);
+ }
+ this.has_been_loaded.resolve();
+ },
+ do_show: function () {
+ var promise;
+ if (this.dataset.index === null) {
+ // null index means we should start a new record
+ promise = this.on_button_new();
+ } else {
+ promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
+ }
+ this.$element.show();
+ if (this.sidebar) {
+ this.sidebar.$element.show();
+ }
+ return promise;
+ },
+ do_hide: function () {
+ this.$element.hide();
+ if (this.sidebar) {
+ this.sidebar.$element.hide();
+ }
+ },
+ on_record_loaded: function(record) {
+ if (!record) {
+ throw("Form: No record received");
+ }
+ if (!record.id) {
+ this.$form_header.find('.oe_form_on_create').show();
+ this.$form_header.find('.oe_form_on_update').hide();
+ if (!this.options["always_show_new_button"]) {
+ this.$form_header.find('button.oe_form_button_new').hide();
+ }
+ } else {
+ this.$form_header.find('.oe_form_on_create').hide();
+ this.$form_header.find('.oe_form_on_update').show();
+ this.$form_header.find('button.oe_form_button_new').show();
+ }
+ this.dirty = false;
+ this.datarecord = record;
+ for (var f in this.fields) {
+ var field = this.fields[f];
+ field.dirty = false;
+ field.set_value(this.datarecord[f] || false);
+ field.validate();
+ }
+ if (!record.id) {
+ // New record: Second pass in order to trigger the onchanges
+ this.dirty = true;
+ this.show_invalid = false;
+ for (var f in record) {
+ var field = this.fields[f];
+ if (field) {
+ field.dirty = true;
+ this.do_onchange(field);
+ }
+ }
+ }
+ this.on_form_changed();
+ this.show_invalid = this.ready = true;
+ this.do_update_pager(record.id == null);
+ if (this.sidebar) {
+ this.sidebar.attachments.do_update();
+ this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id);
+ }
+ if (this.default_focus_field && !this.embedded_view) {
+ this.default_focus_field.focus();
+ }
+ },
+ on_form_changed: function() {
+ for (var w in this.widgets) {
+ w = this.widgets[w];
+ w.process_modifiers();
+ w.update_dom();
+ }
+ },
+ on_pager_action: function(action) {
+ 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;
+ }
+ this.reload();
+ },
+ do_update_pager: function(hide_index) {
+ var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager');
+ var index = hide_index ? '-' : this.dataset.index + 1;
+ $pager.find('span.oe_pager_index').html(index);
+ $pager.find('span.oe_pager_count').html(this.dataset.ids.length);
+ },
+ do_onchange: function(widget, processed) {
+ processed = processed || [];
+ if (widget.node.attrs.on_change) {
+ var self = this;
+ this.ready = false;
+ var onchange = _.trim(widget.node.attrs.on_change);
+ var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
+ if (call) {
+ var method = call[1], args = [];
+ var context_index = null;
+ var argument_replacement = {
+ 'False' : function() {return false;},
+ 'True' : function() {return true;},
+ 'None' : function() {return null;},
+ 'context': function(i) {
+ context_index = i;
+ var ctx = widget.build_context ? widget.build_context() : {};
+ return ctx;
+ }
+ };
+ var parent_fields = null;
+ _.each(call[2].split(','), function(a, i) {
+ var field = _.trim(a);
+ if (field in argument_replacement) {
+ args.push(argument_replacement[field](i));
+ return;
+ } else if (self.fields[field]) {
+ var value = self.fields[field].get_on_change_value();
+ args.push(value == null ? false : value);
+ return;
+ } else {
+ var splitted = field.split('.');
+ if (splitted.length > 1 && _.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
+ if (parent_fields === null) {
+ parent_fields = self.dataset.parent_view.get_fields_values();
+ }
+ var p_val = parent_fields[_.trim(splitted[1])];
+ if (p_val !== undefined) {
+ args.push(p_val == null ? false : p_val);
+ return;
+ }
+ }
+ }
+ throw "Could not get field with name '" + field +
+ "' for onchange '" + onchange + "'";
+ });
+ var ajax = {
+ url: '/web/dataset/call',
+ async: false
+ };
+ return this.rpc(ajax, {
+ model: this.dataset.model,
+ method: method,
+ args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args),
+ context_id: context_index === null ? null : context_index + 1
+ }, function(response) {
+ self.on_processed_onchange(response, processed);
+ });
+ } else {
+ console.log("Wrong on_change format", on_change);
+ }
+ }
+ },
+ on_processed_onchange: function(response, processed) {
+ var result = response;
+ if (result.value) {
+ for (var f in result.value) {
+ var field = this.fields[f];
+ // If field is not defined in the view, just ignore it
+ if (field) {
+ var value = result.value[f];
+ processed.push(field.name);
+ if (field.get_value() != value) {
+ field.set_value(value);
+ field.dirty = true;
+ if (_.indexOf(processed, field.name) < 0) {
+ this.do_onchange(field, processed);
+ }
+ }
+ }
+ }
+ this.on_form_changed();
+ }
+ if (!_.isEmpty(result.warning)) {
+ $(QWeb.render("DialogWarning", result.warning)).dialog({
+ modal: true,
+ buttons: {
+ Ok: function() {
+ $(this).dialog("close");
+ }
+ }
+ });
+ }
+ if (result.domain) {
+ // TODO:
+ }
+ this.ready = true;
+ },
+ on_button_new: function() {
+ var self = this;
+ var def = $.Deferred();
+ $.when(this.has_been_loaded).then(function() {
+ self.dataset.default_get(
+ _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() {
+ def.resolve();
+ });
+ });
+ return def.promise();
+ },
+ /**
+ * 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 {Function} success callback on save success
+ * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end)
+ */
+ do_save: function(success, prepend_on_create) {
+ var self = this;
+ if (!this.ready) {
+ return false;
+ }
+ var form_dirty = false,
+ form_invalid = false,
+ values = {},
+ first_invalid_field = null;
+ for (var f in this.fields) {
+ f = this.fields[f];
+ if (!f.is_valid()) {
+ form_invalid = true;
+ f.update_dom();
+ if (!first_invalid_field) {
+ first_invalid_field = f;
+ }
+ } else if (f.is_dirty()) {
+ form_dirty = true;
+ values[f.name] = f.get_value();
+ }
+ }
+ if (form_invalid) {
+ first_invalid_field.focus();
+ this.on_invalid();
+ return false;
+ } else if (form_dirty) {
+ console.log("About to save", values);
+ if (!this.datarecord.id) {
+ return this.dataset.create(values, function(r) {
+ self.on_created(r, success, prepend_on_create);
+ });
+ } else {
+ return this.dataset.write(this.datarecord.id, values, {}, function(r) {
+ self.on_saved(r, success);
+ });
+ }
+ } else {
+ setTimeout(function() {
+ self.on_saved({ result: true }, success);
+ });
+ return true;
+ }
+ },
+ do_save_edit: function() {
+ this.do_save();
+ //this.switch_readonly(); Use promises
+ },
+ switch_readonly: function() {
+ },
+ switch_editable: function() {
+ },
+ on_invalid: function() {
+ var msg = "<ul>";
+ _.each(this.fields, function(f) {
+ if (!f.is_valid()) {
+ msg += "<li>" + f.string + "</li>";
+ }
+ });
+ msg += "</ul>";
+ this.notification.warn("The following fields are invalid :", msg);
+ },
+ on_saved: function(r, success) {
+ if (!r.result) {
+ // should not happen in the server, but may happen for internal purpose
+ } else {
+ console.debug(_.sprintf("The record #%s has been saved.", this.datarecord.id));
+ if (success) {
+ success(r);
+ }
+ this.reload();
+ }
+ },
+ /**
+ * 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 {Function} success callback to execute after having updated the dataset
+ * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end
+ */
+ on_created: function(r, success, prepend_on_create) {
+ if (!r.result) {
+ // should not happen in the server, but may happen for internal purpose
+ } else {
+ this.datarecord.id = r.result;
+ if (!prepend_on_create) {
+ this.dataset.ids.push(this.datarecord.id);
+ this.dataset.index = this.dataset.ids.length - 1;
+ } else {
+ this.dataset.ids.unshift(this.datarecord.id);
+ this.dataset.index = 0;
+ }
+ this.do_update_pager();
+ if (this.sidebar) {
+ this.sidebar.attachments.do_update();
+ }
+ console.debug("The record has been created with id #" + this.datarecord.id);
+ if (success) {
+ success(_.extend(r, {created: true}));
+ }
+ this.reload();
+ }
+ },
+ do_search: function (domains, contexts, groupbys) {
+ console.debug("Searching form");
+ },
+ on_action: function (action) {
+ console.debug('Executing action', action);
+ },
+ do_cancel: function () {
+ console.debug("Cancelling form");
+ },
+ reload: function() {
+ if (this.dataset.index == null || this.dataset.index < 0) {
+ this.on_button_new();
+ } else {
+ this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
+ }
+ },
+ get_fields_values: function() {
+ var values = {};
+ _.each(this.fields, function(value, key) {
+ var val = value.get_value();
+ values[key] = val;
+ });
+ return values;
+ },
+ get_selected_ids: function() {
+ var id = this.dataset.ids[this.dataset.index];
+ return id ? [id] : [];
+ }
+});
+openerp.web.FormDialog = openerp.web.Dialog.extend({
+ init: function(parent, options, view_id, dataset) {
+ this._super(parent, options);
+ this.dataset = dataset;
+ this.view_id = view_id;
+ return this;
+ },
+ start: function() {
+ this._super();
+ this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, {
+ sidebar: false,
+ pager: false
+ });
+ this.form.start();
+ this.form.on_created.add_last(this.on_form_dialog_saved);
+ this.form.on_saved.add_last(this.on_form_dialog_saved);
+ return this;
+ },
+ load_id: function(id) {
+ var self = this;
+ return this.dataset.read_ids([id], _.keys(this.form.fields_view.fields), function(records) {
+ self.form.on_record_loaded(records[0]);
+ });
+ },
+ on_form_dialog_saved: function(r) {
+ this.close();
+ }
+});
+
+/** @namespace */
+openerp.web.form = {};
+
+openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({
+ init: function(parent, element_id, form_view) {
+ this._super(parent, element_id);
+ this.view = form_view;
+ },
+ do_update: function() {
+ if (!this.view.datarecord.id) {
+ this.on_attachments_loaded([]);
+ } else {
+ (new openerp.web.DataSetSearch(
+ this, 'ir.attachment', this.view.dataset.get_context(),
+ [
+ ['res_model', '=', this.view.dataset.model],
+ ['res_id', '=', this.view.datarecord.id],
+ ['type', 'in', ['binary', 'url']]
+ ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded);
+ }
+ },
+ on_attachments_loaded: function(attachments) {
+ this.attachments = attachments;
+ this.$element.html(QWeb.render('FormView.sidebar.attachments', this));
+ this.$element.find('.oe-binary-file').change(this.on_attachment_changed);
+ this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
+ },
+ on_attachment_changed: function(e) {
+ window[this.element_id + '_iframe'] = this.do_update;
+ var $e = $(e.target);
+ if ($e.val() != '') {
+ this.$element.find('form.oe-binary-form').submit();
+ $e.parent().find('input[type=file]').attr('disabled', 'true');
+ $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
+ }
+ },
+ on_attachment_delete: function(e) {
+ var self = this, $e = $(e.currentTarget);
+ var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
+ if (confirm("Do you really want to delete the attachment " + name + " ?")) {
+ this.rpc('/web/dataset/unlink', {
+ model: 'ir.attachment',
+ ids: [parseInt($e.attr('data-id'))]
+ }, function(r) {
+ $e.parent().remove();
+ self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
+ });
+ }
+ }
+});
+
+openerp.web.form.compute_domain = function(expr, fields) {
+ 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('Unknown domain operator ' + ex);
+ }
+ }
+
+ var field = fields[ex[0]];
+ if (!field) {
+ throw new Error("Domain references unknown field : " + ex[0]);
+ }
+ var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value;
+ var op = ex[1];
+ var val = ex[2];
+
+ switch (op.toLowerCase()) {
+ case '=':
+ case '==':
+ stack.push(field_value == val);
+ break;
+ case '!=':
+ 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 '>=':
+ stack.push(field_value >= val);
+ break;
+ case 'in':
+ stack.push(_(val).contains(field_value));
+ break;
+ case 'not in':
+ stack.push(!_(val).contains(field_value));
+ break;
+ default:
+ console.log("Unsupported operator in modifiers :", op);
+ }
+ }
+ return _.all(stack, _.identity);
+};
+
+openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
+ template: 'Widget',
+ /**
+ * @constructs openerp.web.form.Widget
+ * @extends openerp.web.Widget
+ *
+ * @param view
+ * @param node
+ */
+ init: function(view, node) {
+ this.view = view;
+ this.node = node;
+ this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
+ this.type = this.type || node.tag;
+ this.element_name = this.element_name || this.type;
+ this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_");
+
+ this._super(view, this.element_id);
+
+ this.view.widgets[this.element_id] = this;
+ this.children = node.children;
+ this.colspan = parseInt(node.attrs.colspan || 1, 10);
+ this.decrease_max_width = 0;
+
+ this.string = this.string || node.attrs.string;
+ this.help = this.help || node.attrs.help;
+ this.invisible = this.modifiers['invisible'] === true;
+ this.classname = 'oe_form_' + this.type;
+
+ this.width = this.node.attrs.width;
+ },
+ start: function() {
+ this.$element = $('#' + this.element_id);
+ },
+ stop: function() {
+ if (this.$element) {
+ this.$element.remove();
+ }
+ },
+ process_modifiers: function() {
+ var compute_domain = openerp.web.form.compute_domain;
+ for (var a in this.modifiers) {
+ this[a] = compute_domain(this.modifiers[a], this.view.fields);
+ }
+ },
+ update_dom: function() {
+ this.$element.toggle(!this.invisible);
+ },
+ render: function() {
+ var template = this.template;
+ return QWeb.render(template, { "widget": this });
+ }
+});
+
+openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({
+ template: 'WidgetFrame',
+ init: function(view, node) {
+ this._super(view, node);
+ this.columns = parseInt(node.attrs.col || 4, 10);
+ this.x = 0;
+ this.y = 0;
+ this.table = [];
+ this.add_row();
+ for (var i = 0; i < node.children.length; i++) {
+ var n = node.children[i];
+ if (n.tag == "newline") {
+ this.add_row();
+ } else {
+ this.handle_node(n);
+ }
+ }
+ this.set_row_cells_with(this.table[this.table.length - 1]);
+ },
+ add_row: function(){
+ if (this.table.length) {
+ this.set_row_cells_with(this.table[this.table.length - 1]);
+ }
+ var row = [];
+ this.table.push(row);
+ this.x = 0;
+ this.y += 1;
+ return row;
+ },
+ set_row_cells_with: function(row) {
+ var bypass = 0,
+ max_width = 100;
+ for (var i = 0; i < row.length; i++) {
+ bypass += row[i].width === undefined ? 0 : 1;
+ max_width -= row[i].decrease_max_width;
+ }
+ var size_unit = Math.round(max_width / (this.columns - bypass)),
+ colspan_sum = 0;
+ for (var i = 0; i < row.length; i++) {
+ var w = row[i];
+ colspan_sum += w.colspan;
+ if (w.width === undefined) {
+ var width = (i === row.length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan);
+ max_width -= width;
+ w.width = width + '%';
+ }
+ }
+ },
+ handle_node: function(node) {
+ var type = {};
+ if (node.tag == 'field') {
+ type = this.view.fields_view.fields[node.attrs.name] || {};
+ }
+ var widget = new (this.view.registry.get_any(
+ [node.attrs.widget, type.type, node.tag])) (this.view, node);
+ if (node.tag == 'field') {
+ if (!this.view.default_focus_field || node.attrs.default_focus == '1') {
+ this.view.default_focus_field = widget;
+ }
+ if (node.attrs.nolabel != '1') {
+ var label = new (this.view.registry.get_object('label')) (this.view, node);
+ label["for"] = widget;
+ this.add_widget(label, widget.colspan + 1);
+ }
+ }
+ this.add_widget(widget);
+ },
+ add_widget: function(widget, colspan) {
+ colspan = colspan || widget.colspan;
+ var current_row = this.table[this.table.length - 1];
+ if (current_row.length && (this.x + colspan) > this.columns) {
+ current_row = this.add_row();
+ }
+ current_row.push(widget);
+ this.x += widget.colspan;
+ return widget;
+ }
+});
+
+openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
+ template: 'WidgetNotebook',
+ init: function(view, node) {
+ this._super(view, node);
+ this.pages = [];
+ for (var i = 0; i < node.children.length; i++) {
+ var n = node.children[i];
+ if (n.tag == "page") {
+ var page = new openerp.web.form.WidgetNotebookPage(this.view, n, this, this.pages.length);
+ this.pages.push(page);
+ }
+ }
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.tabs();
+ this.view.on_button_new.add_last(this.do_select_first_visible_tab);
+ },
+ do_select_first_visible_tab: function() {
+ for (var i = 0; i < this.pages.length; i++) {
+ var page = this.pages[i];
+ if (page.invisible === false) {
+ this.$element.tabs('select', page.index);
+ break;
+ }
+ }
+ }
+});
+
+openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
+ template: 'WidgetNotebookPage',
+ init: function(view, node, notebook, index) {
+ this.notebook = notebook;
+ this.index = index;
+ this.element_name = 'page_' + index;
+ this._super(view, node);
+ this.element_tab_id = this.element_id + '_tab';
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element_tab = $('#' + this.element_tab_id);
+ },
+ update_dom: function() {
+ if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
+ this.notebook.do_select_first_visible_tab();
+ }
+ this.$element_tab.toggle(!this.invisible);
+ this.$element.toggle(!this.invisible);
+ }
+});
+
+openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "WidgetSeparator";
+ this.orientation = node.attrs.orientation || 'horizontal';
+ if (this.orientation === 'vertical') {
+ this.width = '1';
+ }
+ this.classname += '_' + this.orientation;
+ }
+});
+
+openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "WidgetButton";
+ if (this.string) {
+ // We don't have button key bindings in the webclient
+ this.string = this.string.replace(/_/g, '');
+ }
+ if (node.attrs.default_focus == '1') {
+ // TODO fme: provide enter key binding to widgets
+ this.view.default_focus_button = this;
+ }
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.click(this.on_click);
+ },
+ on_click: function(saved) {
+ var self = this;
+ if (!this.node.attrs.special && this.view.dirty && saved !== true) {
+ this.view.do_save(function() {
+ self.on_click(true);
+ });
+ } else {
+ if (this.node.attrs.confirm) {
+ var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({
+ title: 'Confirm',
+ modal: true,
+ buttons: {
+ Ok: function() {
+ self.on_confirmed();
+ $(this).dialog("close");
+ },
+ Cancel: function() {
+ $(this).dialog("close");
+ }
+ }
+ });
+ } else {
+ this.on_confirmed();
+ }
+ }
+ },
+ on_confirmed: function() {
+ var self = this;
+
+ this.view.execute_action(
+ this.node.attrs, this.view.dataset, this.view.datarecord.id, function () {
+ self.view.reload();
+ });
+ }
+});
+
+openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
+ init: function(view, node) {
+ this.element_name = 'label_' + node.attrs.name;
+
+ this._super(view, node);
+
+ // TODO fme: support for attrs.align
+ if (this.node.tag == 'label' && (this.node.attrs.colspan || (this.string && this.string.length > 32))) {
+ this.template = "WidgetParagraph";
+ this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
+ } else {
+ this.template = "WidgetLabel";
+ this.colspan = 1;
+ this.width = '1%';
+ this.decrease_max_width = 1;
+ this.nowrap = true;
+ }
+ },
+ render: function () {
+ if (this['for'] && this.type !== 'label') {
+ return QWeb.render(this.template, {widget: this['for']});
+ }
+ // Actual label widgets should not have a false and have type label
+ return QWeb.render(this.template, {widget: this});
+ },
+ start: function() {
+ this._super();
+ var self = this;
+ this.$element.find("label").dblclick(function() {
+ var widget = self['for'] || self;
+ console.log(widget.element_id , widget);
+ window.w = widget;
+ });
+ }
+});
+
+openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{
+ /**
+ * @constructs openerp.web.form.Field
+ * @extends openerp.web.form.Widget
+ *
+ * @param view
+ * @param node
+ */
+ init: function(view, node) {
+ this.name = node.attrs.name;
+ this.value = undefined;
+ view.fields[this.name] = this;
+ this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
+ this.element_name = "field_" + this.name + "_" + this.type;
+
+ this._super(view, node);
+
+ if (node.attrs.nolabel != '1' && this.colspan > 1) {
+ this.colspan--;
+ }
+ this.field = view.fields_view.fields[node.attrs.name] || {};
+ this.string = node.attrs.string || this.field.string;
+ this.help = node.attrs.help || this.field.help;
+ this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1';
+ this.readonly = this.modifiers['readonly'] === true;
+ this.required = this.modifiers['required'] === true;
+ this.invalid = false;
+ this.dirty = false;
+
+ this.classname = 'oe_form_field_' + this.type;
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ if (this.field.translate) {
+ this.view.translatable_fields.push(this);
+ this.$element.find('.oe_field_translate').click(this.on_translate);
+ }
+ },
+ set_value: function(value) {
+ this.value = value;
+ this.invalid = false;
+ this.update_dom();
+ this.on_value_changed();
+ },
+ set_value_from_ui: function() {
+ this.on_value_changed();
+ },
+ on_value_changed: function() {
+ },
+ on_translate: function() {
+ this.view.open_translate_dialog(this);
+ },
+ get_value: function() {
+ return this.value;
+ },
+ is_valid: function() {
+ return !this.invalid;
+ },
+ is_dirty: function() {
+ return this.dirty;
+ },
+ get_on_change_value: function() {
+ return this.get_value();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ if (this.field.translate) {
+ this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
+ }
+ if (!this.disable_utility_classes) {
+ this.$element.toggleClass('disabled', this.readonly);
+ this.$element.toggleClass('required', this.required);
+ if (this.view.show_invalid) {
+ this.$element.toggleClass('invalid', !this.is_valid());
+ }
+ }
+ },
+ on_ui_change: function() {
+ this.dirty = this.view.dirty = true;
+ this.validate();
+ if (this.is_valid()) {
+ this.set_value_from_ui();
+ this.view.do_onchange(this);
+ this.view.on_form_changed();
+ } else {
+ this.update_dom();
+ }
+ },
+ validate: function() {
+ this.invalid = false;
+ },
+ focus: function() {
+ },
+ _build_view_fields_values: function() {
+ var a_dataset = this.view.dataset || {};
+ var fields_values = this.view.get_fields_values();
+ var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {};
+ fields_values.parent = parent_values;
+ return fields_values;
+ },
+ /**
+ * Builds a new context usable for operations related to fields by merging
+ * the fields'context with the action's context.
+ */
+ build_context: function() {
+ // I previously belevied contexts should be herrited, but now I doubt it
+ //var a_context = this.view.dataset.get_context() || {};
+ var f_context = this.field.context || null;
+ // maybe the default_get should only be used when we do a default_get?
+ var v_context1 = this.node.attrs.default_get || {};
+ var v_context2 = this.node.attrs.context || {};
+ var v_context = new openerp.web.CompoundContext(v_context1, v_context2);
+ if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true
+ var fields_values = this._build_view_fields_values();
+ v_context.set_eval_context(fields_values);
+ }
+ // if there is a context on the node, overrides the model's context
+ var ctx = f_context || v_context;
+ return ctx;
+ },
+ build_domain: function() {
+ var f_domain = this.field.domain || null;
+ var v_domain = this.node.attrs.domain || [];
+ if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true
+ var fields_values = this._build_view_fields_values();
+ v_domain = new openerp.web.CompoundDomain(v_domain).set_eval_context(fields_values);
+ }
+ // if there is a domain on the node, overrides the model's domain
+ return f_domain || v_domain;
+ }
+});
+
+openerp.web.form.FieldChar = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldChar";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').change(this.on_ui_change);
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = openerp.web.format_value(value, this, '');
+ this.$element.find('input').val(show_value);
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').attr('disabled', this.readonly);
+ },
+ set_value_from_ui: function() {
+ this.value = openerp.web.parse_value(this.$element.find('input').val(), this);
+ this._super();
+ },
+ validate: function() {
+ this.invalid = false;
+ try {
+ var value = openerp.web.parse_value(this.$element.find('input').val(), this, '');
+ this.invalid = this.required && value === '';
+ } catch(e) {
+ this.invalid = true;
+ }
+ },
+ focus: function() {
+ this.$element.find('input').focus();
+ }
+});
+
+openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldEmail";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('button').click(this.on_button_clicked);
+ },
+ on_button_clicked: function() {
+ if (!this.value || !this.is_valid()) {
+ this.notification.warn("E-mail error", "Can't send email to invalid e-mail address");
+ } else {
+ location.href = 'mailto:' + this.value;
+ }
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val());
+ }
+});
+
+openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldUrl";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('button').click(this.on_button_clicked);
+ },
+ on_button_clicked: function() {
+ if (!this.value) {
+ this.notification.warn("Resource error", "This resource is empty");
+ } else {
+ window.open(this.value);
+ }
+ }
+});
+
+openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
+ set_value: function(value) {
+ if (value === false || value === undefined) {
+ // As in GTK client, floats default to 0
+ value = 0;
+ this.dirty = true;
+ }
+ this._super.apply(this, [value]);
+ }
+});
+
+openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldDate";
+ this.jqueryui_object = 'datetimepicker';
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({
+ dateFormat: 'yy-mm-dd',
+ timeFormat: 'hh:mm:ss',
+ showOn: 'button',
+ buttonImage: '/web/static/src/img/ui/field_calendar.png',
+ buttonImageOnly: true,
+ constrainInput: false
+ });
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ if (!value) {
+ this.$element.find('input').val('');
+ } else {
+ this.$element.find('input').unbind('change');
+ // jQuery UI date picker wrongly call on_change event herebelow
+ this.$element.find('input')[this.jqueryui_object]('setDate', this.parse(value));
+ this.$element.find('input').change(this.on_ui_change);
+ }
+ },
+ set_value_from_ui: function() {
+ this.value = this.$element.find('input')[this.jqueryui_object]('getDate') || false;
+ if (this.value) {
+ this.value = this.format(this.value);
+ }
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').datepicker(this.readonly ? 'disable' : 'enable');
+ },
+ validate: function() {
+ this.invalid = false;
+ var value = this.$element.find('input').val();
+ if (value === "") {
+ this.invalid = this.required;
+ } else {
+ this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate');
+ }
+ },
+ focus: function() {
+ this.$element.find('input').focus();
+ },
+ parse: openerp.web.auto_str_to_date,
+ format: function(val) {
+ return openerp.web.auto_date_to_str(val, this.field.type);
+ }
+});
+
+openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.jqueryui_object = 'datepicker';
+ }
+});
+
+openerp.web.form.FieldText = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldText";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('textarea').change(this.on_ui_change);
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = openerp.web.format_value(value, this, '');
+ this.$element.find('textarea').val(show_value);
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('textarea').attr('disabled', this.readonly);
+ },
+ set_value_from_ui: function() {
+ this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this);
+ this._super();
+ },
+ validate: function() {
+ this.invalid = false;
+ try {
+ var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, '');
+ this.invalid = this.required && value === '';
+ } catch(e) {
+ this.invalid = true;
+ }
+ },
+ focus: function() {
+ this.$element.find('textarea').focus();
+ }
+});
+
+openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldBoolean";
+ },
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.$element.find('input').click(function() {
+ if ($(this).is(':checked') != self.value) {
+ self.on_ui_change();
+ }
+ });
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ this.$element.find('input')[0].checked = value;
+ },
+ set_value_from_ui: function() {
+ this.value = this.$element.find('input').is(':checked');
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').attr('disabled', this.readonly);
+ },
+ validate: function() {
+ this.invalid = this.required && !this.$element.find('input').is(':checked');
+ },
+ focus: function() {
+ this.$element.find('input').focus();
+ }
+});
+
+openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldProgressBar";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('div').progressbar({
+ value: this.value,
+ disabled: this.readonly
+ });
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = Number(value);
+ if (isNaN(show_value)) {
+ show_value = 0;
+ }
+ this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
+ }
+});
+
+openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
+// to replace view editor
+});
+
+openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ var self = this;
+ this._super(view, node);
+ this.template = "FieldSelection";
+ this.values = this.field.selection;
+ _.each(this.values, function(v, i) {
+ if (v[0] === false && v[1] === '') {
+ self.values.splice(i, 1);
+ }
+ });
+ this.values.unshift([false, '']);
+ },
+ start: 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;
+ this._super.apply(this, arguments);
+ this.$element.find('select')
+ .change(this.on_ui_change)
+ .change(function () { ischanging = true; })
+ .click(function () { ischanging = false; })
+ .keyup(function (e) {
+ if (e.which !== 13 || !ischanging) { return; }
+ e.stopPropagation();
+ ischanging = false;
+ });
+ },
+ set_value: function(value) {
+ value = value === null ? false : value;
+ value = value instanceof Array ? value[0] : value;
+ this._super(value);
+ var index = 0;
+ for (var i = 0, ii = this.values.length; i < ii; i++) {
+ if (this.values[i][0] === value) index = i;
+ }
+ this.$element.find('select')[0].selectedIndex = index;
+ },
+ set_value_from_ui: function() {
+ this.value = this.values[this.$element.find('select')[0].selectedIndex][0];
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('select').attr('disabled', this.readonly);
+ },
+ validate: function() {
+ var value = this.values[this.$element.find('select')[0].selectedIndex];
+ this.invalid = !(value && !(this.required && value[0] === false));
+ },
+ focus: function() {
+ this.$element.find('select').focus();
+ }
+});
+
+// jquery autocomplete tweak to allow html
+(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 );
+ }
+ });
+})();
+
+openerp.web.form.dialog = function(content, options) {
+ options = _.extend({
+ autoOpen: true,
+ width: '90%',
+ height: '90%',
+ min_width: '800px',
+ min_height: '600px'
+ }, options || {});
+ options.autoOpen = true;
+ var dialog = new openerp.web.Dialog(null, options);
+ dialog.$dialog = $(content).dialog(dialog.dialog_options);
+ return dialog.$dialog;
+};
+
+openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldMany2One";
+ this.limit = 7;
+ this.value = null;
+ this.cm_id = _.uniqueId('m2o_cm_');
+ this.last_search = [];
+ this.tmp_value = undefined;
+ },
+ start: function() {
+ this._super();
+ var self = this;
+ this.$input = this.$element.find("input");
+ this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
+ this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
+
+ // context menu
+ var init_context_menu_def = $.Deferred().then(function(e) {
+ var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
+ rdataset.call("get", ['action', 'client_action_relate',
+ [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
+ .then(function(result) {
+ self.related_entries = result;
+
+ var $cmenu = $("#" + self.cm_id);
+ $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
+ var bindings = {};
+ bindings[self.cm_id + "_search"] = function() {
+ self._search_create_popup("search");
+ };
+ bindings[self.cm_id + "_create"] = function() {
+ self._search_create_popup("form");
+ };
+ bindings[self.cm_id + "_open"] = function() {
+ if (!self.value) {
+ return;
+ }
+ var pop = new openerp.web.form.FormOpenPopup(self.view);
+ pop.show_element(self.field.relation, self.value[0],self.build_context(), {});
+ pop.on_write_completed.add_last(function() {
+ self.set_value(self.value[0]);
+ });
+ };
+ _.each(_.range(self.related_entries.length), function(i) {
+ bindings[self.cm_id + "_related_" + i] = function() {
+ self.open_related(self.related_entries[i]);
+ };
+ });
+ var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true,
+ bindings: bindings, itemStyle: {"color": ""},
+ onContextMenu: function() {
+ if(self.value) {
+ $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm");
+ } else {
+ $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm");
+ }
+ return true;
+ }, menuStyle: {width: "200px"}
+ });
+ setTimeout(function() {self.$menu_btn.trigger(e);}, 0);
+ });
+ });
+ var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()};
+ this.$menu_btn.bind('contextmenu', ctx_callback);
+ this.$menu_btn.click(ctx_callback);
+
+ // some behavior for input
+ this.$input.keyup(function() {
+ if (self.$input.val() === "") {
+ self._change_int_value(null);
+ } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
+ self._change_int_value(undefined);
+ }
+ });
+ this.$drop_down.click(function() {
+ if (self.$input.autocomplete("widget").is(":visible")) {
+ self.$input.autocomplete("close");
+ } else {
+ if (self.value) {
+ self.$input.autocomplete("search", "");
+ } else {
+ self.$input.autocomplete("search");
+ }
+ self.$input.focus();
+ }
+ });
+ var anyoneLoosesFocus = function() {
+ if (!self.$input.is(":focus") &&
+ !self.$input.autocomplete("widget").is(":visible") &&
+ !self.value) {
+ if (self.value === undefined && self.last_search.length > 0) {
+ self._change_int_ext_value(self.last_search[0]);
+ } else {
+ self._change_int_ext_value(null);
+ }
+ }
+ };
+ this.$input.focusout(anyoneLoosesFocus);
+
+ var isSelecting = false;
+ // autocomplete
+ this.$input.autocomplete({
+ source: function(req, resp) { self.get_search_result(req, resp); },
+ select: function(event, ui) {
+ isSelecting = true;
+ var item = ui.item;
+ if (item.id) {
+ self._change_int_value([item.id, item.name]);
+ } else if (item.action) {
+ self._change_int_value(undefined);
+ item.action();
+ return false;
+ }
+ },
+ focus: function(e, ui) {
+ e.preventDefault();
+ },
+ html: true,
+ close: anyoneLoosesFocus,
+ minLength: 0,
+ delay: 0
+ });
+ // 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) {
+ if (isSelecting)
+ e.stopPropagation();
+ }
+ isSelecting = false;
+ });
+ },
+ // autocomplete component content handling
+ get_search_result: function(request, response) {
+ var search_val = request.term;
+ var self = this;
+
+ var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
+
+ dataset.name_search(search_val, self.build_domain(), 'ilike',
+ this.limit + 1, function(data) {
+ self.last_search = data;
+ // possible selections for the m2o
+ var values = _.map(data, function(x) {
+ return {label: $('<span />').text(x[1]).html(), 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("<em>   Search More...</em>"), action: function() {
+ dataset.name_search(search_val, self.build_domain(), 'ilike'
+ , false, function(data) {
+ self._change_int_value(null);
+ self._search_create_popup("search", data);
+ });
+ }});
+ }
+ // quick create
+ var raw_result = _(data.result).map(function(x) {return x[1];});
+ if (search_val.length > 0 &&
+ !_.include(raw_result, search_val) &&
+ (!self.value || search_val !== self.value[1])) {
+ values.push({label: _.sprintf(_t('<em>   Create "<strong>%s</strong>"</em>'),
+ $('<span />').text(search_val).html()), action: function() {
+ self._quick_create(search_val);
+ }});
+ }
+ // create...
+ values.push({label: _t("<em>   Create and Edit...</em>"), action: function() {
+ self._change_int_value(null);
+ self._search_create_popup("form", undefined, {"default_name": search_val});
+ }});
+
+ response(values);
+ });
+ },
+ _quick_create: function(name) {
+ var self = this;
+ var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
+ dataset.name_create(name, function(data) {
+ self._change_int_ext_value(data);
+ }).fail(function(error, event) {
+ event.preventDefault();
+ self._change_int_value(null);
+ self._search_create_popup("form", undefined, {"default_name": name});
+ });
+ },
+ // all search/create popup handling
+ _search_create_popup: function(view, ids, context) {
+ var self = this;
+ var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.select_element(self.field.relation,{
+ initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
+ initial_view: view,
+ disable_multiple_selection: true
+ }, self.build_domain(),
+ new openerp.web.CompoundContext(self.build_context(), context || {}));
+ pop.on_select_elements.add(function(element_ids) {
+ var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context());
+ dataset.name_get([element_ids[0]], function(data) {
+ self._change_int_ext_value(data[0]);
+ });
+ });
+ },
+ _change_int_ext_value: function(value) {
+ this._change_int_value(value);
+ this.$input.val(this.value ? this.value[1] : "");
+ },
+ _change_int_value: function(value) {
+ this.value = value;
+ var back_orig_value = this.original_value;
+ if (this.value === null || this.value) {
+ this.original_value = this.value;
+ }
+ if (back_orig_value === undefined) { // first use after a set_value()
+ return;
+ }
+ if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null)
+ !== (this.value ? this.value[0] : null))) {
+ this.on_ui_change();
+ }
+ },
+ set_value: function(value) {
+ value = value || null;
+ this.invalid = false;
+ var self = this;
+ this.tmp_value = value;
+ self.update_dom();
+ self.on_value_changed();
+ var real_set_value = function(rval) {
+ self.tmp_value = undefined;
+ self.value = rval;
+ self.original_value = undefined;
+ self._change_int_ext_value(rval);
+ };
+ if(typeof(value) === "number") {
+ var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
+ dataset.name_get([value], function(data) {
+ real_set_value(data[0]);
+ }).fail(function() {self.tmp_value = undefined;});
+ } else {
+ setTimeout(function() {real_set_value(value);}, 0);
+ }
+ },
+ get_value: function() {
+ if (this.tmp_value !== undefined) {
+ if (this.tmp_value instanceof Array) {
+ return this.tmp_value[0];
+ }
+ return this.tmp_value ? this.tmp_value : false;
+ }
+ if (this.value === undefined)
+ return this.original_value ? this.original_value[0] : false;
+ return this.value ? this.value[0] : false;
+ },
+ validate: function() {
+ this.invalid = false;
+ var val = this.tmp_value !== undefined ? this.tmp_value : this.value;
+ if (val === null) {
+ this.invalid = this.required;
+ }
+ },
+ open_related: function(related) {
+ var self = this;
+ if (!self.value)
+ return;
+ var additional_context = {
+ active_id: self.value[0],
+ active_ids: [self.value[0]],
+ active_model: self.field.relation
+ };
+ self.rpc("/web/action/load", {
+ action_id: related[2].id,
+ context: additional_context
+ }, function(result) {
+ result.result.context = _.extend(result.result.context || {}, additional_context);
+ self.do_action(result.result);
+ });
+ }
+});
+
+/*
+# 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];
+ }
+};
+openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
+ multi_selection: false,
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldOne2Many";
+ this.is_started = $.Deferred();
+ this.form_last_update = $.Deferred();
+ this.disable_utility_classes = true;
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+
+ var self = this;
+
+ this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation);
+ this.dataset.o2m = this;
+ this.dataset.parent_view = this.view;
+ this.dataset.on_change.add_last(function() {
+ self.on_ui_change();
+ });
+
+ var modes = this.node.attrs.mode;
+ modes = !!modes ? modes.split(",") : ["tree", "form"];
+ var views = [];
+ _.each(modes, function(mode) {
+ var view = {
+ view_id: false,
+ view_type: mode == "tree" ? "list" : mode,
+ options: { sidebar : false }
+ };
+ if (self.field.views && self.field.views[mode]) {
+ view.embedded_view = self.field.views[mode];
+ }
+ if(view.view_type === "list") {
+ view.options.selectable = self.multi_selection;
+ }
+ views.push(view);
+ });
+ this.views = views;
+
+ this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views);
+ this.viewmanager.registry = openerp.web.views.clone({
+ list: 'openerp.web.form.One2ManyListView',
+ form: 'openerp.web.form.One2ManyFormView'
+ });
+ var once = $.Deferred().then(function() {
+ self.form_last_update.resolve();
+ });
+ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
+ if (view_type == "list") {
+ controller.o2m = self;
+ } else if (view_type == "form") {
+ controller.on_record_loaded.add_last(function() {
+ once.resolve();
+ });
+ controller.on_pager_action.add_first(function() {
+ self.save_form_view();
+ });
+ controller.$element.find(".oe_form_button_save_edit").hide();
+ }
+ self.is_started.resolve();
+ });
+ this.viewmanager.on_mode_switch.add_first(function() {
+ self.save_form_view();
+ });
+ setTimeout(function () {
+ self.viewmanager.appendTo(self.$element);
+ }, 0);
+ },
+ reload_current_view: function() {
+ var self = this;
+ var view = self.viewmanager.views[self.viewmanager.active_view].controller;
+ if(self.viewmanager.active_view === "list") {
+ view.reload_content();
+ } else if (self.viewmanager.active_view === "form") {
+ if (this.dataset.index === null && this.dataset.ids.length >= 1) {
+ this.dataset.index = 0;
+ }
+ this.form_last_update.then(function() {
+ this.form_last_update = view.do_show();
+ });
+ }
+ },
+ set_value: function(value) {
+ value = value || [];
+ var self = this;
+ this.dataset.reset_ids([]);
+ if(value.length >= 1 && value[0] instanceof Array) {
+ var ids = [];
+ _.each(value, function(command) {
+ var obj = {values: command[2]};
+ switch (command[0]) {
+ case commands.CREATE:
+ obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ ids.push(obj.id);
+ return;
+ case commands.UPDATE:
+ obj['id'] = command[1];
+ self.dataset.to_write.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ 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") {
+ var ids = [];
+ this.dataset.delete_all = true;
+ _.each(value, function(command) {
+ var obj = {values: command};
+ obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
+ 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;
+ }
+ $.when(this.is_started).then(function() {
+ self.reload_current_view();
+ });
+ },
+ 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);}));
+ },
+ save_form_view: function() {
+ if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
+ this.viewmanager.views[this.viewmanager.active_view] &&
+ this.viewmanager.views[this.viewmanager.active_view].controller) {
+ var view = this.viewmanager.views[this.viewmanager.active_view].controller;
+ if (this.viewmanager.active_view === "form") {
+ var res = $.when(view.do_save());
+ if (res === false) {
+ // ignore
+ } else if (res.isRejected()) {
+ throw "Save or create on one2many dataset is not supposed to fail.";
+ } else if (!res.isResolved()) {
+ throw "Asynchronous get_value() is not supported in form view.";
+ }
+ return res;
+ }
+ }
+ return false;
+ },
+ is_valid: function() {
+ this.validate();
+ return this._super();
+ },
+ validate: function() {
+ this.invalid = false;
+ var self = this;
+ var view = self.viewmanager.views[self.viewmanager.active_view].controller;
+ if (self.viewmanager.active_view === "form") {
+ for (var f in view.fields) {
+ f = view.fields[f];
+ if (!f.is_valid()) {
+ this.invalid = true;
+ return;
+ }
+ }
+ }
+ },
+ is_dirty: function() {
+ this.save_form_view();
+ return this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.toggleClass('disabled', this.readonly);
+ }
+});
+
+openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({
+ get_context: function() {
+ this.context = this.o2m.build_context();
+ return this.context;
+ }
+});
+
+openerp.web.form.One2ManyFormView = openerp.web.FormView.extend({
+});
+
+openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
+ do_add_record: function () {
+ if (this.options.editable) {
+ this._super.apply(this, arguments);
+ } else {
+ var self = this;
+ var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.select_element(self.o2m.field.relation,{
+ initial_view: "form",
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
+ create_function: function(data) {
+ return self.o2m.dataset.create(data, function(r) {
+ self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
+ self.o2m.dataset.on_change();
+ });
+ },
+ parent_view: self.o2m.view
+ }, self.o2m.build_domain(), self.o2m.build_context());
+ pop.on_select_elements.add_last(function() {
+ self.o2m.reload_current_view();
+ });
+ }
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new openerp.web.form.FormOpenPopup(self.o2m.view);
+ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{
+ auto_write: false,
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
+ parent_view: self.o2m.view,
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ }
+ });
+ pop.on_write.add(function(id, data) {
+ self.o2m.dataset.write(id, data, {}, function(r) {
+ self.o2m.reload_current_view();
+ });
+ });
+ }
+});
+
+openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
+ multi_selection: false,
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldMany2Many";
+ this.list_id = _.uniqueId("many2many");
+ this.is_started = $.Deferred();
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+
+ var self = this;
+
+ this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation);
+ this.dataset.m2m = this;
+ this.dataset.on_unlink.add_last(function(ids) {
+ self.on_ui_change();
+ });
+
+ this.list_view = new openerp.web.form.Many2ManyListView(this, this.list_id, this.dataset, false, {
+ 'addable': 'Add',
+ 'selectable': self.multi_selection
+ });
+ this.list_view.m2m_field = this;
+ this.list_view.on_loaded.add_last(function() {
+ self.is_started.resolve();
+ });
+ setTimeout(function () {
+ self.list_view.start();
+ }, 0);
+ },
+ set_value: function(value) {
+ value = value || [];
+ if (value.length >= 1 && value[0] instanceof Array) {
+ value = value[0][2];
+ }
+ this._super(value);
+ this.dataset.set_ids(value);
+ var self = this;
+ $.when(this.is_started).then(function() {
+ self.list_view.reload_content();
+ });
+ },
+ get_value: function() {
+ return [commands.replace_with(this.dataset.ids)];
+ },
+ validate: function() {
+ this.invalid = false;
+ // TODO niv
+ }
+});
+
+openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({
+ get_context: function() {
+ this.context = this.m2m.build_context();
+ return this.context;
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.ListView
+ */
+openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{
+ do_add_record: function () {
+ var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.select_element(this.model, {},
+ new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
+ this.m2m_field.build_context());
+ var self = this;
+ pop.on_select_elements.add(function(element_ids) {
+ _.each(element_ids, function(element_id) {
+ if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
+ self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
+ self.m2m_field.on_ui_change();
+ self.reload_content();
+ }
+ });
+ });
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new openerp.web.form.FormOpenPopup(this);
+ pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {});
+ pop.on_write_completed.add_last(function() {
+ self.reload_content();
+ });
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.OldWidget
+ */
+openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{
+ identifier_prefix: "selectcreatepopup",
+ template: "SelectCreatePopup",
+ /**
+ * options:
+ * - initial_ids
+ * - initial_view: form or search (default search)
+ * - disable_multiple_selection
+ * - alternative_form_view
+ * - create_function (defaults to a naive saving behavior)
+ * - parent_view
+ */
+ select_element: function(model, options, domain, context) {
+ var self = this;
+ this.model = model;
+ this.domain = domain || [];
+ this.context = context || {};
+ this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() {
+ return self.create_row.apply(self, arguments);
+ }});
+ this.initial_ids = this.options.initial_ids;
+ this.created_elements = [];
+ openerp.web.form.dialog(this.render(), {close:function() {
+ self.check_exit();
+ }});
+ this.start();
+ },
+ start: function() {
+ this._super();
+ this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model,
+ this.context);
+ this.dataset.parent_view = this.options.parent_view;
+ if (this.options.initial_view == "search") {
+ this.setup_search_view();
+ } else { // "form"
+ this.new_object();
+ }
+ },
+ setup_search_view: function() {
+ var self = this;
+ if (this.searchview) {
+ this.searchview.stop();
+ }
+ this.searchview = new openerp.web.SearchView(this,
+ this.element_id + "_search", this.dataset, false, {
+ "selectable": !this.options.disable_multiple_selection,
+ "deletable": false
+ });
+ this.searchview.on_search.add(function(domains, contexts, groupbys) {
+ if (self.initial_ids) {
+ self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]),
+ contexts, groupbys);
+ self.initial_ids = undefined;
+ } else {
+ self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys);
+ }
+ });
+ this.searchview.on_loaded.add_last(function () {
+ var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
+ $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
+ var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
+ $cbutton.click(function() {
+ self.stop();
+ });
+ var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
+ if(self.options.disable_multiple_selection) {
+ $sbutton.hide();
+ }
+ $sbutton.click(function() {
+ self.on_select_elements(self.selected_ids);
+ self.stop();
+ });
+ self.view_list = new openerp.web.form.SelectCreateListView(self,
+ self.element_id + "_view_list", self.dataset, false,
+ {'deletable': false});
+ self.view_list.popup = self;
+ self.view_list.do_show();
+ self.view_list.start().then(function() {
+ self.searchview.do_search();
+ });
+ });
+ this.searchview.start();
+ },
+ create_row: function(data) {
+ var self = this;
+ var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
+ wdataset.parent_view = this.options.parent_view;
+ return wdataset.create(data);
+ },
+ on_select_elements: function(element_ids) {
+ },
+ on_click_element: function(ids) {
+ this.selected_ids = ids || [];
+ if(this.selected_ids.length > 0) {
+ this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
+ } else {
+ this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
+ }
+ },
+ new_object: function() {
+ var self = this;
+ if (this.searchview) {
+ this.searchview.hide();
+ }
+ if (this.view_list) {
+ this.view_list.$element.hide();
+ }
+ this.dataset.index = null;
+ this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false);
+ if (this.options.alternative_form_view) {
+ this.view_form.set_embedded_view(this.options.alternative_form_view);
+ }
+ this.view_form.start();
+ this.view_form.on_loaded.add_last(function() {
+ var $buttons = self.view_form.$element.find(".oe_form_buttons");
+ $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
+ var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new");
+ $nbutton.click(function() {
+ self._created = $.Deferred().then(function() {
+ self._created = undefined;
+ self.view_form.on_button_new();
+ });
+ self.view_form.do_save();
+ });
+ var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
+ $nbutton.click(function() {
+ self._created = $.Deferred().then(function() {
+ self._created = undefined;
+ self.check_exit();
+ });
+ self.view_form.do_save();
+ });
+ var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
+ $cbutton.click(function() {
+ self.check_exit();
+ });
+ });
+ this.dataset.on_create.add(function(data) {
+ self.options.create_function(data).then(function(r) {
+ self.created_elements.push(r.result);
+ if (self._created) {
+ self._created.resolve();
+ }
+ });
+ });
+ this.view_form.do_show();
+ },
+ check_exit: function() {
+ if (this.created_elements.length > 0) {
+ this.on_select_elements(this.created_elements);
+ }
+ this.stop();
+ }
+});
+
+openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
+ do_add_record: function () {
+ this.popup.new_object();
+ },
+ select_record: function(index) {
+ this.popup.on_select_elements([this.dataset.ids[index]]);
+ this.popup.stop();
+ },
+ do_select: function(ids, records) {
+ this._super(ids, records);
+ this.popup.on_click_element(ids);
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.OldWidget
+ */
+openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{
+ identifier_prefix: "formopenpopup",
+ template: "FormOpenPopup",
+ /**
+ * options:
+ * - alternative_form_view
+ * - auto_write (default true)
+ * - read_function
+ * - parent_view
+ */
+ show_element: function(model, row_id, context, options) {
+ this.model = model;
+ this.row_id = row_id;
+ this.context = context || {};
+ this.options = _.defaults(options || {}, {"auto_write": true});
+ jQuery(this.render()).dialog({title: '',
+ modal: true,
+ width: 960,
+ height: 600});
+ this.start();
+ },
+ start: function() {
+ this._super();
+ this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context);
+ this.dataset.fop = this;
+ this.dataset.ids = [this.row_id];
+ this.dataset.index = 0;
+ this.dataset.parent_view = this.options.parent_view;
+ this.setup_form_view();
+ },
+ on_write: function(id, data) {
+ this.stop();
+ if (!this.options.auto_write)
+ return;
+ var self = this;
+ var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
+ wdataset.parent_view = this.options.parent_view;
+ wdataset.write(id, data, {}, function(r) {
+ self.on_write_completed();
+ });
+ },
+ on_write_completed: function() {},
+ setup_form_view: function() {
+ var self = this;
+ this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false);
+ if (this.options.alternative_form_view) {
+ this.view_form.set_embedded_view(this.options.alternative_form_view);
+ }
+ this.view_form.start();
+ this.view_form.on_loaded.add_last(function() {
+ var $buttons = self.view_form.$element.find(".oe_form_buttons");
+ $buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
+ var $nbutton = $buttons.find(".oe_formopenpopup-form-save");
+ $nbutton.click(function() {
+ self.view_form.do_save();
+ });
+ var $cbutton = $buttons.find(".oe_formopenpopup-form-close");
+ $cbutton.click(function() {
+ self.stop();
+ });
+ self.view_form.do_show();
+ });
+ this.dataset.on_write.add(this.on_write);
+ }
+});
+
+openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({
+ read_ids: function() {
+ if (this.fop.options.read_function) {
+ return this.fop.options.read_function.apply(null, arguments);
+ } else {
+ return this._super.apply(this, arguments);
+ }
+ }
+});
+
+openerp.web.form.FieldReference = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldReference";
+ this.fields_view = {
+ fields: {
+ selection: {
+ selection: view.fields_view.fields[this.name].selection
+ },
+ m2o: {
+ relation: null
+ }
+ }
+ };
+ this.get_fields_values = view.get_fields_values;
+ this.do_onchange = this.on_form_changed = this.on_nop;
+ this.widgets = {};
+ this.fields = {};
+ this.selection = new openerp.web.form.FieldSelection(this, { attrs: {
+ name: 'selection',
+ widget: 'selection'
+ }});
+ this.selection.on_value_changed.add_last(this.on_selection_changed);
+ this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: {
+ name: 'm2o',
+ widget: 'many2one'
+ }});
+ },
+ on_nop: function() {
+ },
+ on_selection_changed: function() {
+ this.m2o.field.relation = this.selection.get_value();
+ this.m2o.set_value(null);
+ },
+ start: function() {
+ this._super();
+ this.selection.start();
+ this.m2o.start();
+ },
+ is_valid: function() {
+ return this.required === false || typeof(this.get_value()) === 'string';
+ },
+ is_dirty: function() {
+ return this.selection.is_dirty() || this.m2o.is_dirty();
+ },
+ set_value: function(value) {
+ this._super(value);
+ if (typeof(value) === 'string') {
+ var vals = value.split(',');
+ this.selection.set_value(vals[0]);
+ this.m2o.set_value(parseInt(vals[1], 10));
+ }
+ },
+ get_value: function() {
+ var model = this.selection.get_value(),
+ id = this.m2o.get_value();
+ if (typeof(model) === 'string' && typeof(id) === 'number') {
+ return model + ',' + id;
+ } else {
+ return false;
+ }
+ }
+});
+
+openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.iframe = this.element_id + '_iframe';
+ this.binary_value = false;
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input.oe-binary-file').change(this.on_file_change);
+ this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
+ this.$element.find('.oe-binary-file-clear').click(this.on_clear);
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('.oe-binary').toggle(!this.readonly);
+ },
+ human_filesize : function(size) {
+ var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ var i = 0;
+ while (size >= 1024) {
+ size /= 1024;
+ ++i;
+ }
+ return size.toFixed(2) + ' ' + units[i];
+ },
+ on_file_change: function(e) {
+ // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
+ // http://www.html5rocks.com/tutorials/file/dndfiles/
+ // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
+ window[this.iframe] = this.on_file_uploaded;
+ if ($(e.target).val() != '') {
+ this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
+ this.$element.find('form.oe-binary-form').submit();
+ this.toggle_progress();
+ }
+ },
+ toggle_progress: function() {
+ this.$element.find('.oe-binary-progress, .oe-binary').toggle();
+ },
+ on_file_uploaded: function(size, name, content_type, file_base64) {
+ delete(window[this.iframe]);
+ if (size === false) {
+ this.notification.warn("File Upload", "There was a problem while uploading your file");
+ // TODO: use openerp web crashmanager
+ console.warn("Error while uploading file : ", name);
+ } else {
+ this.on_file_uploaded_and_valid.apply(this, arguments);
+ this.on_ui_change();
+ }
+ this.toggle_progress();
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ },
+ on_save_as: function() {
+ if (!this.view.datarecord.id) {
+ this.notification.warn("Can't save file", "The record has not yet been saved");
+ } else {
+ var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' +
+ this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
+ '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime());
+ window.open(url);
+ }
+ },
+ on_clear: function() {
+ if (this.value !== false) {
+ this.value = false;
+ this.binary_value = false;
+ this.on_ui_change();
+ }
+ return false;
+ }
+});
+
+openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldBinaryFile";
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = (value != null && value !== false) ? value : '';
+ this.$element.find('input').eq(0).val(show_value);
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ this.value = file_base64;
+ this.binary_value = true;
+ var show_value = this.human_filesize(size);
+ this.$element.find('input').eq(0).val(show_value);
+ this.set_filename(name);
+ },
+ set_filename: function(value) {
+ var filename = this.node.attrs.filename;
+ if (this.view.fields[filename]) {
+ this.view.fields[filename].set_value(value);
+ this.view.fields[filename].on_ui_change();
+ }
+ },
+ on_clear: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').eq(0).val('');
+ this.set_filename('');
+ }
+});
+
+openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldBinaryImage";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$image = this.$element.find('img.oe-binary-image');
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ this.set_image_maxwidth();
+ var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
+ this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
+ this.$image.attr('src', url);
+ },
+ set_image_maxwidth: function() {
+ this.$image.css('max-width', this.$element.width());
+ },
+ on_file_change: function() {
+ this.set_image_maxwidth();
+ this._super.apply(this, arguments);
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ this.value = file_base64;
+ this.binary_value = true;
+ this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
+ },
+ on_clear: function() {
+ this._super.apply(this, arguments);
+ this.$image.attr('src', '/web/static/src/img/placeholder.png');
+ }
+});
+
+openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
+ template: "FieldStatus",
+ start: function() {
+ this._super();
+ this.selected_value = null;
+
+ this.render_list();
+ },
+ set_value: function(value) {
+ this._super(value);
+ this.selected_value = value;
+
+ this.render_list();
+ },
+ render_list: function() {
+ var self = this;
+ var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
+ function(x) { return x.trim(); });
+ shown = _.select(shown, function(x) { return x.length > 0; });
+
+ if (shown.length == 0) {
+ this.to_show = this.field.selection;
+ } else {
+ this.to_show = _.select(this.field.selection, function(x) {
+ return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value;
+ });
+ }
+
+ var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_});
+ this.$element.html(content);
+
+ var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
+ var color = colors[this.selected_value];
+ if (color) {
+ var elem = this.$element.find("li.oe-arrow-list-selected span");
+ elem.css("border-color", color);
+ elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before");
+ elem.css("border-left-color", "rgba(0,0,0,0)");
+ elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after");
+ elem.css("border-color", "rgba(0,0,0,0)");
+ elem.css("border-left-color", color);
+ }
+ }
+});
+
+/**
+ * Registry of form widgets, called by :js:`openerp.web.FormView`
+ */
+openerp.web.form.widgets = new openerp.web.Registry({
+ 'frame' : 'openerp.web.form.WidgetFrame',
+ 'group' : 'openerp.web.form.WidgetFrame',
+ 'notebook' : 'openerp.web.form.WidgetNotebook',
+ 'separator' : 'openerp.web.form.WidgetSeparator',
+ 'label' : 'openerp.web.form.WidgetLabel',
+ 'button' : 'openerp.web.form.WidgetButton',
+ 'char' : 'openerp.web.form.FieldChar',
+ 'email' : 'openerp.web.form.FieldEmail',
+ 'url' : 'openerp.web.form.FieldUrl',
+ 'text' : 'openerp.web.form.FieldText',
+ 'text_wiki' : 'openerp.web.form.FieldText',
+ 'date' : 'openerp.web.form.FieldDate',
+ 'datetime' : 'openerp.web.form.FieldDatetime',
+ 'selection' : 'openerp.web.form.FieldSelection',
+ 'many2one' : 'openerp.web.form.FieldMany2One',
+ 'many2many' : 'openerp.web.form.FieldMany2Many',
+ 'one2many' : 'openerp.web.form.FieldOne2Many',
+ 'one2many_list' : 'openerp.web.form.FieldOne2Many',
+ 'reference' : 'openerp.web.form.FieldReference',
+ 'boolean' : 'openerp.web.form.FieldBoolean',
+ 'float' : 'openerp.web.form.FieldFloat',
+ 'integer': 'openerp.web.form.FieldFloat',
+ 'float_time': 'openerp.web.form.FieldFloat',
+ 'progressbar': 'openerp.web.form.FieldProgressBar',
+ 'image': 'openerp.web.form.FieldBinaryImage',
+ 'binary': 'openerp.web.form.FieldBinaryFile',
+ 'statusbar': 'openerp.web.form.FieldStatus'
+});
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
new file mode 100644
index 00000000000..b0dc4fcc289
--- /dev/null
+++ b/addons/web/static/src/js/formats.js
@@ -0,0 +1,197 @@
+
+openerp.web.formats = function(openerp) {
+
+/**
+ * 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
+ */
+openerp.web.format_value = function (value, descriptor, value_if_empty) {
+ // If NaN value, display as with a `false` (empty cell)
+ if (typeof value === 'number' && isNaN(value)) {
+ value = false;
+ }
+ switch (value) {
+ case false:
+ case Infinity:
+ case -Infinity:
+ return value_if_empty === undefined ? '' : value_if_empty;
+ }
+ switch (descriptor.widget || descriptor.type) {
+ case 'integer':
+ return _.sprintf('%d', value);
+ case 'float':
+ var precision = descriptor.digits ? descriptor.digits[1] : 2;
+ var int_part = Math.floor(value);
+ var dec_part = Math.abs(Math.floor((value % 1) * Math.pow(10, precision)));
+ return _.sprintf('%d%s%d',
+ int_part,
+ openerp.web._t.database.parameters.decimal_point, dec_part);
+ case 'float_time':
+ return _.sprintf("%02d:%02d",
+ Math.floor(value),
+ Math.round((value % 1) * 60));
+ case 'progressbar':
+ return _.sprintf(
+ '<progress value="%.2f" max="100.0">%.2f%%</progress>',
+ value, value);
+ case 'many2one':
+ // name_get value format
+ return value[1];
+ case 'datetime':
+ if (typeof(value) == "string")
+ value = openerp.web.auto_str_to_date(value);
+ try {
+ return value.toString(_.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate,
+ Date.CultureInfo.formatPatterns.longTime));
+ } catch (e) {
+ return value.format("%m/%d/%Y %H:%M:%S");
+ }
+ return value;
+ case 'date':
+ if (typeof(value) == "string")
+ value = openerp.web.auto_str_to_date(value);
+ try {
+ return value.toString(Date.CultureInfo.formatPatterns.shortDate);
+ } catch (e) {
+ return value.format("%m/%d/%Y");
+ }
+ case 'time':
+ if (typeof(value) == "string")
+ value = openerp.web.auto_str_to_date(value);
+ try {
+ return value.toString(Date.CultureInfo.formatPatterns.longTime);
+ } catch (e) {
+ return value.format("%H:%M:%S");
+ }
+ default:
+ return value;
+ }
+};
+
+openerp.web.parse_value = function (value, descriptor, value_if_empty) {
+ switch (value) {
+ case false:
+ case "":
+ return value_if_empty === undefined ? false : value_if_empty;
+ }
+ switch (descriptor.widget || descriptor.type) {
+ case 'integer':
+ var tmp = Number(value);
+ if (isNaN(tmp))
+ throw value + " is not a correct integer";
+ return tmp;
+ case 'float':
+ var tmp = Number(value);
+ if (!isNaN(tmp))
+ return tmp;
+ tmp = value.replace(openerp.web._t.database.parameters.decimal_point, ".");
+ var tmp2 = tmp;
+ do {
+ tmp = tmp2;
+ tmp2 = tmp.replace(openerp.web._t.database.parameters.thousands_sep, "");
+ } while(tmp !== tmp2);
+ tmp = Number(tmp);
+ if (isNaN(tmp))
+ throw value + " is not a correct float";
+ return tmp;
+ case 'float_time':
+ var tmp = value.split(":");
+ if (tmp.length != 2)
+ throw value + " is not a correct float_time";
+ var tmp1 = openerp.web.parse_value(tmp[0], {type: "integer"});
+ var tmp2 = openerp.web.parse_value(tmp[1], {type: "integer"});
+ return tmp1 + (tmp2 / 60);
+ case 'progressbar':
+ return openerp.web.parse_value(value, {type: "float"});
+ case 'datetime':
+ var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate,
+ Date.CultureInfo.formatPatterns.longTime));
+ if (tmp !== null)
+ return tmp;
+ tmp = Date.parse(value);
+ if (tmp !== null)
+ return tmp;
+ throw value + " is not a valid datetime";
+ case 'date':
+ var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate);
+ if (tmp !== null)
+ return tmp;
+ tmp = Date.parse(value);
+ if (tmp !== null)
+ return tmp;
+ throw value + " is not a valid date";
+ case 'time':
+ var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime);
+ if (tmp !== null)
+ return tmp;
+ tmp = Date.parse(value);
+ if (tmp !== null)
+ return tmp;
+ throw value + " is not a valid time";
+ }
+ return value;
+};
+
+openerp.web.auto_str_to_date = function(value, type) {
+ try {
+ return openerp.web.str_to_datetime(value);
+ } catch(e) {}
+ try {
+ return openerp.web.str_to_date(value);
+ } catch(e) {}
+ try {
+ return openerp.web.str_to_time(value);
+ } catch(e) {}
+ throw "'" + value + "' is not a valid date, datetime nor time"
+};
+
+openerp.web.auto_date_to_str = function(value, type) {
+ switch(type) {
+ case 'datetime':
+ return openerp.web.datetime_to_str(value);
+ case 'date':
+ return openerp.web.date_to_str(value);
+ case 'time':
+ return openerp.web.time_to_str(value);
+ default:
+ throw type + " is not convertible to date, datetime nor time"
+ }
+};
+
+/**
+ * Formats a provided cell based on its field type
+ *
+ * @param {Object} row_data record whose values should be displayed in the cell
+ * @param {Object} column column descriptor
+ * @param {"button"|"field"} column.tag base control type
+ * @param {String} column.type widget type for a field control
+ * @param {String} [column.string] button label
+ * @param {String} [column.icon] button icon
+ * @param {String} [value_if_empty=''] what to display if the field's value is ``false``
+ */
+openerp.web.format_cell = function (row_data, column, value_if_empty) {
+ var attrs = column.modifiers_for(row_data);
+ if (attrs.invisible) { return ''; }
+ if (column.tag === 'button') {
+ return [
+ '<button type="button" title="', column.string || '', '">',
+ '<img src="/web/static/src/img/icons/', column.icon, '.png"',
+ ' alt="', column.string || '', '"/>',
+ '</button>'
+ ].join('')
+ }
+
+ if (!row_data[column.id]) {
+ return value_if_empty === undefined ? '' : value_if_empty;
+ }
+ return openerp.web.format_value(
+ row_data[column.id].value, column, value_if_empty);
+}
+
+};
diff --git a/addons/web/static/src/js/list-editable.js b/addons/web/static/src/js/list-editable.js
new file mode 100644
index 00000000000..c38c43a4c9c
--- /dev/null
+++ b/addons/web/static/src/js/list-editable.js
@@ -0,0 +1,336 @@
+/**
+ * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
+ * @namespace
+ */
+openerp.web.list_editable = function (openerp) {
+ var KEY_RETURN = 13,
+ KEY_ESCAPE = 27;
+
+ // editability status of list rows
+ openerp.web.ListView.prototype.defaults.editable = null;
+
+ // TODO: not sure second @lends on existing item is correct, to check
+ openerp.web.ListView.include(/** @lends openerp.web.ListView# */{
+ init: function () {
+ var self = this;
+ this._super.apply(this, arguments);
+ $(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.compute_aggregates();
+ }
+ })
+ },
+ /**
+ * 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 {openerp.web.DataSet} dataset dataset in which the record is available
+ */
+ do_edit: function (index, id, dataset) {
+ _.extend(this.dataset, dataset);
+ },
+ /**
+ * Sets editability status for the list, based on defaults, view
+ * architecture and the provided flag, if any.
+ *
+ * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
+ */
+ set_editable: function (force) {
+ // If ``force``, set editability to bottom
+ // otherwise rely on view default
+ // view' @editable is handled separately as we have not yet
+ // fetched and processed the view at this point.
+ this.options.editable = (
+ (force && "bottom")
+ || this.defaults.editable);
+ },
+ /**
+ * Replace do_actual_search to handle editability process
+ */
+ do_actual_search: function (results) {
+ this.set_editable(results.context['set_editable']);
+ this._super(results);
+ },
+ /**
+ * 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 () {
+ if (this.options.editable) {
+ this.groups.new_record();
+ } else {
+ this._super();
+ }
+ },
+ on_loaded: function (data, grouped) {
+ // tree/@editable takes priority on everything else if present.
+ this.options.editable = data.arch.attrs.editable || this.options.editable;
+ return this._super(data, grouped);
+ }
+ });
+
+ openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{
+ passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved",
+ new_record: function () {
+ // TODO: handle multiple children
+ this.children[null].new_record();
+ }
+ });
+
+ openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{
+ row_clicked: function (event) {
+ if (!this.options.editable) {
+ return this._super(event);
+ }
+ this.edit_record($(event.currentTarget).data('id'));
+ },
+ /**
+ * Checks if a record is being edited, and if so cancels it
+ */
+ cancel_pending_edition: function () {
+ var self = this, cancelled = $.Deferred();
+ if (!this.edition) {
+ cancelled.resolve();
+ return cancelled.promise();
+ }
+
+ if (this.edition_id != null) {
+ this.reload_record(self.records.get(this.edition_id)).then(function () {
+ cancelled.resolve();
+ });
+ } else {
+ cancelled.resolve();
+ }
+ cancelled.then(function () {
+ self.view.unpad_columns();
+ self.edition_form.stop();
+ self.edition_form.$element.remove();
+ delete self.edition_form;
+ delete self.edition_id;
+ delete self.edition;
+ });
+ return cancelled.promise();
+ },
+ /**
+ * Adapts this list's view description to be suitable to the inner form
+ * view of a row being edited.
+ *
+ * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
+ */
+ get_form_fields_view: function () {
+ // deep copy of view
+ var view = $.extend(true, {}, this.group.view.fields_view);
+ _(view.arch.children).each(function (widget) {
+ widget.attrs.nolabel = true;
+ if (widget.tag === 'button') {
+ delete widget.attrs.string;
+ }
+ });
+ view.arch.attrs.col = 2 * view.arch.children.length;
+ return view;
+ },
+ render_row_as_form: function (row) {
+ var self = this;
+ this.cancel_pending_edition().then(function () {
+ var record_id = $(row).data('id');
+ var $new_row = $('<tr>', {
+ id: _.uniqueId('oe-editable-row-'),
+ 'data-id': record_id,
+ 'class': $(row).attr('class') + ' oe_forms',
+ click: function (e) {e.stopPropagation();}
+ })
+ .delegate('button.oe-edit-row-save', 'click', function () {
+ self.save_row();
+ })
+ .delegate('button.oe-edit-row-cancel', 'click', function () {
+ self.cancel_edition();
+ })
+ .delegate('button', 'keyup', function (e) {
+ e.stopImmediatePropagation();
+ })
+ .keyup(function (e) {
+ switch (e.which) {
+ case KEY_RETURN:
+ self.save_row(true);
+ break;
+ case KEY_ESCAPE:
+ self.cancel_edition();
+ break;
+ default:
+ return;
+ }
+ });
+ if (row) {
+ $new_row.replaceAll(row);
+ } else if (self.options.editable === 'top') {
+ self.$current.prepend($new_row);
+ } else if (self.options.editable) {
+ self.$current.append($new_row);
+ }
+ self.edition = true;
+ self.edition_id = record_id;
+ self.edition_form = _.extend(new openerp.web.FormView(
+ self, $new_row.attr('id'), self.dataset, false), {
+ template: 'ListView.row.form',
+ registry: openerp.web.list.form.widgets
+ });
+ $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
+ // put in $.when just in case FormView.on_loaded becomes asynchronous
+ $new_row.find('td')
+ .addClass('oe-field-cell')
+ .removeAttr('width')
+ .end()
+ .find('td:first').removeClass('oe-field-cell').end()
+ .find('td:last').removeClass('oe-field-cell').end();
+ // pad in case of groupby
+ _(self.columns).each(function (column) {
+ if (column.meta) {
+ $new_row.prepend('<td>');
+ }
+ });
+ // Add columns for the cancel and save buttons, if
+ // there are none in the list
+ if (!self.options.selectable) {
+ self.view.pad_columns(
+ 1, {except: $new_row, position: 'before'});
+ }
+ if (!self.options.deletable) {
+ self.view.pad_columns(
+ 1, {except: $new_row});
+ }
+
+ self.edition_form.do_show();
+ });
+ });
+ },
+ handle_onwrite: function (source_record_id) {
+ var self = this;
+ var on_write_callback = self.view.fields_view.arch.attrs.on_write;
+ if (!on_write_callback) { return; }
+ this.dataset.call(on_write_callback, [source_record_id], function (ids) {
+ _(ids).each(function (id) {
+ var record = self.records.get(id);
+ if (!record) {
+ // insert after the source record
+ var index = self.records.indexOf(
+ self.records.get(source_record_id)) + 1;
+ record = new openerp.web.list.Record({id: id});
+ self.records.add(record, {at: index});
+ self.dataset.ids.splice(index, 0, id);
+ }
+ self.reload_record(record);
+ });
+ });
+ },
+ /**
+ * Saves the current row, and triggers the edition of its following
+ * sibling if asked.
+ *
+ * @param {Boolean} [edit_next=false] should the next row become editable
+ */
+ save_row: function (edit_next) {
+ var self = this;
+ this.edition_form.do_save(function (result) {
+ if (result.created && !self.edition_id) {
+ self.records.add({id: result.result},
+ {at: self.options.editable === 'top' ? 0 : null});
+ self.edition_id = result.result;
+ }
+ var edited_record = self.records.get(self.edition_id),
+ next_record = self.records.at(
+ self.records.indexOf(edited_record) + 1);
+
+ self.handle_onwrite(self.edition_id);
+ self.cancel_pending_edition().then(function () {
+ $(self).trigger('saved', [self.dataset]);
+ if (!edit_next) {
+ return;
+ }
+ if (result.created) {
+ self.new_record();
+ return;
+ }
+ var next_record_id;
+ if (next_record) {
+ next_record_id = next_record.get('id');
+ self.dataset.index = _(self.dataset.ids)
+ .indexOf(next_record_id);
+ } else {
+ self.dataset.index = 0;
+ next_record_id = self.records.at(0).get('id');
+ }
+ self.edit_record(next_record_id);
+ });
+ }, this.options.editable === 'top');
+ },
+ /**
+ * Cancels the edition of the row for the current dataset index
+ */
+ cancel_edition: function () {
+ this.cancel_pending_edition();
+ },
+ /**
+ * Edits record currently selected via dataset
+ */
+ edit_record: function (record_id) {
+ this.render_row_as_form(
+ this.$current.find('[data-id=' + record_id + ']'));
+ $(this).trigger(
+ 'edit',
+ [record_id, this.dataset]);
+ },
+ new_record: function () {
+ this.dataset.index = null;
+ this.render_row_as_form();
+ }
+ });
+ if (!openerp.web.list) {
+ openerp.web.list = {};
+ }
+ if (!openerp.web.list.form) {
+ openerp.web.list.form = {};
+ }
+ openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({
+ template: 'ListView.row.frame'
+ });
+ var form_widgets = openerp.web.form.widgets;
+ openerp.web.list.form.widgets = form_widgets.clone({
+ 'frame': 'openerp.web.list.form.WidgetFrame'
+ });
+ // All form widgets inherit a problematic behavior from
+ // openerp.web.form.WidgetFrame: the cell itself is removed when invisible
+ // whether it's @invisible or @attrs[invisible]. In list view, only the
+ // former should completely remove the cell. We need to override update_dom
+ // on all widgets since we can't just hit on widget itself (I think)
+ var list_form_widgets = openerp.web.list.form.widgets;
+ _(list_form_widgets.map).each(function (widget_path, key) {
+ if (key === 'frame') { return; }
+ var new_path = 'openerp.web.list.form.' + key;
+
+ openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({
+ update_dom: function () {
+ this.$element.children().css('visibility', '');
+ if (this.modifiers.tree_invisible) {
+ var old_invisible = this.invisible;
+ this.invisible = !!this.modifiers.tree_invisible;
+ this._super();
+ this.invisible = old_invisible;
+ } else if (this.invisible) {
+ this.$element.children().css('visibility', 'hidden');
+ }
+ }
+ });
+ list_form_widgets.add(key, new_path);
+ });
+};
diff --git a/addons/web/static/src/js/list.js b/addons/web/static/src/js/list.js
new file mode 100644
index 00000000000..95a8207144b
--- /dev/null
+++ b/addons/web/static/src/js/list.js
@@ -0,0 +1,1523 @@
+openerp.web.list = function (openerp) {
+var QWeb = openerp.web.qweb;
+openerp.web.views.add('list', 'openerp.web.ListView');
+openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ {
+ defaults: {
+ // records can be selected one by one
+ 'selectable': true,
+ // list rows can be deleted
+ 'deletable': true,
+ // whether the column headers should be displayed
+ 'header': true,
+ // display addition button, with that label
+ 'addable': "New",
+ // whether the list view can be sorted, note that once a view has been
+ // sorted it can not be reordered anymore
+ 'sortable': true,
+ // whether the view rows can be reordered (via vertical drag & drop)
+ 'reorderable': true
+ },
+ /**
+ * Core class for list-type displays.
+ *
+ * As a view, needs a number of view-related parameters to be correctly
+ * instantiated, provides options and overridable methods for behavioral
+ * customization.
+ *
+ * See constructor parameters and method documentations for information on
+ * the default behaviors and possible options for the list view.
+ *
+ * @constructs openerp.web.ListView
+ * @extends openerp.web.View
+ *
+ * @param parent parent object
+ * @param element_id the id of the DOM elements this view should link itself to
+ * @param {openerp.web.DataSet} dataset the dataset the view should work with
+ * @param {String} view_id the listview's identifier, if any
+ * @param {Object} options A set of options used to configure the view
+ * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
+ * @param {Boolean} [options.header=true] should the list's header be displayed
+ * @param {Boolean} [options.deletable=true] are the list rows deletable
+ * @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
+ * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
+ * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
+ *
+ * @borrows openerp.web.ActionExecutor#execute_action as #execute_action
+ */
+ init: function(parent, element_id, dataset, view_id, options) {
+ var self = this;
+ this._super(parent, element_id);
+ this.set_default_options(_.extend({}, this.defaults, options || {}));
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id;
+ this.previous_colspan = null;
+
+ this.columns = [];
+
+ this.records = new Collection();
+
+ this.set_groups(new openerp.web.ListView.Groups(this));
+
+ if (this.dataset instanceof openerp.web.DataSetStatic) {
+ this.groups.datagroup = new openerp.web.StaticDataGroup(this.dataset);
+ } else {
+ this.groups.datagroup = new openerp.web.DataGroup(
+ this, this.model,
+ dataset.get_domain(),
+ dataset.get_context(),
+ {});
+ this.groups.datagroup.sort = this.dataset._sort;
+ }
+
+ this.page = 0;
+ this.records.bind('change', function (event, record, key) {
+ if (!_(self.aggregate_columns).chain()
+ .pluck('name').contains(key).value()) {
+ return;
+ }
+ self.compute_aggregates();
+ });
+ },
+ /**
+ * Retrieves the view's number of records per page (|| section)
+ *
+ * options > defaults > parent.action.limit > indefinite
+ *
+ * @returns {Number|null}
+ */
+ limit: function () {
+ if (this._limit === undefined) {
+ this._limit = (this.options.limit
+ || this.defaults.limit
+ || (this.widget_parent.action || {}).limit
+ || null);
+ }
+ return this._limit;
+ },
+ /**
+ * Set a custom Group construct as the root of the List View.
+ *
+ * @param {openerp.web.ListView.Groups} groups
+ */
+ set_groups: function (groups) {
+ var self = this;
+ if (this.groups) {
+ $(this.groups).unbind("selected deleted action row_link");
+ delete this.groups;
+ }
+
+ this.groups = groups;
+ $(this.groups).bind({
+ 'selected': function (e, ids, records) {
+ self.do_select(ids, records);
+ },
+ 'deleted': function (e, ids) {
+ self.do_delete(ids);
+ },
+ 'action': function (e, action_name, id, callback) {
+ self.do_button_action(action_name, id, callback);
+ },
+ 'row_link': function (e, id, dataset) {
+ self.do_activate_record(dataset.index, id, dataset);
+ }
+ });
+ },
+ /**
+ * View startup method, the default behavior is to set the ``oe-listview``
+ * class on its root element and to perform an RPC load call.
+ *
+ * @returns {$.Deferred} loading promise
+ */
+ start: function() {
+ this.$element.addClass('oe-listview');
+ return this.reload_view(null, null, true);
+ },
+ /**
+ * Called after loading the list view's description, sets up such things
+ * as the view table's columns, renders the table itself and hooks up the
+ * various table-level and row-level DOM events (action buttons, deletion
+ * buttons, selection of records, [New] button, selection of a given
+ * record, ...)
+ *
+ * Sets up the following:
+ *
+ * * Processes arch and fields to generate a complete field descriptor for each field
+ * * Create the table itself and allocate visible columns
+ * * Hook in the top-level (header) [New|Add] and [Delete] button
+ * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
+ * * Sets up event handlers for action buttons and per-row deletion button
+ * * Hooks global callback for clicking on a row
+ * * Sets up its sidebar, if any
+ *
+ * @param {Object} data wrapped fields_view_get result
+ * @param {Object} data.fields_view fields_view_get result (processed)
+ * @param {Object} data.fields_view.fields mapping of fields for the current model
+ * @param {Object} data.fields_view.arch current list view descriptor
+ * @param {Boolean} grouped Is the list view grouped
+ */
+ on_loaded: function(data, grouped) {
+ var self = this;
+ this.fields_view = data;
+ this.name = "" + this.fields_view.arch.attrs.string;
+
+ this.setup_columns(this.fields_view.fields, grouped);
+
+ this.$element.html(QWeb.render("ListView", this));
+
+ // Head hook
+ this.$element.find('.oe-list-add')
+ .click(this.do_add_record)
+ .attr('disabled', grouped && this.options.editable);
+ this.$element.find('.oe-list-delete')
+ .attr('disabled', true)
+ .click(this.do_delete_selected);
+ this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) {
+ e.stopPropagation();
+
+ var $this = $(this);
+ self.dataset.sort($this.data('id'));
+ if ($this.find('span').length) {
+ $this.find('span').toggleClass(
+ 'ui-icon-triangle-1-s ui-icon-triangle-1-n');
+ } else {
+ $this.append('<span class="ui-icon ui-icon-triangle-1-s">')
+ .siblings('.oe-sortable').find('span').remove();
+ }
+
+ self.reload_content();
+ });
+
+ this.$element.find('.oe-list-pager')
+ .delegate('button', 'click', function () {
+ var $this = $(this);
+ switch ($this.data('pager-action')) {
+ case 'first':
+ self.page = 0; break;
+ case 'last':
+ self.page = Math.floor(
+ self.dataset.ids.length / self.limit());
+ break;
+ case 'next':
+ self.page += 1; break;
+ case 'previous':
+ self.page -= 1; break;
+ }
+ self.reload_content();
+ }).find('.oe-pager-state')
+ .click(function (e) {
+ e.stopPropagation();
+ var $this = $(this);
+
+ var $select = $('<select>')
+ .appendTo($this.empty())
+ .click(function (e) {e.stopPropagation();})
+ .append('<option value="80">80</option>' +
+ '<option value="100">100</option>' +
+ '<option value="200">200</option>' +
+ '<option value="500">500</option>' +
+ '<option value="NaN">Unlimited</option>')
+ .change(function () {
+ var val = parseInt($select.val(), 10);
+ self._limit = (isNaN(val) ? null : val);
+ self.page = 0;
+ self.reload_content();
+ })
+ .val(self._limit || 'NaN');
+ });
+ if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) {
+ this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
+ this.sidebar.start();
+ this.sidebar.add_toolbar(this.fields_view.toolbar);
+ this.set_common_sidebar_sections(this.sidebar);
+ }
+ },
+ /**
+ * Configures the ListView pager based on the provided dataset's information
+ *
+ * Horrifying side-effect: sets the dataset's data on this.dataset?
+ *
+ * @param {openerp.web.DataSet} dataset
+ */
+ configure_pager: function (dataset) {
+ this.dataset.ids = dataset.ids;
+
+ var limit = this.limit(),
+ total = dataset.ids.length,
+ first = (this.page * limit),
+ last;
+ if (!limit || (total - first) < limit) {
+ last = total;
+ } else {
+ last = first + limit;
+ }
+ this.$element.find('span.oe-pager-state').empty().text(_.sprintf(
+ "[%d to %d] of %d", first + 1, last, total));
+
+ this.$element
+ .find('button[data-pager-action=first], button[data-pager-action=previous]')
+ .attr('disabled', this.page === 0)
+ .end()
+ .find('button[data-pager-action=last], button[data-pager-action=next]')
+ .attr('disabled', last === total);
+ },
+ /**
+ * Sets up the listview's columns: merges view and fields data, move
+ * grouped-by columns to the front of the columns list and make them all
+ * visible.
+ *
+ * @param {Object} fields fields_view_get's fields section
+ * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
+ */
+ setup_columns: function (fields, grouped) {
+ var domain_computer = openerp.web.form.compute_domain;
+
+ var noop = function () { return {}; };
+ var field_to_column = function (field) {
+ var name = field.attrs.name;
+ var column = _.extend({id: name, tag: field.tag},
+ field.attrs, fields[name]);
+ // modifiers computer
+ if (column.modifiers) {
+ var modifiers = JSON.parse(column.modifiers);
+ column.modifiers_for = function (fields) {
+ if (!modifiers.invisible) {
+ return {};
+ }
+ return {
+ 'invisible': domain_computer(modifiers.invisible, fields)
+ };
+ };
+ if (modifiers['tree_invisible']) {
+ column.invisible = '1';
+ }
+ } else {
+ column.modifiers_for = noop;
+ }
+ return column;
+ };
+
+ this.columns.splice(0, this.columns.length);
+ this.columns.push.apply(
+ this.columns,
+ _(this.fields_view.arch.children).map(field_to_column));
+ if (grouped) {
+ this.columns.unshift({
+ id: '_group', tag: '', string: "Group", meta: true,
+ modifiers_for: function () { return {}; }
+ }, {
+ id: '_count', tag: '', string: '#', meta: true,
+ modifiers_for: function () { return {}; }
+ });
+ }
+
+ this.visible_columns = _.filter(this.columns, function (column) {
+ return column.invisible !== '1';
+ });
+
+ this.aggregate_columns = _(this.visible_columns)
+ .map(function (column) {
+ if (column.type !== 'integer' && column.type !== 'float') {
+ return {};
+ }
+ var aggregation_func = column['group_operator'] || 'sum';
+
+ return _.extend({}, column, {
+ 'function': aggregation_func,
+ label: column[aggregation_func]
+ });
+ });
+ },
+ /**
+ * Used to handle a click on a table row, if no other handler caught the
+ * event.
+ *
+ * The default implementation asks the list view's view manager to switch
+ * to a different view (by calling
+ * :js:func:`~openerp.web.ViewManager.on_mode_switch`), using the
+ * provided record index (within the current list view's dataset).
+ *
+ * If the index is null, ``switch_to_record`` asks for the creation of a
+ * new record.
+ *
+ * @param {Number|void} index the record index (in the current dataset) to switch to
+ * @param {String} [view="form"] the view type to switch to
+ */
+ select_record:function (index, view) {
+ view = view || 'form';
+ this.dataset.index = index;
+ _.delay(_.bind(function () {
+ this.do_switch_view(view);
+ }, this));
+ },
+ do_show: function () {
+ this.$element.show();
+ if (this.sidebar) {
+ this.sidebar.$element.show();
+ }
+ if (!_(this.dataset.ids).isEmpty()) {
+ this.reload_content();
+ }
+ },
+ do_hide: function () {
+ this.$element.hide();
+ if (this.sidebar) {
+ this.sidebar.$element.hide();
+ }
+ },
+ /**
+ * Reloads the list view based on the current settings (dataset & al)
+ *
+ * @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) {
+ var self = this;
+ var callback = function (field_view_get) {
+ self.on_loaded(field_view_get, grouped);
+ };
+ if (this.embedded_view) {
+ return $.Deferred().then(callback).resolve(this.embedded_view);
+ } else {
+ return this.rpc('/web/listview/load', {
+ model: this.model,
+ view_id: this.view_id,
+ view_type: "tree",
+ context: this.dataset.get_context(context),
+ toolbar: this.options.sidebar
+ }, callback);
+ }
+ },
+ /**
+ * re-renders the content of the list view
+ */
+ reload_content: function () {
+ var self = this;
+ this.records.reset();
+ this.$element.find('.oe-listview-content').append(
+ this.groups.render(function () {
+ if (self.dataset.index == null) {
+ var has_one = false;
+ self.records.each(function () { has_one = true; });
+ if (has_one) {
+ self.dataset.index = 0;
+ }
+ }
+ self.compute_aggregates();
+ }));
+ },
+ /**
+ * Event handler for a search, asks for the computation/folding of domains
+ * and contexts (and group-by), then reloads the view's content.
+ *
+ * @param {Array} domains a sequence of literal and non-literal domains
+ * @param {Array} contexts a sequence of literal and non-literal contexts
+ * @param {Array} groupbys a sequence of literal and non-literal group-by contexts
+ * @returns {$.Deferred} fold request evaluation promise
+ */
+ do_search: function (domains, contexts, groupbys) {
+ return this.rpc('/web/session/eval_domain_and_context', {
+ domains: [this.dataset.get_domain()].concat(domains),
+ contexts: [this.dataset.get_context()].concat(contexts),
+ group_by_seq: groupbys
+ }, $.proxy(this, 'do_actual_search'));
+ },
+ /**
+ * Handler for the result of eval_domain_and_context, actually perform the
+ * searching
+ *
+ * @param {Object} results results of evaluating domain and process for a search
+ */
+ do_actual_search: function (results) {
+ this.groups.datagroup = new openerp.web.DataGroup(
+ this, this.model,
+ results.domain,
+ results.context,
+ results.group_by);
+ this.groups.datagroup.sort = this.dataset._sort;
+
+ if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
+ results.group_by = null;
+ }
+
+ this.reload_view(!!results.group_by, results.context).then(
+ $.proxy(this, 'reload_content'));
+ },
+ /**
+ * Handles the signal to delete lines from the records list
+ *
+ * @param {Array} ids the ids of the records to delete
+ */
+ do_delete: function (ids) {
+ if (!ids.length) {
+ return;
+ }
+ var self = this;
+ return $.when(this.dataset.unlink(ids)).then(function () {
+ _(ids).each(function (id) {
+ self.records.remove(self.records.get(id));
+ });
+ self.compute_aggregates();
+ });
+ },
+ /**
+ * Handles the signal indicating that a new record has been selected
+ *
+ * @param {Array} ids selected record ids
+ * @param {Array} records selected record values
+ */
+ do_select: function (ids, records) {
+ this.$element.find('.oe-list-delete')
+ .attr('disabled', !ids.length);
+
+ if (!records.length) {
+ this.compute_aggregates();
+ return;
+ }
+ this.compute_aggregates(_(records).map(function (record) {
+ return {count: 1, values: record};
+ }));
+ },
+ /**
+ * Handles action button signals on a record
+ *
+ * @param {String} name action name
+ * @param {Object} id id of the record the action should be called on
+ * @param {Function} callback should be called after the action is executed, if non-null
+ */
+ do_button_action: function (name, id, callback) {
+ var action = _.detect(this.columns, function (field) {
+ return field.name === name;
+ });
+ if (!action) { return; }
+ this.execute_action(action, this.dataset, id, callback);
+ },
+ /**
+ * Handles the activation of a record (clicking on it)
+ *
+ * @param {Number} index index of the record in the dataset
+ * @param {Object} id identifier of the activated record
+ * @param {openerp.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
+ */
+ do_activate_record: function (index, id, dataset) {
+ var self = this;
+ // TODO is it needed ?
+ this.dataset.read_slice([],{
+ context: dataset.get_context(),
+ domain: dataset.get_domain()
+ }, function () {
+ self.select_record(index);
+ });
+ },
+ /**
+ * Handles signal for the addition of a new record (can be a creation,
+ * can be the addition from a remote source, ...)
+ *
+ * The default implementation is to switch to a new record on the form view
+ */
+ do_add_record: function () {
+ this.select_record(null);
+ },
+ /**
+ * Handles deletion of all selected lines
+ */
+ do_delete_selected: function () {
+ this.do_delete(this.groups.get_selection().ids);
+ },
+ /**
+ * Computes the aggregates for the current list view, either on the
+ * records provided or on the records of the internal
+ * :js:class:`~openerp.web.ListView.Group`, by calling
+ * :js:func:`~openerp.web.ListView.group.get_records`.
+ *
+ * Then displays the aggregates in the table through
+ * :js:method:`~openerp.web.ListView.display_aggregates`.
+ *
+ * @param {Array} [records]
+ */
+ compute_aggregates: function (records) {
+ var columns = _(this.aggregate_columns).filter(function (column) {
+ return column['function']; });
+ if (_.isEmpty(columns)) { return; }
+
+ if (_.isEmpty(records)) {
+ records = this.groups.get_records();
+ }
+
+ var count = 0, sums = {};
+ _(columns).each(function (column) {
+ switch (column['function']) {
+ case 'max':
+ sums[column.id] = -Infinity;
+ break;
+ case 'min':
+ sums[column.id] = Infinity;
+ break;
+ default:
+ sums[column.id] = 0;
+ }
+ });
+ _(records).each(function (record) {
+ count += record.count || 1;
+ _(columns).each(function (column) {
+ var field = column.id,
+ value = record.values[field];
+ switch (column['function']) {
+ case 'sum':
+ sums[field] += value;
+ break;
+ case 'avg':
+ sums[field] += record.count * value;
+ break;
+ case 'min':
+ if (sums[field] > value) {
+ sums[field] = value;
+ }
+ break;
+ case 'max':
+ if (sums[field] < value) {
+ sums[field] = value;
+ }
+ break;
+ }
+ });
+ });
+
+ var aggregates = {};
+ _(columns).each(function (column) {
+ var field = column.id;
+ switch (column['function']) {
+ case 'avg':
+ aggregates[field] = {value: sums[field] / count};
+ break;
+ default:
+ aggregates[field] = {value: sums[field]};
+ }
+ });
+
+ this.display_aggregates(aggregates);
+ },
+ display_aggregates: function (aggregation) {
+ var $footer_cells = this.$element.find('.oe-list-footer');
+ _(this.aggregate_columns).each(function (column) {
+ if (!column['function']) {
+ return;
+ }
+
+ $footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
+ .html(openerp.web.format_cell(aggregation, column));
+ });
+ },
+ get_selected_ids: function() {
+ var ids = this.groups.get_selection().ids;
+ return ids;
+ },
+ /**
+ * Adds padding columns at the start or end of all table rows (including
+ * field names row)
+ *
+ * @param {Number} count number of columns to add
+ * @param {Object} options
+ * @param {"before"|"after"} [position="after"] insertion position for the new columns
+ * @param {Object} [except] content row to not pad
+ */
+ pad_columns: function (count, options) {
+ options = options || {};
+ // padding for action/pager header
+ var $first_header = this.$element.find('thead tr:first th');
+ var colspan = $first_header.attr('colspan');
+ if (colspan) {
+ if (!this.previous_colspan) {
+ this.previous_colspan = colspan;
+ }
+ $first_header.attr('colspan', parseInt(colspan, 10) + count);
+ }
+ // Padding for column titles, footer and data rows
+ var $rows = this.$element
+ .find('.oe-listview-header-columns, tr:not(thead tr)')
+ .not(options['except']);
+ var newcols = new Array(count+1).join('<td class="oe-listview-padding"></td>');
+ if (options.position === 'before') {
+ $rows.prepend(newcols);
+ } else {
+ $rows.append(newcols);
+ }
+ },
+ /**
+ * Removes all padding columns of the table
+ */
+ unpad_columns: function () {
+ this.$element.find('.oe-listview-padding').remove();
+ if (this.previous_colspan) {
+ this.$element
+ .find('thead tr:first th')
+ .attr('colspan', this.previous_colspan);
+ this.previous_colspan = null;
+ }
+ }
+});
+openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.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
+ * other consumers) can subscribe.
+ *
+ * Events on this object are registered via jQuery.
+ *
+ * Available events:
+ *
+ * `selected`
+ * Triggered when a row is selected (using check boxes), provides an
+ * array of ids of all the selected records.
+ * `deleted`
+ * Triggered when deletion buttons are hit, provide an array of ids of
+ * all the records being marked for suppression.
+ * `action`
+ * Triggered when an action button is clicked, provides two parameters:
+ *
+ * * The name of the action to execute (as a string)
+ * * The id of the record to execute the action on
+ * `row_link`
+ * Triggered when a row of the table is clicked, provides the index (in
+ * the rows array) and id of the selected record to the handle function.
+ *
+ * @constructs openerp.web.ListView.List
+ * @extends openerp.web.Class
+ *
+ * @param {Object} opts display options, identical to those of :js:class:`openerp.web.ListView`
+ */
+ init: function (group, opts) {
+ var self = this;
+ this.group = group;
+ this.view = group.view;
+ this.session = this.view.session;
+
+ this.options = opts.options;
+ this.columns = opts.columns;
+ this.dataset = opts.dataset;
+ this.records = opts.records;
+
+ this.record_callbacks = {
+ 'remove': function (event, record) {
+ var $row = self.$current.find(
+ '[data-id=' + record.get('id') + ']');
+ var index = $row.data('index');
+ $row.remove();
+ self.refresh_zebra(index);
+ },
+ 'reset': $.proxy(this, 'on_records_reset'),
+ 'change': function (event, record) {
+ var $row = self.$current.find('[data-id=' + record.get('id') + ']');
+ $row.replaceWith(self.render_record(record));
+ },
+ 'add': function (ev, records, record, index) {
+ var $new_row = $('<tr>').attr({
+ 'data-id': record.get('id')
+ });
+
+ if (index === 0) {
+ $new_row.prependTo(self.$current);
+ } else {
+ var previous_record = records.at(index-1),
+ $previous_sibling = self.$current.find(
+ '[data-id=' + previous_record.get('id') + ']');
+ $new_row.insertAfter($previous_sibling);
+ }
+
+ self.refresh_zebra(index, 1);
+ }
+ };
+ _(this.record_callbacks).each(function (callback, event) {
+ this.records.bind(event, callback);
+ }, this);
+
+ this.$_element = $('<tbody class="ui-widget-content">')
+ .appendTo(document.body)
+ .delegate('th.oe-record-selector', 'click', function (e) {
+ e.stopPropagation();
+ var selection = self.get_selection();
+ $(self).trigger(
+ 'selected', [selection.ids, selection.records]);
+ })
+ .delegate('td.oe-record-delete button', 'click', function (e) {
+ e.stopPropagation();
+ var $row = $(e.target).closest('tr');
+ $(self).trigger('deleted', [[self.row_id($row)]]);
+ })
+ .delegate('td.oe-field-cell button', 'click', function (e) {
+ e.stopPropagation();
+ var $target = $(e.currentTarget),
+ field = $target.closest('td').data('field'),
+ $row = $target.closest('tr'),
+ record_id = self.row_id($row);
+
+ $(self).trigger('action', [field, record_id, function () {
+ return self.reload_record(self.records.get(record_id));
+ }]);
+ })
+ .delegate('tr', 'click', function (e) {
+ e.stopPropagation();
+ self.dataset.index = self.records.indexOf(
+ self.records.get(
+ self.row_id(e.currentTarget)));
+ self.row_clicked(e);
+ });
+ },
+ row_clicked: function () {
+ $(this).trigger(
+ 'row_link',
+ [this.records.at(this.dataset.index).get('id'),
+ this.dataset]);
+ },
+ render: function () {
+ if (this.$current) {
+ this.$current.remove();
+ }
+ this.$current = this.$_element.clone(true);
+ this.$current.empty().append(
+ QWeb.render('ListView.rows', _.extend({
+ render_cell: openerp.web.format_cell}, this)));
+ },
+ /**
+ * Gets the ids of all currently selected records, if any
+ * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves.
+ */
+ get_selection: function () {
+ if (!this.options.selectable) {
+ return [];
+ }
+ var records = this.records;
+ var result = {ids: [], records: []};
+ this.$current.find('th.oe-record-selector input:checked')
+ .closest('tr').each(function () {
+ var record = records.get($(this).data('id'));
+ result.ids.push(record.get('id'));
+ result.records.push(record.attributes);
+ });
+ return result;
+ },
+ /**
+ * Returns the identifier of the object displayed in the provided table
+ * row
+ *
+ * @param {Object} row the selected table row
+ * @returns {Number|String} the identifier of the row's object
+ */
+ row_id: function (row) {
+ return $(row).data('id');
+ },
+ /**
+ * Death signal, cleans up list display
+ */
+ on_records_reset: function () {
+ _(this.record_callbacks).each(function (callback, event) {
+ this.records.unbind(event, callback);
+ }, this);
+ if (!this.$current) { return; }
+ this.$current.remove();
+ this.$current = null;
+ this.$_element.remove();
+ },
+ get_records: function () {
+ return this.records.map(function (record) {
+ return {count: 1, values: record.attributes};
+ });
+ },
+ /**
+ * Reloads the provided record by re-reading its content from the server.
+ *
+ * @param {Record} record
+ * @returns {$.Deferred} promise to the finalization of the reloading
+ */
+ reload_record: function (record) {
+ return this.dataset.read_ids(
+ [record.get('id')],
+ _.pluck(_(this.columns).filter(function (r) {
+ return r.tag === 'field';
+ }), 'name'),
+ function (records) {
+ _(records[0]).each(function (value, key) {
+ record.set(key, value, {silent: true});
+ });
+ record.trigger('change', record);
+ }
+ );
+ },
+ /**
+ * Renders a list record to HTML
+ *
+ * @param {Record} record index of the record to render in ``this.rows``
+ * @returns {String} QWeb rendering of the selected record
+ */
+ render_record: function (record) {
+ var index = this.records.indexOf(record);
+ return QWeb.render('ListView.row', {
+ columns: this.columns,
+ options: this.options,
+ record: record,
+ row_parity: (index % 2 === 0) ? 'even' : 'odd',
+ render_cell: openerp.web.format_cell
+ });
+ },
+ /**
+ * Fixes fixes the even/odd classes
+ *
+ * @param {Number} [from_index] index from which to resequence
+ * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table
+ */
+ refresh_zebra: function (from_index, offset) {
+ offset = offset || 0;
+ from_index = from_index || 0;
+ var dom_offset = offset + from_index;
+ var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null;
+ this.$current.children(sel).each(function (i, e) {
+ var index = from_index + i;
+ // reset record-index accelerators on rows and even/odd
+ var even = index%2 === 0;
+ $(e).toggleClass('even', even)
+ .toggleClass('odd', !even);
+ });
+ }
+});
+openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.ListView.Groups# */{
+ passtrough_events: 'action deleted row_link',
+ /**
+ * Grouped display for the ListView. Handles basic DOM events and interacts
+ * with the :js:class:`~openerp.web.DataGroup` bound to it.
+ *
+ * Provides events similar to those of
+ * :js:class:`~openerp.web.ListView.List`
+ *
+ * @constructs openerp.web.ListView.Groups
+ * @extends openerp.web.Class
+ *
+ * @param {openerp.web.ListView} view
+ * @param {Object} [options]
+ * @param {Collection} [options.records]
+ * @param {Object} [options.options]
+ * @param {Array} [options.columns]
+ */
+ init: function (view, options) {
+ options = options || {};
+ this.view = view;
+ this.records = options.records || view.records;
+ this.options = options.options || view.options;
+ this.columns = options.columns || view.columns;
+ this.datagroup = null;
+
+ this.$row = null;
+ this.children = {};
+
+ this.page = 0;
+
+ this.records.bind('reset', $.proxy(this, 'on_records_reset'));
+ },
+ make_fragment: function () {
+ return document.createDocumentFragment();
+ },
+ /**
+ * Returns a DOM node after which a new tbody can be inserted, so that it
+ * follows the provided row.
+ *
+ * Necessary to insert the result of a new group or list view within an
+ * existing groups render, without losing track of the groups's own
+ * elements
+ *
+ * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body
+ * @returns {HTMLTableSectionElement} element after which a tbody can be inserted
+ */
+ point_insertion: function (row) {
+ var $row = $(row);
+ var red_letter_tboday = $row.closest('tbody')[0];
+
+ var $next_siblings = $row.nextAll();
+ if ($next_siblings.length) {
+ var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday);
+
+ $root_kanal.append($next_siblings);
+ this.elements.splice(
+ _.indexOf(this.elements, red_letter_tboday),
+ 0,
+ $root_kanal[0]);
+ }
+ return red_letter_tboday;
+ },
+ make_paginator: function () {
+ var self = this;
+ var $prev = $('<button type="button" data-pager-action="previous">&lt;</button>')
+ .click(function (e) {
+ e.stopPropagation();
+ self.page -= 1;
+
+ self.$row.closest('tbody').next()
+ .replaceWith(self.render());
+ });
+ var $next = $('<button type="button" data-pager-action="next">&gt;</button>')
+ .click(function (e) {
+ e.stopPropagation();
+ self.page += 1;
+
+ self.$row.closest('tbody').next()
+ .replaceWith(self.render());
+ });
+ this.$row.children().last()
+ .append($prev)
+ .append('<span class="oe-pager-state"></span>')
+ .append($next);
+ },
+ open: function (point_insertion) {
+ this.render().insertAfter(point_insertion);
+ this.make_paginator();
+ },
+ close: function () {
+ this.$row.children().last().empty();
+ this.records.reset();
+ },
+ /**
+ * Prefixes ``$node`` with floated spaces in order to indent it relative
+ * to its own left margin/baseline
+ *
+ * @param {jQuery} $node jQuery object to indent
+ * @param {Number} level current nesting level, >= 1
+ * @returns {jQuery} the indentation node created
+ */
+ indent: function ($node, level) {
+ return $('<span>')
+ .css({'float': 'left', 'white-space': 'pre'})
+ .text(new Array(level).join(' '))
+ .prependTo($node);
+ },
+ render_groups: function (datagroups) {
+ var self = this;
+ var placeholder = this.make_fragment();
+ _(datagroups).each(function (group) {
+ if (self.children[group.value]) {
+ self.records.proxy(group.value).reset();
+ delete self.children[group.value];
+ }
+ var child = self.children[group.value] = new openerp.web.ListView.Groups(self.view, {
+ records: self.records.proxy(group.value),
+ options: self.options,
+ columns: self.columns
+ });
+ self.bind_child_events(child);
+ child.datagroup = group;
+
+ var $row = child.$row = $('<tr>');
+ if (group.openable) {
+ $row.click(function (e) {
+ if (!$row.data('open')) {
+ $row.data('open', true)
+ .find('span.ui-icon')
+ .removeClass('ui-icon-triangle-1-e')
+ .addClass('ui-icon-triangle-1-s');
+ child.open(self.point_insertion(e.currentTarget));
+ } else {
+ $row.removeData('open')
+ .find('span.ui-icon')
+ .removeClass('ui-icon-triangle-1-s')
+ .addClass('ui-icon-triangle-1-e');
+ child.close();
+ }
+ });
+ }
+ placeholder.appendChild($row[0]);
+
+ var $group_column = $('<th class="oe-group-name">').appendTo($row);
+ // Don't fill this if group_by_no_leaf but no group_by
+ if (group.grouped_on) {
+ var row_data = {};
+ row_data[group.grouped_on] = group;
+ var group_column = _(self.columns).detect(function (column) {
+ return column.id === group.grouped_on; });
+ $group_column.html(openerp.web.format_cell(
+ row_data, group_column, "Undefined"
+ ));
+ if (group.openable) {
+ // Make openable if not terminal group & group_by_no_leaf
+ $group_column
+ .prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
+ }
+ }
+ self.indent($group_column, group.level);
+ // count column
+ $('<td>').text(group.length).appendTo($row);
+
+ if (self.options.selectable) {
+ $row.append('<td>');
+ }
+ _(self.columns).chain()
+ .filter(function (column) {return !column.invisible;})
+ .each(function (column) {
+ if (column.meta) {
+ // do not do anything
+ } else if (column.id in group.aggregates) {
+ var value = group.aggregates[column.id];
+ var format;
+ if (column.type === 'integer') {
+ format = "%.0f";
+ } else if (column.type === 'float') {
+ format = "%.2f";
+ }
+ $('<td>')
+ .text(_.sprintf(format, value))
+ .appendTo($row);
+ } else {
+ $row.append('<td>');
+ }
+ });
+ if (self.options.deletable) {
+ $row.append('<td class="oe-group-pagination">');
+ }
+ });
+ return placeholder;
+ },
+ bind_child_events: function (child) {
+ var $this = $(this),
+ self = this;
+ $(child).bind('selected', function (e) {
+ // can have selections spanning multiple links
+ var selection = self.get_selection();
+ $this.trigger(e, [selection.ids, selection.records]);
+ }).bind(this.passtrough_events, function (e) {
+ // additional positional parameters are provided to trigger as an
+ // Array, following the event type or event object, but are
+ // provided to the .bind event handler as *args.
+ // Convert our *args back into an Array in order to trigger them
+ // on the group itself, so it can ultimately be forwarded wherever
+ // it's supposed to go.
+ var args = Array.prototype.slice.call(arguments, 1);
+ $this.trigger.call($this, e, args);
+ });
+ },
+ render_dataset: function (dataset) {
+ var self = this,
+ list = new openerp.web.ListView.List(this, {
+ options: this.options,
+ columns: this.columns,
+ dataset: dataset,
+ records: this.records
+ });
+ this.bind_child_events(list);
+
+ var view = this.view,
+ limit = view.limit(),
+ d = new $.Deferred(),
+ page = this.datagroup.openable ? this.page : view.page;
+
+ var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name');
+ var options = { offset: page * limit, limit: limit };
+ dataset.read_slice(fields, options , function (records) {
+ if (!self.datagroup.openable) {
+ view.configure_pager(dataset);
+ } else {
+ var pages = Math.ceil(dataset.ids.length / limit);
+ self.$row
+ .find('.oe-pager-state')
+ .text(_.sprintf('%d/%d', page + 1, pages))
+ .end()
+ .find('button[data-pager-action=previous]')
+ .attr('disabled', page === 0)
+ .end()
+ .find('button[data-pager-action=next]')
+ .attr('disabled', page === pages - 1);
+ }
+
+ self.records.add(records, {silent: true});
+ list.render();
+ d.resolve(list);
+ });
+ return d.promise();
+ },
+ setup_resequence_rows: function (list, dataset) {
+ // drag and drop enabled if list is not sorted and there is a
+ // "sequence" column in the view.
+ if ((dataset.sort && dataset.sort())
+ || !_(this.columns).any(function (column) {
+ return column.name === 'sequence'; })) {
+ return;
+ }
+ // ondrop, move relevant record & fix sequences
+ list.$current.sortable({
+ stop: function (event, ui) {
+ var to_move = list.records.get(ui.item.data('id')),
+ target_id = ui.item.prev().data('id');
+
+ list.records.remove(to_move);
+ var to = target_id ? list.records.indexOf(list.records.get(target_id)) : 0;
+ list.records.add(to_move, { at: to });
+
+ // resequencing time!
+ var record, index = to,
+ // if drag to 1st row (to = 0), start sequencing from 0
+ // (exclusive lower bound)
+ seq = to ? list.records.at(to - 1).get('sequence') : 0;
+ while (++seq, record = list.records.at(index++)) {
+ // write are independent from one another, so we can just
+ // launch them all at the same time and we don't really
+ // give a fig about when they're done
+ dataset.write(record.get('id'), {sequence: seq});
+ record.set('sequence', seq);
+ }
+
+ list.refresh_zebra();
+ }
+ });
+ },
+ render: function (post_render) {
+ var self = this;
+ var $element = $('<tbody>');
+ this.elements = [$element[0]];
+
+ this.datagroup.list(
+ _(this.view.visible_columns).chain()
+ .filter(function (column) { return column.tag === 'field' })
+ .pluck('name').value(),
+ function (groups) {
+ $element[0].appendChild(
+ self.render_groups(groups));
+ if (post_render) { post_render(); }
+ }, function (dataset) {
+ self.render_dataset(dataset).then(function (list) {
+ self.children[null] = list;
+ self.elements =
+ [list.$current.replaceAll($element)[0]];
+ self.setup_resequence_rows(list, dataset);
+ if (post_render) { post_render(); }
+ });
+ });
+ return $element;
+ },
+ /**
+ * Returns the ids of all selected records for this group, and the records
+ * themselves
+ */
+ get_selection: function () {
+ var ids = [], records = [];
+
+ _(this.children)
+ .each(function (child) {
+ var selection = child.get_selection();
+ ids.push.apply(ids, selection.ids);
+ records.push.apply(records, selection.records);
+ });
+
+ return {ids: ids, records: records};
+ },
+ on_records_reset: function () {
+ this.children = {};
+ $(this.elements).remove();
+ },
+ get_records: function () {
+ if (_(this.children).isEmpty()) {
+ return {
+ count: this.datagroup.length,
+ values: this.datagroup.aggregates
+ }
+ }
+ return _(this.children).chain()
+ .map(function (child) {
+ return child.get_records();
+ }).flatten().value();
+ }
+});
+
+/**
+ * @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 = openerp.web.Class.extend(/** @lends Record# */{
+ /**
+ * @constructs Record
+ * @extends openerp.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 = {};
+ _(this.attributes).each(function (value, key) {
+ form_data[key] = {value: value};
+ });
+
+ return {data: form_data};
+ }
+});
+Record.include(Events);
+var Collection = openerp.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 openerp.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) {
+ 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) {
+ return record = proxy.get(id);
+ });
+ 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) {
+ return this._proxies[section] = new Collection(null, {
+ parent: this,
+ key: section
+ }).bind(null, this._onRecordEvent);
+ },
+ /**
+ * @param {Array} [records]
+ * @returns this
+ */
+ reset: function (records) {
+ _(this._proxies).each(function (proxy) {
+ proxy.reset();
+ });
+ this._proxies = {};
+ this.length = 0;
+ this.records = [];
+ this._byId = {};
+ if (records) {
+ this.add(records);
+ }
+ this.trigger('reset', this);
+ return this;
+ },
+ /**
+ * Removes the provided record from the collection
+ *
+ * @param {Record} record
+ * @returns this
+ */
+ remove: function (record) {
+ var self = this;
+ var index = _(this.records).indexOf(record);
+ if (index === -1) {
+ _(this._proxies).each(function (proxy) {
+ proxy.remove(record);
+ });
+ return this;
+ }
+
+ this.records.splice(index, 1);
+ delete this._byId[record.get('id')];
+ this.length--;
+ this.trigger('remove', record, this);
+ return this;
+ },
+
+ _onRecordEvent: function (event, record, options) {
+ // don't propagate reset events
+ if (event === 'reset') { return; }
+ this.trigger.apply(this, arguments);
+ },
+
+ // underscore-type methods
+ 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);
+ }
+});
+Collection.include(Events);
+openerp.web.list = {
+ Events: Events,
+ Record: Record,
+ Collection: Collection
+}
+};
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
new file mode 100644
index 00000000000..f6c53b4ad17
--- /dev/null
+++ b/addons/web/static/src/js/search.js
@@ -0,0 +1,1220 @@
+openerp.web.search = function(openerp) {
+var QWeb = openerp.web.qweb;
+
+openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
+ /**
+ * @constructs openerp.web.SearchView
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param element_id
+ * @param dataset
+ * @param view_id
+ * @param defaults
+ */
+ init: function(parent, element_id, dataset, view_id, defaults) {
+ this._super(parent, element_id);
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id;
+
+ this.defaults = defaults || {};
+
+ this.inputs = [];
+ this.enabled_filters = [];
+
+ this.has_focus = false;
+
+ this.ready = $.Deferred();
+ },
+ start: function() {
+ this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
+ return this.ready.promise();
+ },
+ show: function () {
+ this.$element.show();
+ },
+ hide: function () {
+ this.$element.hide();
+ },
+ /**
+ * Builds a list of widget rows (each row is an array of widgets)
+ *
+ * @param {Array} items a list of nodes to convert to widgets
+ * @param {Object} fields a mapping of field names to (ORM) field attributes
+ * @returns Array
+ */
+ make_widgets: function (items, fields) {
+ var rows = [],
+ row = [];
+ rows.push(row);
+ var filters = [];
+ _.each(items, function (item) {
+ if (filters.length && item.tag !== 'filter') {
+ row.push(
+ new openerp.web.search.FilterGroup(
+ filters, this));
+ filters = [];
+ }
+
+ if (item.tag === 'newline') {
+ row = [];
+ rows.push(row);
+ } else if (item.tag === 'filter') {
+ if (!this.has_focus) {
+ item.attrs.default_focus = '1';
+ this.has_focus = true;
+ }
+ filters.push(
+ new openerp.web.search.Filter(
+ item, this));
+ } else if (item.tag === 'separator') {
+ // a separator is a no-op
+ } else {
+ if (item.tag === 'group') {
+ // TODO: group and field should be fetched from registries, maybe even filters
+ row.push(
+ new openerp.web.search.Group(
+ item, this, fields));
+ } else if (item.tag === 'field') {
+ if (!this.has_focus) {
+ item.attrs.default_focus = '1';
+ this.has_focus = true;
+ }
+ row.push(
+ this.make_field(
+ item, fields[item['attrs'].name]));
+ }
+ }
+ }, this);
+ if (filters.length) {
+ row.push(new openerp.web.search.FilterGroup(filters, this));
+ }
+
+ return rows;
+ },
+ /**
+ * Creates a field for the provided field descriptor item (which comes
+ * from fields_view_get)
+ *
+ * @param {Object} item fields_view_get node for the field
+ * @param {Object} field fields_get result for the field
+ * @returns openerp.web.search.Field
+ */
+ make_field: function (item, field) {
+ try {
+ return new (openerp.web.search.fields.get_object(field.type))
+ (item, field, this);
+ } catch (e) {
+ if (! e instanceof openerp.web.KeyNotFound) {
+ throw e;
+ }
+ // KeyNotFound means unknown field type
+ console.group('Unknown field type ' + field.type);
+ console.error('View node', item);
+ console.info('View field', field);
+ console.info('In view', this);
+ console.groupEnd();
+ return null;
+ }
+ },
+ on_loaded: function(data) {
+ var self = this,
+ lines = this.make_widgets(
+ data.fields_view['arch'].children,
+ data.fields_view.fields);
+
+ // for extended search view
+ var ext = new openerp.web.search.ExtendedSearch(this, this.model);
+ lines.push([ext]);
+ this.inputs.push(ext);
+
+ var render = QWeb.render("SearchView", {
+ 'view': data.fields_view['arch'],
+ 'lines': lines,
+ 'defaults': this.defaults
+ });
+
+ // We don't understand why the following commented line does not work in Chrome but
+ // the non-commented line does. As far as we investigated, only God knows.
+ //this.$element.html(render);
+ jQuery(render).appendTo(this.$element);
+ this.$element.find(".oe_search-view-custom-filter-btn").click(ext.on_activate);
+
+ var f = this.$element.find('form');
+ this.$element.find('form')
+ .submit(this.do_search)
+ .bind('reset', this.do_clear);
+ // start() all the widgets
+ var widget_starts = _(lines).chain().flatten().map(function (widget) {
+ return widget.start();
+ }).value();
+
+ $.when.apply(null, widget_starts).then(function () {
+ self.ready.resolve();
+ });
+
+ this.reload_managed_filters();
+ },
+ reload_managed_filters: function() {
+ var self = this;
+ return this.rpc('/web/searchview/get_filters', {
+ model: this.dataset.model
+ }).then(function(result) {
+ self.managed_filters = result;
+ var filters = self.$element.find(".oe_search-view-filters-management");
+ filters.html(QWeb.render("SearchView.managed-filters", {filters: result}));
+ filters.change(self.on_filters_management);
+ });
+ },
+ /**
+ * Handle event when the user make a selection in the filters management select box.
+ */
+ on_filters_management: function(e) {
+ var self = this;
+ var select = this.$element.find(".oe_search-view-filters-management");
+ var val = select.val();
+
+ if (val.slice(0,1) == "_") { // useless action
+ select.val("_filters");
+ return;
+ }
+ if (val.slice(0, "get:".length) == "get:") {
+ val = val.slice("get:".length);
+ val = parseInt(val);
+ var filter = this.managed_filters[val];
+ this.on_search([filter.domain], [filter.context], []);
+ } else if (val == "save_filter") {
+ select.val("_filters");
+ var data = this.build_search_data();
+ var context = new openerp.web.CompoundContext();
+ _.each(data.contexts, function(x) {
+ context.add(x);
+ });
+ var domain = new openerp.web.CompoundDomain();
+ _.each(data.domains, function(x) {
+ domain.add(x);
+ });
+ var dial_html = QWeb.render("SearchView.managed-filters.add");
+ var $dial = $(dial_html);
+ $dial.dialog({
+ modal: true,
+ title: "Filter Entry",
+ buttons: {
+ Cancel: function() {
+ $(this).dialog("close");
+ },
+ OK: function() {
+ $(this).dialog("close");
+ var name = $(this).find("input").val();
+ self.rpc('/web/searchview/save_filter', {
+ model: self.dataset.model,
+ context_to_save: context,
+ domain: domain,
+ name: name
+ }).then(function() {
+ self.reload_managed_filters();
+ });
+ }
+ }
+ });
+ } else { // manage_filters
+ select.val("_filters");
+ this.do_action({
+ res_model: 'ir.filters',
+ views: [[false, 'list'], [false, 'form']],
+ type: 'ir.actions.act_window',
+ context: {"search_default_user_id": this.session.uid,
+ "search_default_model_id": this.dataset.model},
+ target: "current",
+ limit : 80,
+ auto_search : true
+ });
+ }
+ },
+ /**
+ * Performs the search view collection of widget data.
+ *
+ * If the collection went well (all fields are valid), then triggers
+ * :js:func:`openerp.web.SearchView.on_search`.
+ *
+ * If at least one field failed its validation, triggers
+ * :js:func:`openerp.web.SearchView.on_invalid` instead.
+ *
+ * @param e jQuery event object coming from the "Search" button
+ */
+ do_search: function (e) {
+ // reset filters management
+ var select = this.$element.find(".oe_search-view-filters-management");
+ select.val("_filters");
+
+ if (e && e.preventDefault) { e.preventDefault(); }
+
+ var data = this.build_search_data();
+
+ if (data.errors.length) {
+ this.on_invalid(data.errors);
+ return;
+ }
+
+ this.on_search(data.domains, data.contexts, data.groupbys);
+ },
+ build_search_data: function() {
+ var domains = [],
+ contexts = [],
+ errors = [];
+
+ _.each(this.inputs, function (input) {
+ try {
+ var domain = input.get_domain();
+ if (domain) {
+ domains.push(domain);
+ }
+
+ var context = input.get_context();
+ if (context) {
+ contexts.push(context);
+ }
+ } catch (e) {
+ if (e instanceof openerp.web.search.Invalid) {
+ errors.push(e);
+ } else {
+ throw e;
+ }
+ }
+ });
+
+ // TODO: do we need to handle *fields* with group_by in their context?
+ var groupbys = _(this.enabled_filters)
+ .chain()
+ .map(function (filter) { return filter.get_context();})
+ .compact()
+ .value();
+ return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
+ },
+ /**
+ * Triggered after the SearchView has collected all relevant domains and
+ * contexts.
+ *
+ * It is provided with an Array of domains and an Array of contexts, which
+ * may or may not be evaluated (each item can be either a valid domain or
+ * context, or a string to evaluate in order in the sequence)
+ *
+ * It is also passed an array of contexts used for group_by (they are in
+ * the correct order for group_by evaluation, which contexts may not be)
+ *
+ * @event
+ * @param {Array} domains an array of literal domains or domain references
+ * @param {Array} contexts an array of literal contexts or context refs
+ * @param {Array} groupbys ordered contexts which may or may not have group_by keys
+ */
+ on_search: function (domains, contexts, groupbys) {
+ },
+ /**
+ * Triggered after a validation error in the SearchView fields.
+ *
+ * Error objects have three keys:
+ * * ``field`` is the name of the invalid field
+ * * ``value`` is the invalid value
+ * * ``message`` is the (in)validation message provided by the field
+ *
+ * @event
+ * @param {Array} errors a never-empty array of error objects
+ */
+ on_invalid: function (errors) {
+ this.notification.notify("Invalid Search", "triggered from search view");
+ },
+ do_clear: function () {
+ $('.filter_label').removeClass('enabled');
+ var string = $('a.searchview_group_string');
+ _.each(string, function(str){
+ $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded');
+ });
+ this.$element.find('table:last').hide();
+
+ $('.searchview_extended_groups_list').empty();
+ setTimeout(this.on_clear);
+ },
+ /**
+ * Triggered when the search view gets cleared
+ *
+ * @event
+ */
+ on_clear: function () {
+ this.do_search();
+ },
+ /**
+ * Called by a filter propagating its state changes
+ *
+ * @param {openerp.web.search.Filter} filter a filter which got toggled
+ * @param {Boolean} default_enabled filter got enabled through the default values, at render time.
+ */
+ do_toggle_filter: function (filter, default_enabled) {
+ if (default_enabled || filter.is_enabled()) {
+ this.enabled_filters.push(filter);
+ } else {
+ this.enabled_filters = _.without(
+ this.enabled_filters, filter);
+ }
+
+ if (!default_enabled) {
+ // selecting a filter after initial loading automatically
+ // triggers refresh
+ this.$element.find('form').submit();
+ }
+ }
+});
+
+/** @namespace */
+openerp.web.search = {};
+/**
+ * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
+ * find and instantiate its field widgets.
+ */
+openerp.web.search.fields = new openerp.web.Registry({
+ 'char': 'openerp.web.search.CharField',
+ 'text': 'openerp.web.search.CharField',
+ 'boolean': 'openerp.web.search.BooleanField',
+ 'integer': 'openerp.web.search.IntegerField',
+ 'float': 'openerp.web.search.FloatField',
+ 'selection': 'openerp.web.search.SelectionField',
+ 'datetime': 'openerp.web.search.DateTimeField',
+ 'date': 'openerp.web.search.DateField',
+ 'many2one': 'openerp.web.search.ManyToOneField',
+ 'many2many': 'openerp.web.search.CharField',
+ 'one2many': 'openerp.web.search.CharField'
+});
+openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.search.Invalid# */{
+ /**
+ * Exception thrown by search widgets when they hold invalid values,
+ * which they can not return when asked.
+ *
+ * @constructs openerp.web.search.Invalid
+ * @extends openerp.web.Class
+ *
+ * @param field the name of the field holding an invalid value
+ * @param value the invalid value
+ * @param message validation failure message
+ */
+ init: function (field, value, message) {
+ this.field = field;
+ this.value = value;
+ this.message = message;
+ },
+ toString: function () {
+ return ('Incorrect value for field ' + this.field +
+ ': [' + this.value + '] is ' + this.message);
+ }
+});
+openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.search.Widget# */{
+ template: null,
+ /**
+ * Root class of all search widgets
+ *
+ * @constructs openerp.web.search.Widget
+ * @extends openerp.web.Widget
+ *
+ * @param view the ancestor view of this widget
+ */
+ init: function (view) {
+ this.view = view;
+ },
+ /**
+ * Sets and returns a globally unique identifier for the widget.
+ *
+ * If a prefix is specified, the identifier will be appended to it.
+ *
+ * @params prefix prefix sections, empty/falsy sections will be removed
+ */
+ make_id: function () {
+ this.element_id = _.uniqueId(
+ ['search'].concat(
+ _.compact(_.toArray(arguments)),
+ ['']).join('_'));
+ return this.element_id;
+ },
+ /**
+ * "Starts" the widgets. Called at the end of the rendering, this allows
+ * widgets to hook themselves to their view sections.
+ *
+ * On widgets, if they kept a reference to a view and have an element_id,
+ * will fetch and set their root element on $element.
+ */
+ start: function () {
+ this._super();
+ if (this.view && this.element_id) {
+ // id is unique, and no getElementById on elements
+ this.$element = $(document.getElementById(
+ this.element_id));
+ }
+ },
+ /**
+ * "Stops" the widgets. Called when the view destroys itself, this
+ * lets the widgets clean up after themselves.
+ */
+ stop: function () {
+ delete this.view;
+ this._super();
+ },
+ render: function (defaults) {
+ return QWeb.render(
+ this.template, _.extend(this, {
+ defaults: defaults
+ }));
+ }
+});
+openerp.web.search.FilterGroup = openerp.web.search.Widget.extend(/** @lends openerp.web.search.FilterGroup# */{
+ template: 'SearchView.filters',
+ /**
+ * Inclusive group of filters, creates a continuous "button" with clickable
+ * sections (the normal display for filters is to be a self-contained button)
+ *
+ * @constructs openerp.web.search.FilterGroup
+ * @extends openerp.web.search.Widget
+ *
+ * @param {Array<openerp.web.search.Filter>} filters elements of the group
+ * @param {openerp.web.SearchView} view view in which the filters are contained
+ */
+ init: function (filters, view) {
+ this._super(view);
+ this.filters = filters;
+ this.length = filters.length;
+ },
+ start: function () {
+ this._super();
+ _.each(this.filters, function (filter) {
+ filter.start();
+ });
+ }
+});
+openerp.web.search.add_expand_listener = function($root) {
+ $root.find('a.searchview_group_string').click(function (e) {
+ $root.toggleClass('folded expanded');
+ e.stopPropagation();
+ e.preventDefault();
+ });
+};
+openerp.web.search.Group = openerp.web.search.Widget.extend({
+ template: 'SearchView.group',
+ init: function (view_section, view, fields) {
+ this._super(view);
+ this.attrs = view_section.attrs;
+ this.lines = view.make_widgets(
+ view_section.children, fields);
+ this.make_id('group');
+ },
+ start: function () {
+ this._super();
+ openerp.web.search.add_expand_listener(this.$element);
+ var widget_starts = _(this.lines).chain().flatten()
+ .map(function (widget) { return widget.start(); })
+ .value();
+ return $.when.apply(null, widget_starts);
+ }
+});
+
+openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.web.search.Input# */{
+ /**
+ * @constructs openerp.web.search.Input
+ * @extends openerp.web.search.Widget
+ *
+ * @param view
+ */
+ init: function (view) {
+ this._super(view);
+ this.view.inputs.push(this);
+ },
+ get_context: function () {
+ throw new Error(
+ "get_context not implemented for widget " + this.attrs.type);
+ },
+ get_domain: function () {
+ throw new Error(
+ "get_domain not implemented for widget " + this.attrs.type);
+ }
+});
+openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.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)
+ *
+ * @constructs openerp.web.search.Filter
+ * @extends openerp.web.search.Input
+ *
+ * @param node
+ * @param view
+ */
+ init: function (node, view) {
+ this._super(view);
+ this.attrs = node.attrs;
+ this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
+ this.make_id('filter', this.attrs.name);
+ },
+ start: function () {
+ this._super();
+ var self = this;
+ this.$element.click(function (e) {
+ $(this).toggleClass('enabled');
+ self.view.do_toggle_filter(self);
+ });
+ },
+ /**
+ * Returns whether the filter is currently enabled (in use) or not.
+ *
+ * @returns a boolean
+ */
+ is_enabled:function () {
+ return this.$element.hasClass('enabled');
+ },
+ /**
+ * If the filter is present in the defaults (and has a truthy value),
+ * enable the filter.
+ *
+ * @param {Object} defaults the search view's default values
+ */
+ render: function (defaults) {
+ if (this.attrs.name && defaults[this.attrs.name]) {
+ this.classes.push('enabled');
+ this.view.do_toggle_filter(this, true);
+ }
+ return this._super(defaults);
+ },
+ get_context: function () {
+ if (!this.is_enabled()) {
+ return;
+ }
+ return this.attrs.context;
+ },
+ get_domain: function () {
+ if (!this.is_enabled()) {
+ return;
+ }
+ return this.attrs.domain;
+ }
+});
+openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
+ template: 'SearchView.field',
+ default_operator: '=',
+ /**
+ * @constructs openerp.web.search.Field
+ * @extends openerp.web.search.Input
+ *
+ * @param view_section
+ * @param field
+ * @param view
+ */
+ init: function (view_section, field, view) {
+ this._super(view);
+ this.attrs = _.extend({}, field, view_section.attrs);
+ this.filters = new openerp.web.search.FilterGroup(_.map(
+ view_section.children, function (filter_node) {
+ return new openerp.web.search.Filter(
+ filter_node, view);
+ }), view);
+ this.make_id('input', field.type, this.attrs.name);
+ },
+ start: function () {
+ this._super();
+ this.filters.start();
+ },
+ get_context: function () {
+ var val = this.get_value();
+ // A field needs a value to be "active", and a context to send when
+ // active
+ var has_value = (val !== null && val !== '');
+ var context = this.attrs.context;
+ if (!(has_value && context)) {
+ return;
+ }
+ return _.extend(
+ {}, context,
+ {own_values: {self: val}});
+ },
+ /**
+ * 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} value parsed value for the field
+ * @returns {Array<Array>} domain to include in the resulting search
+ */
+ make_domain: function (name, operator, value) {
+ return [[name, operator, value]];
+ },
+ get_domain: function () {
+ var val = this.get_value();
+ if (val === null || val === '') {
+ return;
+ }
+
+ var domain = this.attrs['filter_domain'];
+ if (!domain) {
+ return this.make_domain(
+ this.attrs.name,
+ this.attrs.operator || this.default_operator,
+ val);
+ }
+ return _.extend({}, domain, {own_values: {self: val}});
+ }
+});
+/**
+ * Implementation of the ``char`` OpenERP field type:
+ *
+ * * Default operator is ``ilike`` rather than ``=``
+ *
+ * * The Javascript and the HTML values are identical (strings)
+ *
+ * @class
+ * @extends openerp.web.search.Field
+ */
+openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ {
+ default_operator: 'ilike',
+ get_value: function () {
+ return this.$element.val();
+ }
+});
+openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{
+ get_value: function () {
+ if (!this.$element.val()) {
+ return null;
+ }
+ var val = this.parse(this.$element.val()),
+ check = Number(this.$element.val());
+ if (isNaN(val) || val !== check) {
+ this.$element.addClass('error');
+ throw new openerp.web.search.Invalid(
+ this.attrs.name, this.$element.val(), this.error_message);
+ }
+ this.$element.removeClass('error');
+ return val;
+ }
+});
+/**
+ * @class
+ * @extends openerp.web.search.NumberField
+ */
+openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{
+ error_message: "not a valid integer",
+ parse: function (value) {
+ return parseInt(value, 10);
+ }
+});
+/**
+ * @class
+ * @extends openerp.web.search.NumberField
+ */
+openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{
+ error_message: "not a valid number",
+ parse: function (value) {
+ return parseFloat(value);
+ }
+});
+/**
+ * @class
+ * @extends openerp.web.search.Field
+ */
+openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
+ template: 'SearchView.field.selection',
+ get_value: function () {
+ return this.$element.val();
+ }
+});
+openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{
+ /**
+ * @constructs openerp.web.search.BooleanField
+ * @extends openerp.web.search.BooleanField
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.attrs.selection = [
+ ['true', 'Yes'],
+ ['false', 'No']
+ ];
+ },
+ /**
+ * Search defaults likely to be boolean values (for a boolean field).
+ *
+ * In the HTML, we only want/get strings, and our strings here are ``true``
+ * and ``false``, so ensure we use precisely those by truth-testing the
+ * default value (iif there is one in the view's defaults).
+ *
+ * @param {Object} defaults default values for this search view
+ * @returns {String} rendered boolean field
+ */
+ render: function (defaults) {
+ var name = this.attrs.name;
+ if (name in defaults) {
+ defaults[name] = defaults[name] ? "true" : "false";
+ }
+ return this._super(defaults);
+ },
+ get_value: function () {
+ switch (this.$element.val()) {
+ case 'false': return false;
+ case 'true': return true;
+ default: return null;
+ }
+ }
+});
+/**
+ * @class
+ * @extends openerp.web.search.DateField
+ */
+openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{
+ /**
+ * enables date picker on the HTML widgets
+ */
+ start: function () {
+ this._super();
+ this.$element.addClass('field_date').datepicker({
+ dateFormat: 'yy-mm-dd'
+ });
+ },
+ stop: function () {
+ this.$element.datepicker('destroy');
+ },
+ get_value: function () {
+ return this.$element.val();
+ }
+});
+/**
+ * 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 openerp.web.DateField
+ */
+openerp.web.search.DateTimeField = openerp.web.search.DateField.extend(/** @lends openerp.web.search.DateTimeField# */{
+ make_domain: function (name, operator, value) {
+ return ['&', [name, '>=', value + ' 00:00:00'],
+ [name, '<=', value + ' 23:59:59']];
+ }
+});
+openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({
+ init: function (view_section, field, view) {
+ this._super(view_section, field, view);
+ var self = this;
+ this.got_name = $.Deferred().then(function () {
+ self.$element.val(self.name);
+ });
+ this.dataset = new openerp.web.DataSet(
+ this.view, this.attrs['relation']);
+ },
+ start: function () {
+ this._super();
+ this.setup_autocomplete();
+ var started = $.Deferred();
+ this.got_name.then(function () { started.resolve();},
+ function () { started.resolve(); });
+ return started.promise();
+ },
+ setup_autocomplete: function () {
+ var self = this;
+ this.$element.autocomplete({
+ source: function (req, resp) {
+ self.dataset.name_search(
+ req.term, self.attrs.domain, 'ilike', 8, function (data) {
+ resp(_.map(data, function (result) {
+ return {id: result[0], label: result[1]}
+ }));
+ });
+ },
+ select: function (event, ui) {
+ self.id = ui.item.id;
+ self.name = ui.item.label;
+ },
+ delay: 0
+ })
+ },
+ on_name_get: function (name_get) {
+ if (!name_get.length) {
+ delete this.id;
+ this.got_name.reject();
+ return;
+ }
+ this.name = name_get[0][1];
+ this.got_name.resolve();
+ },
+ render: function (defaults) {
+ if (defaults[this.attrs.name]) {
+ this.id = defaults[this.attrs.name];
+ if (this.id instanceof Array)
+ this.id = this.id[0];
+ // TODO: maybe this should not be completely removed
+ delete defaults[this.attrs.name];
+ this.dataset.name_get([this.id], $.proxy(this, 'on_name_get'));
+ } else {
+ this.got_name.reject();
+ }
+ return this._super(defaults);
+ },
+ make_domain: function (name, operator, value) {
+ if (this.id && this.name) {
+ if (value === this.name) {
+ return [[name, '=', this.id]];
+ } else {
+ delete this.id;
+ delete this.name;
+ }
+ }
+ return this._super(name, operator, value);
+ }
+});
+
+openerp.web.search.ExtendedSearch = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search',
+ identifier_prefix: 'extended-search',
+ init: function (parent, model) {
+ this._super(parent);
+ this.model = model;
+ },
+ add_group: function() {
+ var group = new openerp.web.search.ExtendedSearchGroup(this, this.fields);
+ group.appendTo(this.$element.find('.searchview_extended_groups_list'));
+ this.check_last_element();
+ },
+ start: function () {
+ this._super();
+ if (!this.$element) {
+ return; // not a logical state but sometimes it happens
+ }
+ this.$element.closest("table.oe-searchview-render-line").css("display", "none");
+ var self = this;
+ this.rpc("/web/searchview/fields_get",
+ {"model": this.model}, function(data) {
+ self.fields = data.fields;
+ openerp.web.search.add_expand_listener(self.$element);
+ self.$element.find('.searchview_extended_add_group').click(function (e) {
+ self.add_group();
+ });
+ });
+ },
+ get_context: function() {
+ return null;
+ },
+ get_domain: function() {
+ if (!this.$element) {
+ return null; // not a logical state but sometimes it happens
+ }
+ if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") {
+ return null;
+ }
+ return _.reduce(this.widget_children,
+ function(mem, x) { return mem.concat(x.get_domain());}, []);
+ },
+ on_activate: function() {
+ this.add_group();
+ var table = this.$element.closest("table.oe-searchview-render-line");
+ table.css("display", "");
+ if(this.$element.hasClass("folded")) {
+ this.$element.toggleClass("folded expanded");
+ }
+ },
+ hide: function() {
+ var table = this.$element.closest("table.oe-searchview-render-line");
+ table.css("display", "none");
+ if(this.$element.hasClass("expanded")) {
+ this.$element.toggleClass("folded expanded");
+ }
+ },
+ check_last_element: function() {
+ _.each(this.widget_children, function(x) {x.set_last_group(false);});
+ if (this.widget_children.length >= 1) {
+ this.widget_children[this.widget_children.length - 1].set_last_group(true);
+ }
+ }
+});
+
+openerp.web.search.ExtendedSearchGroup = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.group',
+ identifier_prefix: 'extended-search-group',
+ init: function (parent, fields) {
+ this._super(parent);
+ this.fields = fields;
+ },
+ add_prop: function() {
+ var prop = new openerp.web.search.ExtendedSearchProposition(this, this.fields);
+ var render = prop.render({'index': this.widget_children.length - 1});
+ this.$element.find('.searchview_extended_propositions_list').append(render);
+ prop.start();
+ },
+ start: function () {
+ this._super();
+ var _this = this;
+ this.add_prop();
+ this.$element.find('.searchview_extended_add_proposition').click(function () {
+ _this.add_prop();
+ });
+ this.$element.find('.searchview_extended_delete_group').click(function () {
+ _this.stop();
+ });
+ },
+ get_domain: function() {
+ var props = _(this.widget_children).chain().map(function(x) {
+ return x.get_proposition();
+ }).compact().value();
+ var choice = this.$element.find(".searchview_extended_group_choice").val();
+ var op = choice == "all" ? "&" : "|";
+ return choice == "none" ? ['!'] : [].concat(
+ _.map(_.range(_.max([0,props.length - 1])), function() { return op; }),
+ props);
+ },
+ stop: function() {
+ var parent = this.widget_parent;
+ if (this.widget_parent.widget_children.length == 1)
+ this.widget_parent.hide();
+ this._super();
+ parent.check_last_element();
+ },
+ set_last_group: function(is_last) {
+ this.$element.toggleClass('last_group', is_last);
+ }
+});
+
+openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** @lends openerp.web.search.ExtendedSearchProposition# */{
+ template: 'SearchView.extended_search.proposition',
+ identifier_prefix: 'extended-search-proposition',
+ /**
+ * @constructs openerp.web.search.ExtendedSearchProposition
+ * @extends openerp.web.OldWidget
+ *
+ * @param parent
+ * @param fields
+ */
+ init: function (parent, fields) {
+ this._super(parent);
+ this.fields = _(fields).chain()
+ .map(function(val, key) { return _.extend({}, val, {'name': key}); })
+ .sortBy(function(field) {return field.string;})
+ .value();
+ this.attrs = {_: _, fields: this.fields, selected: null};
+ this.value = null;
+ },
+ start: function () {
+ this._super();
+ this.select_field(this.fields.length > 0 ? this.fields[0] : null);
+ var _this = this;
+ this.$element.find(".searchview_extended_prop_field").change(function() {
+ _this.changed();
+ });
+ this.$element.find('.searchview_extended_delete_prop').click(function () {
+ _this.stop();
+ });
+ },
+ stop: function() {
+ var parent;
+ if (this.widget_parent.widget_children.length == 1)
+ parent = this.widget_parent;
+ this._super();
+ if (parent)
+ parent.stop();
+ },
+ changed: function() {
+ var nval = this.$element.find(".searchview_extended_prop_field").val();
+ if(this.attrs.selected == null || nval != this.attrs.selected.name) {
+ this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
+ }
+ },
+ /**
+ * 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 _this = this;
+ if(this.attrs.selected != null) {
+ this.value.stop();
+ this.value = null;
+ this.$element.find('.searchview_extended_prop_op').html('');
+ }
+ this.attrs.selected = field;
+ if(field == null) {
+ return;
+ }
+
+ var type = field.type;
+ try {
+ openerp.web.search.custom_filters.get_object(type);
+ } catch (e) {
+ if (! e instanceof openerp.web.KeyNotFound) {
+ throw e;
+ }
+ type = "char";
+ console.log('Unknow field type ' + e.key);
+ }
+ this.value = new (openerp.web.search.custom_filters.get_object(type))
+ (this);
+ if(this.value.set_field) {
+ this.value.set_field(field);
+ }
+ _.each(this.value.operators, function(operator) {
+ var option = jQuery('<option>', {value: operator.value})
+ .text(operator.text)
+ .appendTo(_this.$element.find('.searchview_extended_prop_op'));
+ });
+ this.$element.find('.searchview_extended_prop_value').html(
+ this.value.render({}));
+ this.value.start();
+
+ },
+ get_proposition: function() {
+ if ( this.attrs.selected == null)
+ return null;
+ var field = this.attrs.selected.name;
+ var op = this.$element.find('.searchview_extended_prop_op').val();
+ var value = this.value.get_value();
+ return [field, op, value];
+ }
+});
+
+openerp.web.search.ExtendedSearchProposition.Char = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.char',
+ identifier_prefix: 'extended-search-proposition-char',
+ operators: [
+ {value: "ilike", text: "contains"},
+ {value: "not ilike", text: "doesn't contain"},
+ {value: "=", text: "is equal to"},
+ {value: "!=", text: "is not equal to"},
+ {value: ">", text: "greater than"},
+ {value: "<", text: "less than"},
+ {value: ">=", text: "greater or equal than"},
+ {value: "<=", text: "less or equal than"}
+ ],
+ get_value: function() {
+ return this.$element.val();
+ }
+});
+openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.datetime',
+ identifier_prefix: 'extended-search-proposition-datetime',
+ operators: [
+ {value: "=", text: "is equal to"},
+ {value: "!=", text: "is not equal to"},
+ {value: ">", text: "greater than"},
+ {value: "<", text: "less than"},
+ {value: ">=", text: "greater or equal than"},
+ {value: "<=", text: "less or equal than"}
+ ],
+ get_value: function() {
+ return this.$element.val();
+ },
+ start: function() {
+ this._super();
+ this.$element.datetimepicker({
+ dateFormat: 'yy-mm-dd',
+ timeFormat: 'hh:mm:ss'
+ });
+ }
+});
+openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.date',
+ identifier_prefix: 'extended-search-proposition-date',
+ operators: [
+ {value: "=", text: "is equal to"},
+ {value: "!=", text: "is not equal to"},
+ {value: ">", text: "greater than"},
+ {value: "<", text: "less than"},
+ {value: ">=", text: "greater or equal than"},
+ {value: "<=", text: "less or equal than"}
+ ],
+ get_value: function() {
+ return this.$element.val();
+ },
+ start: function() {
+ this._super();
+ this.$element.datepicker({
+ dateFormat: 'yy-mm-dd',
+ timeFormat: 'hh:mm:ss'
+ });
+ }
+});
+openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.integer',
+ identifier_prefix: 'extended-search-proposition-integer',
+ operators: [
+ {value: "=", text: "is equal to"},
+ {value: "!=", text: "is not equal to"},
+ {value: ">", text: "greater than"},
+ {value: "<", text: "less than"},
+ {value: ">=", text: "greater or equal than"},
+ {value: "<=", text: "less or equal than"}
+ ],
+ get_value: function() {
+ var value = parseFloat(this.$element.val());
+ if(value != 0 && !value) {
+ return "";
+ }
+ return Math.round(value);
+ }
+});
+openerp.web.search.ExtendedSearchProposition.Float = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.float',
+ identifier_prefix: 'extended-search-proposition-float',
+ operators: [
+ {value: "=", text: "is equal to"},
+ {value: "!=", text: "is not equal to"},
+ {value: ">", text: "greater than"},
+ {value: "<", text: "less than"},
+ {value: ">=", text: "greater or equal than"},
+ {value: "<=", text: "less or equal than"}
+ ],
+ get_value: function() {
+ var value = parseFloat(this.$element.val());
+ if(value != 0 && !value) {
+ return "";
+ }
+ return value;
+ }
+});
+openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.selection',
+ identifier_prefix: 'extended-search-proposition-selection',
+ operators: [
+ {value: "=", text: "is"},
+ {value: "!=", text: "is not"}
+ ],
+ set_field: function(field) {
+ this.field = field;
+ },
+ get_value: function() {
+ return this.$element.val();
+ }
+});
+openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.OldWidget.extend({
+ template: 'SearchView.extended_search.proposition.boolean',
+ identifier_prefix: 'extended-search-proposition-boolean',
+ operators: [
+ {value: "=", text: "is true"},
+ {value: "!=", text: "is false"}
+ ],
+ get_value: function() {
+ return true;
+ }
+});
+
+openerp.web.search.custom_filters = new openerp.web.Registry({
+ 'char': 'openerp.web.search.ExtendedSearchProposition.Char',
+ 'text': 'openerp.web.search.ExtendedSearchProposition.Char',
+ 'one2many': 'openerp.web.search.ExtendedSearchProposition.Char',
+ 'many2one': 'openerp.web.search.ExtendedSearchProposition.Char',
+ 'many2many': 'openerp.web.search.ExtendedSearchProposition.Char',
+
+ 'datetime': 'openerp.web.search.ExtendedSearchProposition.DateTime',
+ 'date': 'openerp.web.search.ExtendedSearchProposition.Date',
+ 'integer': 'openerp.web.search.ExtendedSearchProposition.Integer',
+ 'float': 'openerp.web.search.ExtendedSearchProposition.Float',
+ 'boolean': 'openerp.web.search.ExtendedSearchProposition.Boolean',
+ 'selection': 'openerp.web.search.ExtendedSearchProposition.Selection'
+});
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/view_editor.js b/addons/web/static/src/js/view_editor.js
new file mode 100644
index 00000000000..22800facf9b
--- /dev/null
+++ b/addons/web/static/src/js/view_editor.js
@@ -0,0 +1,147 @@
+openerp.web.view_editor = function(openerp) {
+openerp.web.ViewEditor = openerp.web.Widget.extend({
+ init: function(parent, element_id, dataset, view, options) {
+ this._super(parent);
+ this.element_id = element_id
+ this.parent = parent
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.xml_id = 0;
+ },
+ start: function() {
+ this.View_editor();
+ },
+ View_editor : function(){
+ var self = this;
+ var action = {
+ name:'ViewEditor',
+ context:this.session.user_context,
+ domain: [["model", "=", this.dataset.model]],
+ res_model: 'ir.ui.view',
+ views : [[false, 'list']],
+ type: 'ir.actions.act_window',
+ target: "current",
+ limit : 80,
+ auto_search : true,
+ flags: {
+ sidebar: false,
+ views_switcher: false,
+ action_buttons:false,
+ search_view:false,
+ pager:false,
+ radio:true
+ },
+ };
+ var action_manager = new openerp.web.ActionManager(this);
+ this.dialog = new openerp.web.Dialog(this,{
+ modal: true,
+ title: 'ViewEditor',
+ width: 750,
+ height: 500,
+ buttons: {
+ "Create": function(){
+
+ },
+ "Edit": function(){
+ self.xml_id=0;
+ self.edit_view();
+ },
+ "Close": function(){
+ $(this).dialog('destroy');
+ }
+ },
+
+ });
+ this.dialog.start();
+ this.dialog.open();
+ action_manager.appendTo(this.dialog);
+ action_manager.do_action(action);
+
+
+ },
+ check_attr:function(xml,tag){
+ var obj = new Object();
+ obj.child_id = [];
+ obj.id = this.xml_id++;
+ var att_list = [];
+ var name1 = "<" + tag;
+ $(xml).each(function() {
+ att_list = this.attributes;
+ att_list = _.select(att_list, function(attrs){
+ if(attrs.nodeName == "string" || attrs.nodeName == "name" || attrs.nodeName == "index"){
+ name1 += ' ' +attrs.nodeName+'='+'"'+attrs.nodeValue+'"';}
+ });
+ name1+= ">";
+ });
+ obj.name = name1;
+ return obj;
+ },
+ recursiveFunction : function(main_object,parent_id,child_id){
+ var self = this;
+ var check = false;
+ var main_object = _.detect(main_object , function(node){
+ if(node.id == parent_id){
+ node.child_id = child_id;
+ check = true;
+ }
+ return main_object;
+ });
+ if(check){
+ return main_object;
+ }else{
+ //todo recursion for saving object into objects
+ }
+ },
+ children_function : function(xml,root,main_object,parent_id){
+ var self = this;
+ var child_obj_list = [];
+ var main_object = main_object;
+ var children_list = $(xml).filter(root).children();
+ _.each(children_list, function(child_node){
+ var string = self.check_attr(child_node,child_node.tagName.toLowerCase());
+ child_obj_list.push(string);
+ });
+ if(children_list.length != 0){
+ main_object = self.recursiveFunction(main_object,parent_id,child_obj_list);
+ }
+ for(var i=0;i<children_list.length;i++){
+ self.children_function(children_list[i],children_list[i].tagName.toLowerCase(),main_object,child_obj_list[i].id);
+ }
+ return child_obj_list;
+ },
+ edit_view : function(){
+ var self = this;
+ var view_id =(($("input[name='radiogroup']:checked").parent()).parent()).attr('data-id');
+ var ve_dataset = new openerp.web.DataSet(this,'ir.ui.view');
+ ve_dataset.read_ids([parseInt(view_id)],['arch'],function (arch) {
+ var arch = arch[0].arch;
+ var root = $(arch).filter(":first")[0];
+ var tag = root.tagName.toLowerCase();
+ var root_object = self.check_attr(root,tag);
+ var all_list = self.children_function(arch,tag,[root_object],root_object.id);
+
+ //todo render the final object of tree view so xml with it child node display...
+ });
+ this.dialog = new openerp.web.Dialog(this,{
+ modal: true,
+ title: 'Edit Xml',
+ width: 750,
+ height: 500,
+ buttons: {
+ "Inherited View": function(){
+
+ },
+ "Preview": function(){
+
+ },
+ "Close": function(){
+ $(this).dialog('destroy');
+ }
+ },
+
+ });
+ this.dialog.start().open();
+ }
+
+});
+};
diff --git a/addons/web/static/src/js/view_help.js b/addons/web/static/src/js/view_help.js
new file mode 100644
index 00000000000..f7e5f7c259e
--- /dev/null
+++ b/addons/web/static/src/js/view_help.js
@@ -0,0 +1,15 @@
+/*---------------------------------------------------------
+ * OpenERP web library
+ *---------------------------------------------------------*/
+
+openerp.web.view_help = function(openerp) {
+
+openerp.web.ProcessView = openerp.web.Widget.extend({
+});
+
+openerp.web.HelpView = openerp.web.Widget.extend({
+});
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js
new file mode 100644
index 00000000000..2a314f57fa8
--- /dev/null
+++ b/addons/web/static/src/js/view_tree.js
@@ -0,0 +1,216 @@
+/*---------------------------------------------------------
+ * OpenERP web library
+ *---------------------------------------------------------*/
+
+openerp.web.view_tree = function(openerp) {
+var QWeb = openerp.web.qweb;
+
+openerp.web.views.add('tree', 'openerp.web.TreeView');
+openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# */{
+ /**
+ * Indicates that this view is not searchable, and thus that no search
+ * view should be displayed (if there is one active).
+ */
+ searchable : false,
+ /**
+ * Genuine tree view (the one displayed as a tree, not the list)
+ *
+ * @constructs openerp.web.TreeView
+ * @extends openerp.web.View
+ *
+ * @param parent
+ * @param element_id
+ * @param dataset
+ * @param view_id
+ * @param options
+ */
+ init: function(parent, element_id, dataset, view_id, options) {
+ this._super(parent, element_id);
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id;
+
+ this.records = {};
+
+ this.options = _.extend({}, this.defaults, options || {});
+ },
+
+ start: function () {
+ this._super();
+ return this.rpc("/web/treeview/load", {
+ model: this.model,
+ view_id: this.view_id,
+ view_type: "tree",
+ toolbar: this.view_manager ? !!this.view_manager.sidebar : false
+ }, this.on_loaded);
+ },
+ /**
+ * Returns the list of fields needed to correctly read objects.
+ *
+ * Gathers the names of all fields in fields_view_get, and adds the
+ * field_parent (children_field in the tree view) if it's not already one
+ * of the fields to fetch
+ *
+ * @returns {Array} an array of fields which can be provided to DataSet.read_slice and others
+ */
+ fields_list: function () {
+ var fields = _.keys(this.fields);
+ if (!_(fields).contains(this.children_field)) {
+ fields.push(this.children_field);
+ }
+ return fields;
+ },
+ on_loaded: function (fields_view) {
+ var self = this;
+ var has_toolbar = !!fields_view.arch.attrs.toolbar;
+ // 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.fields_view = fields_view;
+ _(this.fields_view.arch.children).each(function (field) {
+ if (field.attrs.modifiers) {
+ field.attrs.modifiers = JSON.parse(field.attrs.modifiers);
+ }
+ });
+ this.fields = fields_view.fields;
+ this.hook_row_click();
+ this.$element.html(QWeb.render('TreeView', {
+ 'title': this.fields_view.arch.attrs.string,
+ 'fields_view': this.fields_view.arch.children,
+ 'fields': this.fields,
+ 'toolbar': has_toolbar
+ }));
+
+ this.dataset.read_slice(this.fields_list(), {}, function (records) {
+ if (!has_toolbar) {
+ // WARNING: will do a second read on the same ids, but only on
+ // first load so not very important
+ self.getdata(null, _(records).pluck('id'));
+ return;
+ }
+
+ var $select = self.$element.find('select')
+ .change(function () {
+ var $option = $(this).find(':selected');
+ self.getdata($option.val(), $option.data('children'));
+ });
+ _(records).each(function (record) {
+ self.records[record.id] = record;
+ $('<option>')
+ .val(record.id)
+ .text(record.name)
+ .data('children', record[self.children_field])
+ .appendTo($select);
+ });
+
+ if (!_.isEmpty(records)) {
+ $select.change();
+ }
+ });
+ },
+ /**
+ * Sets up opening a row
+ */
+ hook_row_click: function () {
+ var self = this;
+ this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) {
+ e.stopImmediatePropagation();
+ self.activate($(this).closest('tr').data('id'));
+ });
+
+ this.$element.delegate('.treeview-tr', 'click', function () {
+ var is_loaded = 0,
+ $this = $(this),
+ record_id = $this.data('id'),
+ record = self.records[record_id],
+ children_ids = record[self.children_field];
+
+ _(children_ids).each(function(childid) {
+ if (self.$element.find('#treerow_' + childid).length) {
+ if (self.$element.find('#treerow_' + childid).is(':hidden')) {
+ is_loaded = -1;
+ } else {
+ is_loaded++;
+ }
+ }
+ });
+ if (is_loaded === 0) {
+ if (!$this.parent().hasClass('oe-open')) {
+ self.getdata(record_id, children_ids);
+ }
+ } else {
+ self.showcontent(record_id, is_loaded < 0);
+ }
+ });
+ },
+ // get child data of selected value
+ getdata: function (id, children_ids) {
+ var self = this;
+
+ self.dataset.read_ids(children_ids, this.fields_list(), function (records) {
+ _(records).each(function (record) {
+ self.records[record.id] = record;
+ });
+
+ var $curr_node = self.$element.find('#treerow_' + id);
+ var children_rows = QWeb.render('TreeView.rows', {
+ 'records': records,
+ 'children_field': self.children_field,
+ 'fields_view': self.fields_view.arch.children,
+ 'fields': self.fields,
+ 'level': $curr_node.data('level') || 0,
+ 'render': openerp.web.format_value
+ });
+
+ if ($curr_node.length) {
+ $curr_node.addClass('oe-open');
+ $curr_node.after(children_rows);
+ } else {
+ self.$element.find('tbody').html(children_rows);
+ }
+ });
+ },
+
+ // Get details in listview
+ activate: function(id) {
+ var self = this;
+ this.rpc('/web/treeview/action', {
+ id: id,
+ model: this.dataset.model,
+ context: new openerp.web.CompoundContext(
+ this.dataset.get_context(), {
+ active_model: this.dataset.model,
+ active_id: id,
+ active_ids: [id]})
+ }, function (actions) {
+ if (!actions.length) { return; }
+ var action = actions[0][2];
+ self.do_action(action);
+ });
+ },
+
+ // show & hide the contents
+ showcontent: function (record_id, show) {
+ this.$element.find('#treerow_' + record_id)
+ .toggleClass('oe-open', show);
+
+ _(this.records[record_id][this.children_field]).each(function (child_id) {
+ var $child_row = this.$element.find('#treerow_' + child_id);
+ if ($child_row.hasClass('oe-open')) {
+ this.showcontent(child_id, false);
+ }
+ $child_row.toggle(show);
+ }, this);
+ },
+
+ do_show: function () {
+ this.$element.show();
+ },
+
+ do_hide: function () {
+ this.$element.hide();
+ this.hidden = true;
+ }
+});
+};
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
new file mode 100644
index 00000000000..39d02d221ff
--- /dev/null
+++ b/addons/web/static/src/js/views.js
@@ -0,0 +1,829 @@
+/*---------------------------------------------------------
+ * OpenERP web library
+ *---------------------------------------------------------*/
+
+openerp.web.views = function(openerp) {
+
+var _t = openerp.web._t;
+var QWeb = openerp.web.qweb;
+
+/**
+ * Registry for all the client actions key: tag value: widget
+ */
+openerp.web.client_actions = new openerp.web.Registry();
+
+openerp.web.ActionManager = openerp.web.Widget.extend({
+ identifier_prefix: "actionmanager",
+ init: function(parent) {
+ this._super(parent);
+ this.inner_viewmanager = null;
+ this.dialog = null;
+ this.dialog_viewmanager = null;
+ this.client_widget = null;
+ this.url = {}
+ },
+ render: function() {
+ return "<div id='"+this.element_id+"'></div>";
+ },
+ dialog_stop: function () {
+ if (this.dialog) {
+ this.dialog_viewmanager.stop();
+ this.dialog_viewmanager = null;
+ this.dialog.stop();
+ this.dialog = null;
+ }
+ },
+ content_stop: function () {
+ if (this.inner_viewmanager) {
+ this.inner_viewmanager.stop();
+ this.inner_viewmanager = null;
+ }
+ if (this.client_widget) {
+ this.client_widget.stop();
+ this.client_widget = null;
+ }
+ },
+ url_update: function(action) {
+ // this.url = {
+ // "model": action.model,
+ // "domain": action.domain,
+ // };
+ // action.res_model
+ // action.domain
+ // action.context
+ // after
+ // action.views
+ // action.res_id
+ // mode
+ // menu
+ },
+ url_stringify: function(action) {
+ },
+ url_parse: function(action) {
+ },
+ on_url_update: function(url) {
+ },
+ do_url_action: function(url) {
+ },
+ do_action: function(action, on_close) {
+ var type = action.type.replace(/\./g,'_');
+ var popup = action.target === 'new';
+ action.flags = _.extend({
+ views_switcher : !popup,
+ search_view : !popup,
+ action_buttons : !popup,
+ sidebar : !popup,
+ pager : !popup
+ }, action.flags || {});
+ if (!(type in this)) {
+ console.log("Action manager can't handle action of type " + action.type, action);
+ return;
+ }
+ this[type](action, on_close);
+ },
+ ir_actions_act_window: function (action, on_close) {
+ if (action.target === 'new') {
+ if (this.dialog == null) {
+ this.dialog = new openerp.web.Dialog(this, { title: action.name, width: '80%' });
+ if(on_close)
+ this.dialog.on_close.add(on_close);
+ this.dialog.start();
+ } else {
+ this.dialog_viewmanager.stop();
+ }
+ this.dialog_viewmanager = new openerp.web.ViewManagerAction(this, action);
+ this.dialog_viewmanager.appendTo(this.dialog.$element);
+ this.dialog.open();
+ } else {
+ this.dialog_stop();
+ this.content_stop();
+ this.inner_viewmanager = new openerp.web.ViewManagerAction(this, action);
+ this.inner_viewmanager.appendTo(this.$element);
+ this.url_update(action);
+ }
+ /* new window code
+ this.rpc("/web/session/save_session_action", { the_action : action}, function(key) {
+ var url = window.location.protocol + "//" + window.location.host + window.location.pathname + "?" + jQuery.param({ s_action : "" + key });
+ window.open(url,'_blank');
+ });
+ */
+ },
+ ir_actions_act_window_close: function (action, on_closed) {
+ this.dialog_stop();
+ },
+ ir_actions_server: function (action, on_closed) {
+ var self = this;
+ this.rpc('/web/action/run', {
+ action_id: action.id,
+ context: action.context || {}
+ }).then(function (action) {
+ self.do_action(action, on_closed)
+ });
+ },
+ ir_actions_client: function (action) {
+ this.content_stop();
+ var ClientWidget = openerp.web.client_actions.get_object(action.tag);
+ (this.client_widget = new ClientWidget(this, action.params)).appendTo(this);
+ },
+ ir_actions_report_xml: function(action) {
+ $.blockUI();
+ this.session.get_file({
+ url: '/web/report',
+ data: {action: JSON.stringify(action)},
+ complete: $.unblockUI
+ });
+ }
+});
+
+openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.ViewManager# */{
+ identifier_prefix: "viewmanager",
+ template: "ViewManager",
+ /**
+ * @constructs openerp.web.ViewManager
+ * @extends openerp.web.Widget
+ *
+ * @param parent
+ * @param dataset
+ * @param views
+ */
+ init: function(parent, dataset, views) {
+ this._super(parent);
+ this.model = dataset.model;
+ this.dataset = dataset;
+ this.searchview = null;
+ this.active_view = null;
+ this.views_src = _.map(views, function(x) {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;});
+ this.views = {};
+ this.flags = this.flags || {};
+ this.registry = openerp.web.views;
+ },
+ render: function() {
+ return QWeb.render(this.template, {"prefix": this.element_id, views: this.views_src})
+ },
+ /**
+ * @returns {jQuery.Deferred} initial view loading promise
+ */
+ start: function() {
+ this._super();
+ var self = this;
+ this.dataset.start();
+ this.$element.find('.oe_vm_switch button').click(function() {
+ self.on_mode_switch($(this).data('view-type'));
+ });
+ var views_ids = {};
+ _.each(this.views_src, function(view) {
+ self.views[view.view_type] = $.extend({}, view, {
+ controller : null,
+ options : _.extend({
+ sidebar_id : self.element_id + '_sidebar_' + view.view_type,
+ action : self.action,
+ action_views_ids : views_ids
+ }, self.flags, view.options || {})
+ });
+ views_ids[view.view_type] = view.view_id;
+ });
+ if (this.flags.views_switcher === false) {
+ this.$element.find('.oe_vm_switch').hide();
+ }
+ // switch to the first one in sequence
+ return this.on_mode_switch(this.views_src[0].view_type);
+ },
+ /**
+ * Asks the view manager to switch visualization mode.
+ *
+ * @param {String} view_type type of view to display
+ * @returns {jQuery.Deferred} new view loading promise
+ */
+ on_mode_switch: function(view_type) {
+ var self = this,
+ view_promise;
+ this.active_view = view_type;
+ var view = this.views[view_type];
+ if (!view.controller) {
+ // Lazy loading of views
+ var controllerclass = this.registry.get_object(view_type);
+ var controller = new controllerclass(this, this.element_id + '_view_' + view_type,
+ this.dataset, view.view_id, view.options);
+ if (view.embedded_view) {
+ controller.set_embedded_view(view.embedded_view);
+ }
+ controller.do_switch_view.add_last(this.on_mode_switch);
+ if (view_type === 'list' && this.flags.search_view === false && this.action && this.action['auto_search']) {
+ // In case the search view is not instantiated: manually call ListView#search
+ var domains = !_(self.action.domain).isEmpty()
+ ? [self.action.domain] : [],
+ contexts = !_(self.action.context).isEmpty()
+ ? [self.action.context] : [];
+ controller.on_loaded.add({
+ callback: function () {
+ controller.do_search(domains, contexts, []);
+ },
+ position: 'last',
+ unique: true
+ });
+ }
+ view_promise = controller.start();
+ $.when(view_promise).then(function() {
+ self.on_controller_inited(view_type, controller);
+ });
+ this.views[view_type].controller = controller;
+ }
+
+
+ if (this.searchview) {
+ if (view.controller.searchable === false) {
+ this.searchview.hide();
+ } else {
+ this.searchview.show();
+ }
+ }
+
+ this.$element
+ .find('.views-switchers button').removeAttr('disabled')
+ .filter('[data-view-type="' + view_type + '"]')
+ .attr('disabled', true);
+
+ for (var view_name in this.views) {
+ if (!this.views.hasOwnProperty(view_name)) { continue; }
+ if (this.views[view_name].controller) {
+ if (view_name === view_type) {
+ $.when(view_promise).then(this.views[view_name].controller.do_show);
+ } else {
+ this.views[view_name].controller.do_hide();
+ }
+ }
+ }
+ return view_promise;
+ },
+ /**
+ * Event launched when a controller has been inited.
+ *
+ * @param {String} view_type type of view
+ * @param {String} view the inited controller
+ */
+ on_controller_inited: function(view_type, view) {
+ },
+ /**
+ * 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(view_id, search_defaults) {
+ var self = this;
+ if (this.searchview) {
+ this.searchview.stop();
+ }
+ this.searchview = new openerp.web.SearchView(
+ this, this.element_id + "_search", this.dataset,
+ view_id, search_defaults);
+
+ this.searchview.on_search.add(function(domains, contexts, groupbys) {
+ var controller = self.views[self.active_view].controller;
+ controller.do_search.call(controller, domains, contexts, groupbys);
+ });
+ return this.searchview.start();
+ },
+ /**
+ * Called when one of the view want to execute an action
+ */
+ on_action: function(action) {
+ },
+ on_create: function() {
+ },
+ on_remove: function() {
+ },
+ on_edit: function() {
+ }
+});
+
+openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{
+ template:"ViewManagerAction",
+ /**
+ * @constructs openerp.web.ViewManagerAction
+ * @extends openerp.web.ViewManager
+ *
+ * @param {openerp.web.ActionManager} parent parent object/widget
+ * @param {Object} action descriptor for the action this viewmanager needs to manage its views.
+ */
+ init: function(parent, action) {
+ // dataset initialization will take the session from ``this``, so if we
+ // do not have it yet (and we don't, because we've not called our own
+ // ``_super()``) rpc requests will blow up.
+ this.session = parent.session;
+ this.action = action;
+ var dataset = new openerp.web.DataSetSearch(this, action.res_model, action.context, action.domain);
+ if (action.res_id) {
+ dataset.ids.push(action.res_id);
+ dataset.index = 0;
+ }
+ this._super(parent, dataset, action.views);
+ this.flags = this.action.flags || {};
+ if (action.res_model == 'board.board' && action.views.length == 1 && action.views) {
+ // Not elegant but allows to avoid form chrome (pager, save/new
+ // buttons, sidebar, ...) displaying
+ this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false;
+ }
+ },
+ /**
+ * Initializes the ViewManagerAction: sets up the searchview (if the
+ * searchview is enabled in the manager's action flags), calls into the
+ * parent to initialize the primary view and (if the VMA has a searchview)
+ * launches an initial search after both views are done rendering.
+ */
+ start: function() {
+ var searchview_loaded;
+ if (this.flags.search_view !== false) {
+ var search_defaults = {};
+ _.each(this.action.context, function (value, key) {
+ var match = /^search_default_(.*)$/.exec(key);
+ if (match) {
+ search_defaults[match[1]] = value;
+ }
+ });
+ // init search view
+ var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
+
+ searchview_loaded = this.setup_search_view(
+ searchview_id || false, search_defaults);
+ }
+
+ var main_view_loaded = this._super();
+
+ var manager_ready = $.when(searchview_loaded, main_view_loaded);
+ if (searchview_loaded && this.action['auto_search']) {
+ // schedule auto_search
+ manager_ready.then(this.searchview.do_search);
+ }
+ return manager_ready;
+ },
+ on_mode_switch: function (view_type) {
+ return $.when(
+ this._super(view_type),
+ this.shortcut_check(this.views[view_type]));
+ },
+ shortcut_check : function(view) {
+ var self = this;
+ var grandparent = this.widget_parent && this.widget_parent.widget_parent;
+ // display shortcuts if on the first view for the action
+ var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle');
+ if (!(grandparent instanceof openerp.web.WebClient) ||
+ !(view.view_type === this.views_src[0].view_type
+ && view.view_id === this.views_src[0].view_id)) {
+ $shortcut_toggle.hide();
+ return;
+ }
+ $shortcut_toggle.removeClass('oe-shortcut-remove').show();
+ if (_(this.session.shortcuts).detect(function (shortcut) {
+ return shortcut.res_id === self.session.active_id; })) {
+ $shortcut_toggle.addClass("oe-shortcut-remove");
+ }
+ this.shortcut_add_remove();
+ },
+ shortcut_add_remove: function() {
+ var self = this;
+ var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle');
+ $shortcut_toggle
+ .unbind("click")
+ .click(function() {
+ if ($shortcut_toggle.hasClass("oe-shortcut-remove")) {
+ $(self.session.shortcuts.binding).trigger('remove-current');
+ $shortcut_toggle.removeClass("oe-shortcut-remove");
+ } else {
+ $(self.session.shortcuts.binding).trigger('add', {
+ 'user_id': self.session.uid,
+ 'res_id': self.session.active_id,
+ 'resource': 'ir.ui.menu',
+ 'name': self.action.name
+ });
+ $shortcut_toggle.addClass("oe-shortcut-remove");
+ }
+ });
+ }
+});
+
+openerp.web.Sidebar = openerp.web.Widget.extend({
+ init: function(parent, element_id) {
+ this._super(parent, element_id);
+ this.items = {};
+ this.sections = {};
+ },
+ start: function() {
+ this._super(this);
+ var self = this;
+ this.$element.html(QWeb.render('Sidebar'));
+ this.$element.find(".toggle-sidebar").click(function(e) {
+ self.do_toggle();
+ });
+ },
+ add_toolbar: function(toolbar) {
+ var self = this;
+ _.each([['print', "Reports"], ['action', "Actions"], ['relate', "Links"]], function(type) {
+ var items = toolbar[type[0]];
+ if (items.length) {
+ for (var i = 0; i < items.length; i++) {
+ items[i] = {
+ label: items[i]['name'],
+ action: items[i],
+ classname: 'oe_sidebar_' + type[0]
+ }
+ }
+ self.add_section(type[0], type[1], items);
+ }
+ });
+ },
+ add_section: function(code, name, items) {
+ // For each section, we pass a name/label and optionally an array of items.
+ // If no items are passed, then the section will be created as a custom section
+ // returning back an element_id to be used by a custom controller.
+ // Else, the section is a standard section with items displayed as links.
+ // An item is a dictonary : {
+ // label: label to be displayed for the link,
+ // action: action to be launch when the link is clicked,
+ // callback: a function to be executed when the link is clicked,
+ // classname: optional dom class name for the line,
+ // title: optional title for the link
+ // }
+ // Note: The item should have one action or/and a callback
+ var self = this,
+ section_id = _.uniqueId(this.element_id + '_section_' + code + '_');
+ if (items) {
+ for (var i = 0; i < items.length; i++) {
+ items[i].element_id = _.uniqueId(section_id + '_item_');
+ this.items[items[i].element_id] = items[i];
+ }
+ }
+ var $section = $(QWeb.render("Sidebar.section", {
+ section_id: section_id,
+ name: name,
+ classname: 'oe_sidebar_' + code,
+ items: items
+ }));
+ if (items) {
+ $section.find('a.oe_sidebar_action_a').click(function() {
+ var item = self.items[$(this).attr('id')];
+ if (item.callback) {
+ item.callback();
+ }
+ if (item.action) {
+ var ids = self.widget_parent.get_selected_ids();
+ if (ids.length == 0) {
+ //TODO: make prettier warning?
+ $("<div />").text(_t("You must choose at least one record.")).dialog({
+ title: _t("Warning"),
+ modal: true
+ });
+ return false;
+ }
+ var additional_context = {
+ active_id: ids[0],
+ active_ids: ids,
+ active_model: self.widget_parent.dataset.model
+ };
+ self.rpc("/web/action/load", {
+ action_id: item.action.id,
+ context: additional_context
+ }, function(result) {
+ result.result.context = _.extend(result.result.context || {},
+ additional_context);
+ result.result.flags = result.result.flags || {};
+ result.result.flags.new_window = true;
+ self.do_action(result.result);
+ });
+ }
+ return false;
+ });
+ }
+ $section.appendTo(this.$element.find('div.sidebar-actions'));
+ this.sections[code] = $section;
+ return section_id;
+ },
+ do_fold: function() {
+ this.$element.addClass('closed-sidebar').removeClass('open-sidebar');
+ },
+ do_unfold: function() {
+ this.$element.addClass('open-sidebar').removeClass('closed-sidebar');
+ },
+ do_toggle: function() {
+ this.$element.toggleClass('open-sidebar closed-sidebar');
+ }
+});
+
+openerp.web.TranslateDialog = openerp.web.Dialog.extend({
+ dialog_title: _t("Translations"),
+ init: function(view) {
+ // TODO fme: should add the language to fields_view_get because between the fields view get
+ // and the moment the user opens the translation dialog, the user language could have been changed
+ this.view_language = view.session.user_context.lang;
+ this['on_button' + _t("Save")] = this.on_button_Save;
+ this['on_button' + _t("Close")] = this.on_button_Close;
+ this._super(view, {
+ width: '80%',
+ height: '80%'
+ });
+ this.view = view;
+ this.view_type = view.fields_view.type || '';
+ this.$fields_form = null;
+ this.$view_form = null;
+ this.$sidebar_form = null;
+ this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name });
+ this.languages = null;
+ this.languages_loaded = $.Deferred();
+ (new openerp.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(),
+ [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }, this.on_languages_loaded);
+ },
+ start: function() {
+ var self = this;
+ this._super();
+ $.when(this.languages_loaded).then(function() {
+ self.$element.html(QWeb.render('TranslateDialog', { widget: self }));
+ self.$element.tabs();
+ if (!(self.view.translatable_fields && self.view.translatable_fields.length)) {
+ self.hide_tabs('fields');
+ self.select_tab('view');
+ }
+ self.$fields_form = self.$element.find('.oe_translation_form');
+ self.$fields_form.find('.oe_trad_field').change(function() {
+ $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value')));
+ });
+ });
+ return this;
+ },
+ on_languages_loaded: function(langs) {
+ this.languages = langs;
+ this.languages_loaded.resolve();
+ },
+ do_load_fields_values: function(callback) {
+ var self = this,
+ deffered = [];
+ this.$fields_form.find('.oe_trad_field').val('').removeClass('touched');
+ _.each(self.languages, function(lg) {
+ var deff = $.Deferred();
+ deffered.push(deff);
+ var callback = function(values) {
+ _.each(self.translatable_fields_keys, function(f) {
+ self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || '');
+ });
+ deff.resolve();
+ };
+ if (lg.code === self.view_language) {
+ var values = {};
+ _.each(self.translatable_fields_keys, function(field) {
+ values[field] = self.view.fields[field].get_value();
+ });
+ callback([values]);
+ } else {
+ self.rpc('/web/dataset/get', {
+ model: self.view.dataset.model,
+ ids: [self.view.datarecord.id],
+ fields: self.translatable_fields_keys,
+ context: self.view.dataset.get_context({
+ 'lang': lg.code
+ })}, callback);
+ }
+ });
+ $.when.apply(null, deffered).then(callback);
+ },
+ show_tabs: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ this.$element.find('ul.oe_translate_tabs li a[href$="' + arguments[i] + '"]').parent().show();
+ }
+ },
+ hide_tabs: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ this.$element.find('ul.oe_translate_tabs li a[href$="' + arguments[i] + '"]').parent().hide();
+ }
+ },
+ select_tab: function(name) {
+ this.show_tabs(name);
+ var index = this.$element.find('ul.oe_translate_tabs li a[href$="' + arguments[i] + '"]').parent().index() - 1;
+ this.$element.tabs('select', index);
+ },
+ open: function(field) {
+ var self = this,
+ sup = this._super;
+ $.when(this.languages_loaded).then(function() {
+ if (self.view.translatable_fields && self.view.translatable_fields.length) {
+ self.do_load_fields_values(function() {
+ sup.call(self);
+ if (field) {
+ // TODO: focus and scroll to field
+ }
+ });
+ } else {
+ sup.call(self);
+ }
+ });
+ },
+ on_button_Save: function() {
+ var trads = {},
+ self = this;
+ self.$fields_form.find('.oe_trad_field.touched').each(function() {
+ var field = $(this).attr('name').split('-');
+ if (!trads[field[0]]) {
+ trads[field[0]] = {};
+ }
+ trads[field[0]][field[1]] = $(this).val();
+ });
+ _.each(trads, function(data, code) {
+ if (code === self.view_language) {
+ _.each(data, function(value, field) {
+ self.view.fields[field].set_value(value);
+ });
+ } else {
+ self.view.dataset.write(self.view.datarecord.id, data, { 'lang': code });
+ }
+ });
+ this.close();
+ },
+ on_button_Close: function() {
+ this.close();
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.Widget
+ */
+openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{
+ set_default_options: function(options) {
+ this.options = options || {};
+ _.defaults(this.options, {
+ // All possible views options should be defaulted here
+ sidebar_id: null,
+ sidebar: true,
+ action: null,
+ action_views_ids: {}
+ });
+ },
+ open_translate_dialog: function(field) {
+ if (!this.translate_dialog) {
+ this.translate_dialog = new openerp.web.TranslateDialog(this).start();
+ }
+ this.translate_dialog.open(field);
+ },
+ /**
+ * 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 {openerp.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)
+ */
+ execute_action: function (action_data, dataset, record_id, on_closed) {
+ var self = this;
+ var handler = function (r) {
+ var action = r.result;
+ if (action && action.constructor == Object) {
+ action.context = action.context || {};
+ _.extend(action.context, {
+ active_id: record_id || false,
+ active_ids: [record_id || false],
+ active_model: dataset.model
+ });
+ action.context = new openerp.web.CompoundContext(dataset.get_context(), action.context);
+ self.do_action(action, on_closed);
+ } else if (on_closed) {
+ on_closed(action);
+ }
+ };
+
+ var context = new openerp.web.CompoundContext(dataset.get_context(), action_data.context || {});
+
+ if (action_data.special) {
+ handler({result: {"type":"ir.actions.act_window_close"}});
+ } else if (action_data.type=="object") {
+ return dataset.call_button(action_data.name, [[record_id], context], handler);
+ } else if (action_data.type=="action") {
+ return this.rpc('/web/action/load', { action_id: parseInt(action_data.name, 10), context: context }, handler);
+ } else {
+ return dataset.exec_workflow(record_id, action_data.name, 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 openerp.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;
+ this.options.sidebar = false;
+ },
+ do_switch_view: function(view) {
+ },
+ set_common_sidebar_sections: function(sidebar) {
+ sidebar.add_section('customize', "Customize", [
+ {
+ label: "Manage Views",
+ callback: this.on_sidebar_manage_view,
+ title: "Manage views of the current object"
+ }, {
+ label: "Edit Workflow",
+ callback: this.on_sidebar_edit_workflow,
+ title: "Manage views of the current object",
+ classname: 'oe_hide oe_sidebar_edit_workflow'
+ }, {
+ label: "Customize Object",
+ callback: this.on_sidebar_customize_object,
+ title: "Manage views of the current object"
+ }
+ ]);
+ sidebar.add_section('other', "Other Options", [
+ {
+ label: "Import",
+ callback: this.on_sidebar_import
+ }, {
+ label: "Export",
+ callback: this.on_sidebar_export
+ }, {
+ label: "Translate",
+ callback: this.on_sidebar_translate,
+ classname: 'oe_sidebar_translate oe_hide'
+ }, {
+ label: "View Log",
+ callback: this.on_sidebar_view_log,
+ classname: 'oe_hide oe_sidebar_view_log'
+ }
+ ]);
+ },
+ on_sidebar_manage_view: function() {
+ if (this.fields_view && this.fields_view.arch) {
+// $('<xmp>' + openerp.base.json_node_to_xml(this.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600});
+ var view_editor = new openerp.base.ViewEditor(this, this.$element, this.dataset, this.fields_view.arch)
+ view_editor.start();
+ } else {
+ this.notification.warn("Manage Views", "Could not find current view declaration");
+ }
+ },
+ on_sidebar_edit_workflow: function() {
+ console.log('Todo');
+ },
+ on_sidebar_customize_object: function() {
+ console.log('Todo');
+ },
+ on_sidebar_import: function() {
+ },
+ on_sidebar_export: function() {
+ var export_view = new openerp.web.DataExport(this, this.dataset);
+ export_view.start();
+ },
+ on_sidebar_translate: function() {
+ this.open_translate_dialog();
+ },
+ on_sidebar_view_log: function() {
+ }
+});
+
+/**
+ * Registry for all the main views
+ */
+openerp.web.views = new openerp.web.Registry();
+
+openerp.web.json_node_to_xml = function(node, single_quote, indent) {
+ // For debugging purpose, this function will convert a json node back to xml
+ // Maybe usefull for xml view editor
+
+ if (typeof(node) === 'string') {
+ return node;
+ }
+ else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
+ throw("Node a json node");
+ }
+ indent = indent || 0;
+ var sindent = new Array(indent + 1).join('\t'),
+ r = sindent + '<' + node.tag;
+ 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 (single_quote) {
+ vattr = vattr.replace(/&quot;/g, "'");
+ }
+ r += ' ' + attr + '="' + vattr + '"';
+ }
+ if (node.children && node.children.length) {
+ r += '>\n';
+ var childs = [];
+ for (var i = 0, ii = node.children.length; i < ii; i++) {
+ childs.push(openerp.web.json_node_to_xml(node.children[i], single_quote, indent + 1));
+ }
+ r += childs.join('\n');
+ r += '\n' + sindent + '</' + node.tag + '>';
+ return r;
+ } else {
+ return r + '/>';
+ }
+}
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: