summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/form_relational_widgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/form_relational_widgets.js')
-rw-r--r--addons/web/static/src/js/views/form_relational_widgets.js2028
1 files changed, 2028 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js
new file mode 100644
index 00000000000..19ca1903048
--- /dev/null
+++ b/addons/web/static/src/js/views/form_relational_widgets.js
@@ -0,0 +1,2028 @@
+odoo.define('web.form_relational', ['web.core', 'web.data', 'web.Dialog', 'web.form_common', 'web.FormView', 'web.ListView', 'web.Model', 'web.session', 'web.utils', 'web.ViewManager'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var common = require('web.form_common');
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var Model = require('web.Model');
+var session = require('web.session');
+var utils = require('web.utils');
+var ViewManager = require('web.ViewManager');
+
+
+var _t = core._t;
+var QWeb = core.qweb;
+var commands = common.commands;
+var list_widget_registry = core.list_widget_registry;
+
+var M2ODialog = Dialog.extend({
+ template: "M2ODialog",
+ init: function(parent) {
+ this.name = parent.string;
+ this._super(parent, {
+ title: _.str.sprintf(_t("Create a %s"), parent.string),
+ size: 'medium',
+ });
+ },
+ start: function() {
+ var self = this;
+ var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
+ this.$("p").text( text );
+ this.$buttons.html(QWeb.render("M2ODialog.buttons"));
+ this.$("input").val(this.getParent().$input.val());
+ this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
+ if (self.$("input").val() !== ''){
+ self.getParent()._quick_create(self.$("input").val());
+ self.destroy();
+ } else{
+ e.preventDefault();
+ self.$("input").focus();
+ }
+ });
+ this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
+ self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
+ self.destroy();
+ });
+ this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
+ self.destroy();
+ });
+ },
+});
+
+var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, common.ReinitializeFieldMixin, {
+ template: "FieldMany2One",
+ events: {
+ 'keydown input': function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ e.stopPropagation();
+ }
+ },
+ },
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ common.CompletionFieldMixin.init.call(this);
+ this.set({'value': false});
+ this.display_value = {};
+ this.display_value_backup = {};
+ this.last_search = [];
+ this.floating = false;
+ this.current_display = null;
+ this.is_started = false;
+ this.ignore_focusout = false;
+ },
+ reinit_value: function(val) {
+ this.internal_set_value(val);
+ this.floating = false;
+ if (this.is_started && !this.no_rerender) {
+ this.render_value();
+ }
+ },
+ initialize_field: function() {
+ this.is_started = true;
+ core.bus.on('click', this, function() {
+ if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
+ this.$input.autocomplete("close");
+ }
+ });
+ common.ReinitializeFieldMixin.initialize_field.call(this);
+ },
+ initialize_content: function() {
+ if (!this.get("effective_readonly"))
+ this.render_editable();
+ },
+ destroy_content: function () {
+ if (this.$drop_down) {
+ this.$drop_down.off('click');
+ delete this.$drop_down;
+ }
+ if (this.$input) {
+ this.$input.closest(".modal .modal-content").off('scroll');
+ this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
+ 'focus focusout change keydown');
+ delete this.$input;
+ }
+ if (this.$follow_button) {
+ this.$follow_button.off('blur focus click');
+ delete this.$follow_button;
+ }
+ },
+ destroy: function () {
+ this.destroy_content();
+ return this._super();
+ },
+ init_error_displayer: function() {
+ // nothing
+ },
+ hide_error_displayer: function() {
+ // doesn't work
+ },
+ show_error_displayer: function() {
+ new M2ODialog(this).open();
+ },
+ render_editable: function() {
+ var self = this;
+ this.$input = this.$el.find("input");
+
+ this.init_error_displayer();
+
+ self.$input.on('focus', function() {
+ self.hide_error_displayer();
+ });
+
+ this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
+ this.$follow_button = $(".oe_m2o_cm_button", this.$el);
+
+ this.$follow_button.click(function(ev) {
+ ev.preventDefault();
+ if (!self.get('value')) {
+ self.focus();
+ return;
+ }
+ var pop = new common.FormOpenPopup(self);
+ var context = self.build_context().eval();
+ var model_obj = new Model(self.field.relation);
+ model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
+ pop.show_element(
+ self.field.relation,
+ self.get("value"),
+ self.build_context(),
+ {
+ title: _t("Open: ") + self.string,
+ view_id: view_id,
+ readonly: !self.can_write
+ }
+ );
+ pop.on('write_completed', self, function(){
+ self.display_value = {};
+ self.display_value_backup = {};
+ self.render_value();
+ self.focus();
+ self.trigger('changed_value');
+ });
+ });
+ });
+
+ // some behavior for input
+ var input_changed = function() {
+ if (self.current_display !== self.$input.val()) {
+ self.current_display = self.$input.val();
+ if (self.$input.val() === "") {
+ self.internal_set_value(false);
+ self.floating = false;
+ } else {
+ self.floating = true;
+ }
+ }
+ };
+ this.$input.keydown(input_changed);
+ this.$input.change(input_changed);
+ this.$drop_down.click(function() {
+ self.$input.focus();
+ if (self.$input.autocomplete("widget").is(":visible")) {
+ self.$input.autocomplete("close");
+ } else {
+ if (self.get("value") && ! self.floating) {
+ self.$input.autocomplete("search", "");
+ } else {
+ self.$input.autocomplete("search");
+ }
+ }
+ });
+
+ // Autocomplete close on dialog content scroll
+ var close_autocomplete = _.debounce(function() {
+ if (self.$input.autocomplete("widget").is(":visible")) {
+ self.$input.autocomplete("close");
+ }
+ }, 50);
+ this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
+
+ self.ed_def = $.Deferred();
+ self.uned_def = $.Deferred();
+ var ed_delay = 200;
+ var ed_duration = 15000;
+ var anyoneLoosesFocus = function (e) {
+ if (self.ignore_focusout) { return; }
+ var used = false;
+ if (self.floating) {
+ if (self.last_search.length > 0) {
+ if (self.last_search[0][0] != self.get("value")) {
+ self.display_value = {};
+ self.display_value_backup = {};
+ self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
+ self.reinit_value(self.last_search[0][0]);
+ self.last_search = [];
+ } else {
+ used = true;
+ self.render_value();
+ }
+ } else {
+ used = true;
+ }
+ self.floating = false;
+ }
+ if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
+ self.ed_def.reject();
+ self.uned_def.reject();
+ self.ed_def = $.Deferred();
+ self.ed_def.done(function() {
+ self.can_create && self.show_error_displayer();
+ ignore_blur = false;
+ self.trigger('focused');
+ });
+ ignore_blur = true;
+ setTimeout(function() {
+ self.ed_def.resolve();
+ self.uned_def.reject();
+ self.uned_def = $.Deferred();
+ self.uned_def.done(function() {
+ self.hide_error_displayer();
+ });
+ setTimeout(function() {self.uned_def.resolve();}, ed_duration);
+ }, ed_delay);
+ } else {
+ self.no_ed = false;
+ self.ed_def.reject();
+ }
+ };
+ var ignore_blur = false;
+ this.$input.on({
+ focusout: anyoneLoosesFocus,
+ focus: function () { self.trigger('focused'); },
+ autocompleteopen: function () { ignore_blur = true; },
+ autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
+ blur: function () {
+ // autocomplete open
+ if (ignore_blur) { $(this).focus(); return; }
+ if (_(self.getChildren()).any(function (child) {
+ return child instanceof common.AbstractFormPopup;
+ })) { return; }
+ self.trigger('blurred');
+ }
+ });
+
+ var isSelecting = false;
+ // autocomplete
+ this.$input.autocomplete({
+ source: function(req, resp) {
+ self.get_search_result(req.term).done(function(result) {
+ resp(result);
+ });
+ },
+ select: function(event, ui) {
+ isSelecting = true;
+ var item = ui.item;
+ if (item.id) {
+ self.display_value = {};
+ self.display_value_backup = {};
+ self.display_value["" + item.id] = item.name;
+ self.reinit_value(item.id);
+ } else if (item.action) {
+ item.action();
+ // Cancel widget blurring, to avoid form blur event
+ self.trigger('focused');
+ return false;
+ }
+ },
+ focus: function(e, ui) {
+ e.preventDefault();
+ },
+ html: true,
+ // disabled to solve a bug, but may cause others
+ //close: anyoneLoosesFocus,
+ minLength: 0,
+ delay: 200,
+ });
+ var appendTo = this.$input.parents('.oe-view-manager-content, .modal-dialog').last();
+ if (appendTo.length === 0) {
+ appendTo = '.oe_application > *';
+ }
+ this.$input.autocomplete({
+ appendTo: appendTo
+ });
+ // set position for list of suggestions box
+ this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
+ this.$input.autocomplete("widget").openerpClass();
+ // used to correct a bug when selecting an element by pushing 'enter' in an editable list
+ this.$input.keyup(function(e) {
+ if (e.which === 13) { // ENTER
+ if (isSelecting)
+ e.stopPropagation();
+ }
+ isSelecting = false;
+ });
+ this.setupFocus(this.$follow_button);
+ },
+ render_value: function(no_recurse) {
+ var self = this;
+ if (! this.get("value")) {
+ this.display_string("");
+ return;
+ }
+ var display = this.display_value["" + this.get("value")];
+ if (display) {
+ this.display_string(display);
+ return;
+ }
+ if (! no_recurse) {
+ var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context());
+ var def = this.alive(dataset.name_get([self.get("value")])).done(function(data) {
+ if (!data[0]) {
+ self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
+ return;
+ }
+ self.display_value["" + self.get("value")] = data[0][1];
+ self.render_value(true);
+ }).fail( function (data, event) {
+ // avoid displaying crash errors as many2One should be name_get compliant
+ event.preventDefault();
+ self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
+ self.render_value(true);
+ });
+ if (this.view && this.view.render_value_defs){
+ this.view.render_value_defs.push(def);
+ }
+ }
+ },
+ display_string: function(str) {
+ var self = this;
+ if (!this.get("effective_readonly")) {
+ this.$input.val(str.split("\n")[0]);
+ this.current_display = this.$input.val();
+ if (this.is_false()) {
+ this.$('.oe_m2o_cm_button').css({'display':'none'});
+ } else {
+ this.$('.oe_m2o_cm_button').css({'display':'inline'});
+ }
+ } else {
+ var lines = _.escape(str).split("\n");
+ var link = "";
+ var follow = "";
+ link = lines[0];
+ follow = _.rest(lines).join("<br />");
+ if (follow)
+ link += "<br />";
+ var $link = this.$el.find('.oe_form_uri')
+ .unbind('click')
+ .html(link);
+ if (! this.options.no_open)
+ $link.click(function () {
+ var context = self.build_context().eval();
+ var model_obj = new Model(self.field.relation);
+ model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
+ self.do_action(action);
+ });
+ return false;
+ });
+ $(".oe_form_m2o_follow", this.$el).html(follow);
+ }
+ },
+ set_value: function(value_) {
+ if (value_ instanceof Array) {
+ this.display_value = {};
+ this.display_value_backup = {};
+ if (! this.options.always_reload) {
+ this.display_value["" + value_[0]] = value_[1];
+ }
+ else {
+ this.display_value_backup["" + value_[0]] = value_[1];
+ }
+ value_ = value_[0];
+ }
+ value_ = value_ || false;
+ this.reinit_value(value_);
+ },
+ get_displayed: function() {
+ return this.display_value["" + this.get("value")];
+ },
+ add_id: function(id) {
+ this.display_value = {};
+ this.display_value_backup = {};
+ this.reinit_value(id);
+ },
+ is_false: function() {
+ return ! this.get("value");
+ },
+ focus: function () {
+ var input = !this.get('effective_readonly') && this.$input && this.$input[0];
+ return input ? input.focus() : false;
+ },
+ _quick_create: function() {
+ this.no_ed = true;
+ this.ed_def.reject();
+ return common.CompletionFieldMixin._quick_create.apply(this, arguments);
+ },
+ _search_create_popup: function() {
+ this.no_ed = true;
+ this.ed_def.reject();
+ this.ignore_focusout = true;
+ this.reinit_value(false);
+ var res = common.CompletionFieldMixin._search_create_popup.apply(this, arguments);
+ this.ignore_focusout = false;
+ this.no_ed = false;
+ return res;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ if (!this.get("effective_readonly") && this.$input)
+ this.$input.css('height', height);
+ }
+});
+
+var Many2OneButton = common.AbstractField.extend({
+ template: 'Many2OneButton',
+ init: function(field_manager, node) {
+ this._super.apply(this, arguments);
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.set_button();
+ },
+ set_button: function() {
+ var self = this;
+ if (this.$button) {
+ this.$button.remove();
+ }
+ this.string = '';
+ this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
+ this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
+ this.$button.addClass('oe_link').css({'padding':'4px'});
+ this.$el.append(this.$button);
+ this.$button.on('click', self.on_click);
+ },
+ on_click: function(ev) {
+ var self = this;
+ this.popup = new common.FormOpenPopup(this);
+ this.popup.show_element(
+ this.field.relation,
+ this.get('value'),
+ this.build_context(),
+ {title: this.string}
+ );
+ this.popup.on('create_completed', self, function(r) {
+ self.set_value(r);
+ });
+ },
+ set_value: function(value_) {
+ if (value_ instanceof Array) {
+ value_ = value_[0];
+ }
+ value_ = value_ || false;
+ this.set('value', value_);
+ this.set_button();
+ },
+});
+
+/**
+ * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
+ * the big ugly button in the header.
+ *
+ * Requires the implementation of a ``is_readonly`` method (usually a proxy to
+ * the corresponding field's readonly or effective_readonly property) to
+ * decide whether the special row should or should not be inserted.
+ *
+ * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
+ * set on the insertion row.
+ */
+var AddAnItemList = ListView.List.extend({
+ pad_table_to: function (count) {
+ if (!this.view.is_action_enabled('create') || this.is_readonly()) {
+ this._super(count);
+ return;
+ }
+
+ this._super(count > 0 ? count - 1 : 0);
+
+ var self = this;
+ var columns = _(this.columns).filter(function (column) {
+ return column.invisible !== '1';
+ }).length;
+ if (this.options.selectable) { columns++; }
+ if (this.options.deletable) { columns++; }
+
+ var $cell = $('<td>', {
+ colspan: columns,
+ 'class': this._add_row_class || ''
+ }).append(
+ $('<a>', {href: '#'}).text(_t("Add an item"))
+ .mousedown(function () {
+ // FIXME: needs to be an official API somehow
+ if (self.view.editor.is_editing()) {
+ self.view.__ignore_blur = true;
+ }
+ })
+ .click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ // FIXME: there should also be an API for that one
+ if (self.view.editor.form.__blur_timeout) {
+ clearTimeout(self.view.editor.form.__blur_timeout);
+ self.view.editor.form.__blur_timeout = false;
+ }
+ self.view.ensure_saved().done(function () {
+ self.view.do_add_record();
+ });
+ }));
+
+ var $padding = this.$current.find('tr:not([data-id]):first');
+ var $newrow = $('<tr>').append($cell);
+ if ($padding.length) {
+ $padding.before($newrow);
+ } else {
+ this.$current.append($newrow);
+ }
+ }
+});
+
+
+var FieldOne2Many = common.AbstractField.extend({
+ multi_selection: false,
+ disable_utility_classes: true,
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.is_loaded = $.Deferred();
+ this.initial_is_loaded = this.is_loaded;
+ this.form_last_update = $.Deferred();
+ this.init_form_last_update = this.form_last_update;
+ this.is_started = false;
+ this.dataset = new One2ManyDataSet(this, this.field.relation);
+ this.dataset.o2m = this;
+ this.dataset.parent_view = this.view;
+ this.dataset.child_name = this.name;
+ var self = this;
+ this.dataset.on('dataset_changed', this, function() {
+ self.trigger_on_change();
+ });
+ this.set_value([]);
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$el.addClass('oe_form_field oe_form_field_one2many');
+
+ var self = this;
+
+ self.load_views();
+ var destroy = function() {
+ self.is_loaded = self.is_loaded.then(function() {
+ self.viewmanager.destroy();
+ return $.when(self.load_views()).done(function() {
+ self.reload_current_view();
+ });
+ });
+ };
+ this.is_loaded.done(function() {
+ self.on("change:effective_readonly", self, destroy);
+ });
+ this.view.on("on_button_cancel", self, destroy);
+ this.is_started = true;
+ this.reload_current_view();
+ },
+ trigger_on_change: function() {
+ this.trigger('changed_value');
+ },
+ load_views: function() {
+ var self = this;
+
+ var modes = this.node.attrs.mode;
+ modes = !!modes ? modes.split(",") : ["tree"];
+ var views = [];
+ _.each(modes, function(mode) {
+ if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
+ throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
+ }
+ var view = {
+ view_id: false,
+ view_type: mode == "tree" ? "list" : mode,
+ options: {}
+ };
+ if (self.field.views && self.field.views[mode]) {
+ view.embedded_view = self.field.views[mode];
+ }
+ if(view.view_type === "list") {
+ _.extend(view.options, {
+ addable: null,
+ selectable: self.multi_selection,
+ sortable: true,
+ import_enabled: false,
+ deletable: true
+ });
+ if (self.get("effective_readonly")) {
+ _.extend(view.options, {
+ deletable: null,
+ reorderable: false,
+ });
+ }
+ } else if (view.view_type === "form") {
+ if (self.get("effective_readonly")) {
+ view.view_type = 'form';
+ }
+ _.extend(view.options, {
+ not_interactible_on_create: true,
+ });
+ } else if (view.view_type === "kanban") {
+ _.extend(view.options, {
+ confirm_on_delete: false,
+ });
+ if (self.get("effective_readonly")) {
+ _.extend(view.options, {
+ action_buttons: false,
+ quick_creatable: false,
+ creatable: false,
+ read_only_mode: true,
+ });
+ }
+ }
+ views.push(view);
+ });
+ this.views = views;
+
+ this.viewmanager = new One2ManyViewManager(this, this.dataset, views, {});
+ this.viewmanager.o2m = self;
+ var once = $.Deferred().done(function() {
+ self.init_form_last_update.resolve();
+ });
+ var def = $.Deferred().done(function() {
+ self.initial_is_loaded.resolve();
+ });
+ this.viewmanager.on("controller_inited", self, function(view_type, controller) {
+ controller.o2m = self;
+ if (view_type == "list") {
+ if (self.get("effective_readonly")) {
+ controller.on('edit:before', self, function (e) {
+ e.cancel = true;
+ });
+ _(controller.columns).find(function (column) {
+ if (!(column instanceof list_widget_registry.get('field.handle'))) {
+ return false;
+ }
+ column.modifiers.invisible = true;
+ return true;
+ });
+ }
+ } else if (view_type === "form") {
+ if (self.get("effective_readonly")) {
+ $(".oe_form_buttons", controller.$el).children().remove();
+ }
+ controller.on("load_record", self, function(){
+ once.resolve();
+ });
+ controller.on('pager_action_executed',self,self.save_any_view);
+ } else if (view_type == "graph") {
+ self.reload_current_view();
+ }
+ def.resolve();
+ });
+ this.viewmanager.on("switch_mode", self, function(n_mode) {
+ $.when(self.save_any_view()).done(function() {
+ if (n_mode === "list") {
+ $.async_when().done(function() {
+ self.reload_current_view();
+ });
+ }
+ });
+ });
+ $.async_when().done(function () {
+ self.viewmanager.appendTo(self.$el);
+ });
+ return def;
+ },
+ reload_current_view: function() {
+ var self = this;
+ self.is_loaded = self.is_loaded.then(function() {
+ var view = self.get_active_view();
+ if (view.type === "list") {
+ return view.controller.reload_content();
+ } else if (view.type === "form") {
+ if (self.dataset.index === null && self.dataset.ids.length >= 1) {
+ self.dataset.index = 0;
+ }
+ var act = function() {
+ return view.controller.do_show();
+ };
+ self.form_last_update = self.form_last_update.then(act, act);
+ return self.form_last_update;
+ } else if (view.controller.do_search) {
+ return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
+ }
+ }, undefined);
+ return self.is_loaded;
+ },
+ get_active_view: function () {
+ /**
+ * Returns the current active view if any.
+ */
+ return (this.viewmanager && this.viewmanager.active_view);
+ },
+ set_value: function(value_) {
+ value_ = value_ || [];
+ var self = this;
+ var view = this.get_active_view();
+ this.dataset.reset_ids([]);
+ var ids;
+ if(value_.length >= 1 && value_[0] instanceof Array) {
+ ids = [];
+ _.each(value_, function(command) {
+ var obj = {values: command[2]};
+ switch (command[0]) {
+ case commands.CREATE:
+ obj.id = _.uniqueId(self.dataset.virtual_id_prefix);
+ obj.defaults = {};
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
+ ids.push(obj.id);
+ return;
+ case commands.UPDATE:
+ obj.id = command[1];
+ self.dataset.to_write.push(obj);
+ self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
+ ids.push(obj.id);
+ return;
+ case commands.DELETE:
+ self.dataset.to_delete.push({id: command[1]});
+ return;
+ case commands.LINK_TO:
+ ids.push(command[1]);
+ return;
+ case commands.DELETE_ALL:
+ self.dataset.delete_all = true;
+ return;
+ }
+ });
+ this._super(ids);
+ this.dataset.set_ids(ids);
+ } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
+ ids = [];
+ this.dataset.delete_all = true;
+ _.each(value_, function(command) {
+ var obj = {values: command};
+ obj.id = _.uniqueId(self.dataset.virtual_id_prefix);
+ obj.defaults = {};
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ ids.push(obj.id);
+ });
+ this._super(ids);
+ this.dataset.set_ids(ids);
+ } else {
+ this._super(value_);
+ this.dataset.reset_ids(value_);
+ }
+ if (this.dataset.index === null && this.dataset.ids.length > 0) {
+ this.dataset.index = 0;
+ }
+ this.trigger_on_change();
+ if (this.is_started && !this.no_rerender) {
+ return self.reload_current_view();
+ } else {
+ return $.when();
+ }
+ },
+ get_value: function() {
+ var self = this;
+ if (!this.dataset)
+ return [];
+ var val = this.dataset.delete_all ? [commands.delete_all()] : [];
+ val = val.concat(_.map(this.dataset.ids, function(id) {
+ var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
+ if (alter_order) {
+ return commands.create(alter_order.values);
+ }
+ alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
+ if (alter_order) {
+ return commands.update(alter_order.id, alter_order.values);
+ }
+ return commands.link_to(id);
+ }));
+ return val.concat(_.map(
+ this.dataset.to_delete, function(x) {
+ return commands['delete'](x.id);}));
+ },
+ commit_value: function() {
+ return this.save_any_view();
+ },
+ save_any_view: function() {
+ var view = this.get_active_view();
+ if (view) {
+ if (this.viewmanager.active_view.type === "form") {
+ if (view.controller.is_initialized.state() !== 'resolved') {
+ return $.when(false);
+ }
+ return $.when(view.controller.save());
+ } else if (this.viewmanager.active_view.type === "list") {
+ return $.when(view.controller.ensure_saved());
+ }
+ }
+ return $.when(false);
+ },
+ is_syntax_valid: function() {
+ var view = this.get_active_view();
+ if (!view){
+ return true;
+ }
+ switch (this.viewmanager.active_view.type) {
+ case 'form':
+ return _(view.controller.fields).chain()
+ .invoke('is_valid')
+ .all(_.identity)
+ .value();
+ case 'list':
+ return view.controller.is_valid();
+ }
+ return true;
+ },
+});
+
+var One2ManyDataSet = data.BufferedDataSet.extend({
+ get_context: function() {
+ this.context = this.o2m.build_context();
+ return this.context;
+ }
+});
+
+var One2ManyListView = ListView.extend({
+ _template: 'One2Many.listview',
+ init: function (parent, dataset, view_id, options) {
+ this._super(parent, dataset, view_id, _.extend(options || {}, {
+ GroupsType: One2ManyGroups,
+ ListType: One2ManyList
+ }));
+ this.on('edit:after', this, this.proxy('_after_edit'));
+ this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
+
+ this.records
+ .bind('add', this.proxy("changed_records"))
+ .bind('remove', this.proxy("changed_records"));
+ this.on('save:after', this, this.proxy("changed_records"));
+ },
+ start: function () {
+ var ret = this._super();
+ this.$el
+ .off('mousedown.handleButtons')
+ .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
+ return ret;
+ },
+ changed_records: function () {
+ this.o2m.trigger_on_change();
+ },
+ is_valid: function () {
+ var self = this;
+ if (!this.fields_view || !this.editable()){
+ return true;
+ }
+ if (_.isEmpty(this.records.records)){
+ return true;
+ }
+ var current_values = {};
+ _.each(this.editor.form.fields, function(field){
+ field._inhibit_on_change_flag = true;
+ field.no_rerender = true;
+ current_values[field.name] = field.get('value');
+ });
+ var valid = _.every(this.records.records, function(record){
+ _.each(self.editor.form.fields, function(field){
+ field.set_value(record.attributes[field.name]);
+ });
+ return _.every(self.editor.form.fields, function(field){
+ field.process_modifiers();
+ field._check_css_flags();
+ return field.is_valid();
+ });
+ });
+ _.each(this.editor.form.fields, function(field){
+ field.set('value', current_values[field.name]);
+ field._inhibit_on_change_flag = false;
+ field.no_rerender = false;
+ });
+ return valid;
+ },
+ do_add_record: function () {
+ if (this.editable()) {
+ this._super.apply(this, arguments);
+ } else {
+ var self = this;
+ var pop = new common.SelectCreatePopup(this);
+ pop.select_element(
+ self.o2m.field.relation,
+ {
+ title: _t("Create: ") + self.o2m.string,
+ initial_view: "form",
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views.form : undefined,
+ create_function: function(data, options) {
+ return self.o2m.dataset.create(data, options).done(function(r) {
+ self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
+ self.o2m.dataset.trigger("dataset_changed", r);
+ });
+ },
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ },
+ parent_view: self.o2m.view,
+ child_name: self.o2m.name,
+ form_view_options: {'not_interactible_on_create':true}
+ },
+ self.o2m.build_domain(),
+ self.o2m.build_context()
+ );
+ pop.on("elements_selected", self, function() {
+ self.o2m.reload_current_view();
+ });
+ }
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new common.FormOpenPopup(self);
+ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
+ title: _t("Open: ") + self.o2m.string,
+ write_function: function(id, data) {
+ return self.o2m.dataset.write(id, data, {}).done(function() {
+ self.o2m.reload_current_view();
+ });
+ },
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views.form : undefined,
+ parent_view: self.o2m.view,
+ child_name: self.o2m.name,
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ },
+ form_view_options: {'not_interactible_on_create':true},
+ readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
+ });
+ },
+ do_button_action: function (name, id, callback) {
+ if (!_.isNumber(id)) {
+ core.bus.trigger('display_notification_warning',
+ _t("Action Button"),
+ _t("The o2m record must be saved before an action can be used"));
+ return;
+ }
+ var parent_form = this.o2m.view;
+ var self = this;
+ this.ensure_saved().then(function () {
+ if (parent_form)
+ return parent_form.save();
+ else
+ return $.when();
+ }).done(function () {
+ var ds = self.o2m.dataset;
+ var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
+ return value.length;
+ });
+ if (!self.o2m.options.reload_on_button && !cached_records) {
+ self.handle_button(name, id, callback);
+ }else {
+ self.handle_button(name, id, function(){
+ self.o2m.view.reload();
+ });
+ }
+ });
+ },
+
+ _after_edit: function () {
+ this.__ignore_blur = false;
+ this.editor.form.on('blurred', this, this._on_form_blur);
+
+ // The form's blur thing may be jiggered during the edition setup,
+ // potentially leading to the o2m instasaving the row. Cancel any
+ // blurring triggered the edition startup here
+ this.editor.form.widgetFocused();
+ },
+ _before_unedit: function () {
+ this.editor.form.off('blurred', this, this._on_form_blur);
+ },
+ _button_down: function () {
+ // If a button is clicked (usually some sort of action button), it's
+ // the button's responsibility to ensure the editable list is in the
+ // correct state -> ignore form blurring
+ this.__ignore_blur = true;
+ },
+ /**
+ * Handles blurring of the nested form (saves the currently edited row),
+ * unless the flag to ignore the event is set to ``true``
+ *
+ * Makes the internal form go away
+ */
+ _on_form_blur: function () {
+ if (this.__ignore_blur) {
+ this.__ignore_blur = false;
+ return;
+ }
+ // FIXME: why isn't there an API for this?
+ if (this.editor.form.$el.hasClass('oe_form_dirty')) {
+ this.ensure_saved();
+ return;
+ }
+ this.cancel_edition();
+ },
+ keypress_ENTER: function () {
+ // blurring caused by hitting the [Return] key, should skip the
+ // autosave-on-blur and let the handler for [Return] do its thing (save
+ // the current row *anyway*, then create a new one/edit the next one)
+ this.__ignore_blur = true;
+ this._super.apply(this, arguments);
+ },
+ do_delete: function (ids) {
+ var confirm = window.confirm;
+ window.confirm = function () { return true; };
+ try {
+ return this._super(ids);
+ } finally {
+ window.confirm = confirm;
+ }
+ },
+ reload_record: function (record, options) {
+ if (!options || !options.do_not_evict) {
+ // Evict record.id from cache to ensure it will be reloaded correctly
+ this.dataset.evict_record(record.get('id'));
+ }
+
+ return this._super(record);
+ }
+});
+
+var One2ManyGroups = ListView.Groups.extend({
+ setup_resequence_rows: function () {
+ if (!this.view.o2m.get('effective_readonly')) {
+ this._super.apply(this, arguments);
+ }
+ }
+});
+
+var One2ManyList = AddAnItemList.extend({
+ _add_row_class: 'oe_form_field_one2many_list_row_add',
+ is_readonly: function () {
+ return this.view.o2m.get('effective_readonly');
+ },
+});
+
+var One2ManyFormView = FormView.extend({
+ form_template: 'One2Many.formview',
+ load_form: function(data) {
+ this._super(data);
+ var self = this;
+ this.$buttons.find('button.oe_form_button_create').click(function() {
+ self.save().done(self.on_button_new);
+ });
+ },
+ do_notify_change: function() {
+ if (this.dataset.parent_view) {
+ this.dataset.parent_view.do_notify_change();
+ } else {
+ this._super.apply(this, arguments);
+ }
+ }
+});
+
+var One2ManyViewManager = ViewManager.extend({
+ template: 'One2Many.viewmanager',
+ init: function(parent, dataset, views, flags) {
+ this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
+ this.registry = core.view_registry.extend({
+ list: One2ManyListView,
+ form: One2ManyFormView,
+ });
+ this.__ignore_blur = false;
+ },
+ switch_mode: function(mode, unused) {
+ if (mode !== 'form') {
+ return this._super(mode, unused);
+ }
+ var self = this;
+ var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
+ var pop = new common.FormOpenPopup(this);
+ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
+ title: _t("Open: ") + self.o2m.string,
+ create_function: function(data, options) {
+ return self.o2m.dataset.create(data, options).done(function(r) {
+ self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
+ self.o2m.dataset.trigger("dataset_changed", r);
+ });
+ },
+ write_function: function(id, data, options) {
+ return self.o2m.dataset.write(id, data, {}).done(function() {
+ self.o2m.reload_current_view();
+ });
+ },
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views.form : undefined,
+ parent_view: self.o2m.view,
+ child_name: self.o2m.name,
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ },
+ form_view_options: {'not_interactible_on_create':true},
+ readonly: self.o2m.get("effective_readonly")
+ });
+ pop.on("elements_selected", self, function() {
+ self.o2m.reload_current_view();
+ });
+ },
+});
+
+var FieldMany2ManyTags = common.AbstractField.extend(common.CompletionFieldMixin, common.ReinitializeFieldMixin, {
+ template: "FieldMany2ManyTags",
+ tag_template: "FieldMany2ManyTag",
+ init: function() {
+ this._super.apply(this, arguments);
+ common.CompletionFieldMixin.init.call(this);
+ this.set({"value": []});
+ this._display_orderer = new utils.DropMisordered();
+ this._drop_shown = false;
+ },
+ initialize_texttext: function(){
+ var self = this;
+ return {
+ plugins : 'tags arrow autocomplete',
+ autocomplete: {
+ render: function(suggestion) {
+ return $('<span class="text-label"/>').
+ data('index', suggestion.index).html(suggestion.label);
+ }
+ },
+ ext: {
+ autocomplete: {
+ selectFromDropdown: function() {
+ this.trigger('hideDropdown');
+ var index = Number(this.selectedSuggestionElement().children().children().data('index'));
+ var data = self.search_result[index];
+ if (data.id) {
+ self.add_id(data.id);
+ } else {
+ self.ignore_blur = true;
+ data.action();
+ }
+ this.trigger('setSuggestions', {result : []});
+ },
+ },
+ tags: {
+ isTagAllowed: function(tag) {
+ return !!tag.name;
+
+ },
+ removeTag: function(tag) {
+ var id = tag.data("id");
+ self.set({"value": _.without(self.get("value"), id)});
+ },
+ renderTag: function(stuff) {
+ return $.fn.textext.TextExtTags.prototype.renderTag.
+ call(this, stuff).data("id", stuff.id);
+ },
+ },
+ itemManager: {
+ itemToString: function(item) {
+ return item.name;
+ },
+ },
+ core: {
+ onSetInputData: function(e, data) {
+ if (data === '') {
+ this._plugins.autocomplete._suggestions = null;
+ }
+ this.input().val(data);
+ },
+ },
+ },
+ };
+ },
+ initialize_content: function() {
+ if (this.get("effective_readonly"))
+ return;
+ var self = this;
+ self.ignore_blur = false;
+ self.$text = this.$("textarea");
+ self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
+ var _this = this;
+ var str = !!data ? data.query || '' : '';
+ self.get_search_result(str).done(function(result) {
+ self.search_result = result;
+ $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
+ return _.extend(el, {index:i});
+ })});
+ });
+ }).bind('hideDropdown', function() {
+ self._drop_shown = false;
+ }).bind('showDropdown', function() {
+ self._drop_shown = true;
+ });
+ self.tags = self.$text.textext()[0].tags();
+ self.$text
+ .focusin(function () {
+ self.trigger('focused');
+ self.ignore_blur = false;
+ })
+ .focusout(function() {
+ self.$text.trigger("setInputData", "");
+ if (!self.ignore_blur) {
+ self.trigger('blurred');
+ }
+ }).keydown(function(e) {
+ if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
+ self.$text.textext()[0].autocomplete().selectFromDropdown();
+ }
+ });
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ is_false: function() {
+ return _(this.get("value")).isEmpty();
+ },
+ get_value: function() {
+ var tmp = [commands.replace_with(this.get("value"))];
+ return tmp;
+ },
+ get_search_blacklist: function() {
+ return this.get("value");
+ },
+ map_tag: function(data){
+ return _.map(data, function(el) {return {name: el[1], id:el[0]};});
+ },
+ get_render_data: function(ids){
+ var self = this;
+ var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context());
+ return dataset.name_get(ids);
+ },
+ render_tag: function(data) {
+ var self = this;
+ if (! self.get("effective_readonly")) {
+ self.tags.containerElement().children().remove();
+ self.$('textarea').css("padding-left", "3px");
+ self.tags.addTags(self.map_tag(data));
+ } else {
+ self.$el.html(QWeb.render(self.tag_template, {elements: data}));
+ }
+ },
+ render_value: function() {
+ var self = this;
+ var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context());
+ var values = self.get("value");
+ var handle_names = function(data) {
+ if (self.isDestroyed())
+ return;
+ var indexed = {};
+ _.each(data, function(el) {
+ indexed[el[0]] = el;
+ });
+ data = _.map(values, function(el) { return indexed[el]; });
+ self.render_tag(data);
+ };
+ if (! values || values.length > 0) {
+ return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
+ } else {
+ handle_names([]);
+ }
+ },
+ add_id: function(id) {
+ this.set({'value': _.uniq(this.get('value').concat([id]))});
+ },
+ focus: function () {
+ var input = this.$text && this.$text[0];
+ return input ? input.focus() : false;
+ },
+ set_dimensions: function (height, width) {
+ this._super(height, width);
+ this.$("textarea").css({
+ width: width,
+ minHeight: height
+ });
+ },
+ _search_create_popup: function() {
+ this.ignore_blur = true;
+ return common.CompletionFieldMixin._search_create_popup.apply(this, arguments);
+ },
+});
+
+/**
+ widget options:
+ - reload_on_button: Reload the whole form view if click on a button in a list view.
+ If you see this options, do not use it, it's basically a dirty hack to make one
+ precise o2m to behave the way we want.
+*/
+var FieldMany2Many = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ multi_selection: false,
+ disable_utility_classes: true,
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.is_loaded = $.Deferred();
+ this.dataset = new Many2ManyDataSet(this, this.field.relation);
+ this.dataset.m2m = this;
+ var self = this;
+ this.dataset.on('unlink', self, function(ids) {
+ self.dataset_changed();
+ });
+ this.set_value([]);
+ this.list_dm = new utils.DropMisordered();
+ this.render_value_dm = new utils.DropMisordered();
+ },
+ initialize_content: function() {
+ var self = this;
+
+ this.$el.addClass('oe_form_field oe_form_field_many2many');
+
+ this.list_view = new Many2ManyListView(this, this.dataset, false, {
+ 'addable': null,
+ 'deletable': this.get("effective_readonly") ? false : true,
+ 'selectable': this.multi_selection,
+ 'sortable': false,
+ 'reorderable': false,
+ 'import_enabled': false,
+ });
+ var embedded = (this.field.views || {}).tree;
+ if (embedded) {
+ this.list_view.set_embedded_view(embedded);
+ }
+ this.list_view.m2m_field = this;
+ var loaded = $.Deferred();
+ this.list_view.on("list_view_loaded", this, function() {
+ loaded.resolve();
+ });
+ this.list_view.appendTo(this.$el);
+
+ var old_def = self.is_loaded;
+ self.is_loaded = $.Deferred().done(function() {
+ old_def.resolve();
+ });
+ this.list_dm.add(loaded).then(function() {
+ self.is_loaded.resolve();
+ });
+ },
+ destroy_content: function() {
+ this.list_view.destroy();
+ this.list_view = undefined;
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ get_value: function() {
+ return [commands.replace_with(this.get('value'))];
+ },
+ is_false: function () {
+ return _(this.get("value")).isEmpty();
+ },
+ render_value: function() {
+ var self = this;
+ this.dataset.set_ids(this.get("value"));
+ this.render_value_dm.add(this.is_loaded).then(function() {
+ return self.list_view.reload_content();
+ });
+ },
+ dataset_changed: function() {
+ this.internal_set_value(this.dataset.ids);
+ },
+});
+
+var Many2ManyDataSet = data.DataSetStatic.extend({
+ get_context: function() {
+ this.context = this.m2m.build_context();
+ return this.context;
+ }
+});
+
+/**
+ * @class
+ * @extends instance.web.ListView
+ */
+var Many2ManyListView = ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
+ init: function (parent, dataset, view_id, options) {
+ this._super(parent, dataset, view_id, _.extend(options || {}, {
+ ListType: Many2ManyList,
+ }));
+ },
+ do_add_record: function () {
+ var pop = new common.SelectCreatePopup(this);
+ pop.select_element(
+ this.model,
+ {
+ title: _t("Add: ") + this.m2m_field.string,
+ alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views.form : undefined,
+ no_create: this.m2m_field.options.no_create,
+ },
+ new data.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
+ this.m2m_field.build_context()
+ );
+ var self = this;
+ pop.on("elements_selected", self, function(element_ids) {
+ var reload = false;
+ _(element_ids).each(function (id) {
+ if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
+ self.dataset.set_ids(self.dataset.ids.concat([id]));
+ self.m2m_field.dataset_changed();
+ reload = true;
+ }
+ });
+ if (reload) {
+ self.reload_content();
+ }
+ });
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new common.FormOpenPopup(this);
+ pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
+ title: _t("Open: ") + this.m2m_field.string,
+ alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views.form : undefined,
+ readonly: this.getParent().get("effective_readonly")
+ });
+ pop.on('write_completed', self, self.reload_content);
+ },
+ do_button_action: function(name, id, callback) {
+ var self = this;
+ var _sup = _.bind(this._super, this);
+ if (! this.m2m_field.options.reload_on_button) {
+ return _sup(name, id, callback);
+ } else {
+ return this.m2m_field.view.save().then(function() {
+ return _sup(name, id, function() {
+ self.m2m_field.view.reload();
+ });
+ });
+ }
+ },
+ is_action_enabled: function () { return true; },
+});
+var Many2ManyList = AddAnItemList.extend({
+ _add_row_class: 'oe_form_field_many2many_list_row_add',
+ is_readonly: function () {
+ return this.view.m2m_field.get('effective_readonly');
+ }
+});
+
+var FieldMany2ManyKanban = common.AbstractField.extend(common.CompletionFieldMixin, {
+ disable_utility_classes: true,
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ common.CompletionFieldMixin.init.call(this);
+ this.is_loaded = $.Deferred();
+ this.initial_is_loaded = this.is_loaded;
+
+ var self = this;
+ this.dataset = new Many2ManyDataSet(this, this.field.relation);
+ this.dataset.m2m = this;
+ this.dataset.on('unlink', self, function(ids) {
+ self.dataset_changed();
+ });
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+
+ var self = this;
+
+ self.load_view();
+ self.on("change:effective_readonly", self, function() {
+ self.is_loaded = self.is_loaded.then(function() {
+ self.kanban_view.destroy();
+ return $.when(self.load_view()).done(function() {
+ self.render_value();
+ });
+ });
+ });
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ get_value: function() {
+ return [commands.replace_with(this.get('value'))];
+ },
+ load_view: function() {
+ var self = this;
+ var Many2ManyKanbanView = core.view_registry.get('many2many_kanban');
+ this.kanban_view = new Many2ManyKanbanView(this, this.dataset, false, {
+ 'create_text': _t("Add"),
+ 'creatable': self.get("effective_readonly") ? false : true,
+ 'quick_creatable': self.get("effective_readonly") ? false : true,
+ 'read_only_mode': self.get("effective_readonly") ? true : false,
+ 'confirm_on_delete': false,
+ });
+ var embedded = (this.field.views || {}).kanban;
+ if (embedded) {
+ this.kanban_view.set_embedded_view(embedded);
+ }
+ this.kanban_view.m2m = this;
+ var loaded = $.Deferred();
+ this.kanban_view.on("kanban_view_loaded",self,function() {
+ self.initial_is_loaded.resolve();
+ loaded.resolve();
+ });
+ this.kanban_view.on('switch_mode', this, this.open_popup);
+ $.async_when().done(function () {
+ self.kanban_view.appendTo(self.$el);
+ });
+ return loaded;
+ },
+ render_value: function() {
+ var self = this;
+ this.dataset.set_ids(this.get("value"));
+ this.is_loaded = this.is_loaded.then(function() {
+ return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
+ });
+ },
+ dataset_changed: function() {
+ this.set({'value': this.dataset.ids});
+ },
+ open_popup: function(type, unused) {
+ if (type !== "form")
+ return;
+ var self = this;
+ var pop;
+ if (this.dataset.index === null) {
+ pop = new common.SelectCreatePopup(this);
+ pop.select_element(
+ this.field.relation,
+ {
+ title: _t("Add: ") + this.string
+ },
+ new data.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
+ this.build_context()
+ );
+ pop.on("elements_selected", self, function(element_ids) {
+ _.each(element_ids, function(one_id) {
+ if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
+ self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
+ self.dataset_changed();
+ self.render_value();
+ }
+ });
+ });
+ } else {
+ var id = self.dataset.ids[self.dataset.index];
+ pop = new common.FormOpenPopup(this);
+ pop.show_element(self.field.relation, id, self.build_context(), {
+ title: _t("Open: ") + self.string,
+ write_function: function(id, data, options) {
+ return self.dataset.write(id, data, {}).done(function() {
+ self.render_value();
+ });
+ },
+ alternative_form_view: self.field.views ? self.field.views.form : undefined,
+ parent_view: self.view,
+ child_name: self.name,
+ readonly: self.get("effective_readonly")
+ });
+ }
+ },
+ add_id: function(id) {
+ this.quick_create.add_id(id);
+ },
+});
+
+/**
+ * Widget for (many2many field) to upload one or more file in same time and display in list.
+ * The user can delete his files.
+ * Options on attribute ; "blockui" {Boolean} block the UI or not
+ * during the file is uploading
+ */
+var FieldMany2ManyBinaryMultiFiles = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ template: "FieldBinaryFileUploader",
+ init: function(field_manager, node) {
+ this._super(field_manager, node);
+ this.field_manager = field_manager;
+ this.node = node;
+ this.session = session;
+ if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
+ throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
+ }
+ this.data = {};
+ this.set_value([]);
+ this.ds_file = new data.DataSetSearch(this, 'ir.attachment');
+ this.fileupload_id = _.uniqueId('oe_fileupload_temp');
+ $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
+ },
+ initialize_content: function() {
+ this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
+ },
+ // WARNING: duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ this._super(value_);
+ },
+ get_value: function() {
+ return [commands.replace_with(this.get("value"))];
+ },
+ get_file_url: function (attachment) {
+ return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment.id});
+ },
+ read_name_values : function () {
+ var self = this;
+ // don't reset know values
+ var ids = this.get('value');
+ var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
+ // send request for get_name
+ if (_value.length) {
+ return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
+ _.each(datas, function (data) {
+ data.no_unlink = true;
+ data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
+ self.data[data.id] = data;
+ });
+ return ids;
+ });
+ } else {
+ return $.when(ids);
+ }
+ },
+ render_value: function () {
+ var self = this;
+ this.read_name_values().then(function (ids) {
+ var render = $(QWeb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
+ render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
+ self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
+
+ // reinit input type file
+ var $input = self.$('input.oe_form_binary_file');
+ $input.after($input.clone(true)).remove();
+ self.$(".oe_fileupload").show();
+
+ });
+ },
+ on_file_change: function (event) {
+ event.stopPropagation();
+ var self = this;
+ var $target = $(event.target);
+ if ($target.val() !== '') {
+ var filename = $target.val().replace(/.*[\\\/]/,'');
+ // don't uplode more of one file in same time
+ if (self.data[0] && self.data[0].upload ) {
+ return false;
+ }
+ for (var id in this.get('value')) {
+ // if the files exits, delete the file before upload (if it's a new file)
+ if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
+ self.ds_file.unlink([id]);
+ }
+ }
+
+ // block UI or not
+ if(this.node.attrs.blockui>0) {
+ framework.blockUI();
+ }
+
+ // TODO : unactivate send on wizard and form
+
+ // submit file
+ this.$('form.oe_form_binary_form').submit();
+ this.$(".oe_fileupload").hide();
+ // add file on data result
+ this.data[0] = {
+ 'id': 0,
+ 'name': filename,
+ 'filename': filename,
+ 'url': '',
+ 'upload': true
+ };
+ }
+ },
+ on_file_loaded: function (event, result) {
+ var files = this.get('value');
+
+ // unblock UI
+ if(this.node.attrs.blockui>0) {
+ framework.unblockUI();
+ }
+
+ if (result.error || !result.id ) {
+ this.do_warn( _t('Uploading Error'), result.error);
+ delete this.data[0];
+ } else {
+ if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
+ delete this.data[0];
+ this.data[result.id] = {
+ 'id': result.id,
+ 'name': result.name,
+ 'filename': result.filename,
+ 'url': this.get_file_url(result)
+ };
+ } else {
+ this.data[result.id] = {
+ 'id': result.id,
+ 'name': result.name,
+ 'filename': result.filename,
+ 'url': this.get_file_url(result)
+ };
+ }
+ var values = _.clone(this.get('value'));
+ values.push(result.id);
+ this.set({'value': values});
+ }
+ this.render_value();
+ },
+ on_file_delete: function (event) {
+ event.stopPropagation();
+ var file_id=$(event.target).data("id");
+ if (file_id) {
+ var files = _.filter(this.get('value'), function (id) {return id != file_id;});
+ if(!this.data[file_id].no_unlink) {
+ this.ds_file.unlink([file_id]);
+ }
+ this.set({'value': files});
+ }
+ },
+});
+
+/*
+ This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
+ record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
+ will be added to the relation.
+*/
+var FieldMany2ManyCheckBoxes = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ className: "oe_form_many2many_checkboxes",
+ init: function() {
+ this._super.apply(this, arguments);
+ this.set("value", {});
+ this.set("records", []);
+ this.field_manager.on("view_content_has_changed", this, function() {
+ var domain = new data.CompoundDomain(this.build_domain()).eval();
+ if (! _.isEqual(domain, this.get("domain"))) {
+ this.set("domain", domain);
+ }
+ });
+ this.records_orderer = new utils.DropMisordered();
+ },
+ initialize_field: function() {
+ common.ReinitializeFieldMixin.initialize_field.call(this);
+ this.on("change:domain", this, this.query_records);
+ this.set("domain", new data.CompoundDomain(this.build_domain()).eval());
+ this.on("change:records", this, this.render_value);
+ },
+ query_records: function() {
+ var self = this;
+ var model = new Model(this.field.relation);
+ this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
+ return model.call("name_get", [record_ids] , {"context": self.build_context()});
+ })).then(function(res) {
+ self.set("records", res);
+ });
+ },
+ render_value: function() {
+ this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
+ var inputs = this.$("input");
+ inputs.change(_.bind(this.from_dom, this));
+ if (this.get("effective_readonly"))
+ inputs.attr("disabled", "true");
+ },
+ from_dom: function() {
+ var new_value = {};
+ this.$("input").each(function() {
+ var elem = $(this);
+ new_value[elem.data("record-id")] = elem.is(":checked") ? true : undefined;
+ });
+ if (! _.isEqual(new_value, this.get("value")))
+ this.internal_set_value(new_value);
+ },
+ // WARNING: (mostly) duplicated in 4 other M2M widgets
+ set_value: function(value_) {
+ value_ = value_ || [];
+ if (value_.length >= 1 && value_[0] instanceof Array) {
+ // value_ is a list of m2m commands. We only process
+ // LINK_TO and REPLACE_WITH in this context
+ var val = [];
+ _.each(value_, function (command) {
+ if (command[0] === commands.LINK_TO) {
+ val.push(command[1]); // (4, id[, _])
+ } else if (command[0] === commands.REPLACE_WITH) {
+ val = command[2]; // (6, _, ids)
+ }
+ });
+ value_ = val;
+ }
+ var formatted = {};
+ _.each(value_, function(el) {
+ formatted[JSON.stringify(el)] = true;
+ });
+ this._super(formatted);
+ },
+ get_value: function() {
+ var value = _.filter(_.keys(this.get("value")), function(el) {
+ return this.get("value")[el];
+ }, this);
+ value = _.map(value, function(el) {
+ return JSON.parse(el);
+ });
+ return [commands.replace_with(value)];
+ },
+});
+
+/**
+ This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
+ "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
+ action on the model of the relation and show only the linked records.
+
+ Widget options:
+
+ * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
+ to display (or False to take the default one) and the second element is the type of the view. Defaults to
+ [[false, "tree"], [false, "form"]] .
+*/
+var X2ManyCounter = common.AbstractField.extend(common.ReinitializeFieldMixin, {
+ className: "oe_form_x2many_counter",
+ init: function() {
+ this._super.apply(this, arguments);
+ this.set("value", []);
+ _.defaults(this.options, {
+ "views": [[false, "tree"], [false, "form"]],
+ });
+ },
+ render_value: function() {
+ var text = _.str.sprintf("%d %s", this.val().length, this.string);
+ this.$().html(QWeb.render("X2ManyCounter", {text: text}));
+ this.$("a").click(_.bind(this.go_to, this));
+ },
+ go_to: function() {
+ return this.view.recursive_save().then(_.bind(function() {
+ var val = this.val();
+ var context = {};
+ if (this.field.type === "one2many") {
+ context["default_" + this.field.relation_field] = this.view.datarecord.id;
+ }
+ var domain = [["id", "in", val]];
+ return this.do_action({
+ type: 'ir.actions.act_window',
+ name: this.string,
+ res_model: this.field.relation,
+ views: this.options.views,
+ target: 'current',
+ context: context,
+ domain: domain,
+ });
+ }, this));
+ },
+ val: function() {
+ var value = this.get("value") || [];
+ if (value.length >= 1 && value[0] instanceof Array) {
+ value = value[0][2];
+ }
+ return value;
+ }
+});
+
+
+
+core.form_widget_registry
+ .add('many2one', FieldMany2One)
+ .add('many2onebutton', Many2OneButton)
+ .add('many2many', FieldMany2Many)
+ .add('many2many_tags', FieldMany2ManyTags)
+ .add('many2many_kanban', FieldMany2ManyKanban)
+ .add('one2many', FieldOne2Many)
+ .add('one2many_list', FieldOne2Many)
+ .add('many2many_binary', FieldMany2ManyBinaryMultiFiles)
+ .add('many2many_checkboxes', FieldMany2ManyCheckBoxes)
+ .add('x2many_counter', X2ManyCounter);
+
+return {
+ FieldMany2ManyTags: FieldMany2ManyTags,
+};
+
+});
+
+odoo.define('web_kanban.Many2ManyKanbanView', ['web.core', 'web_kanban.KanbanView', 'web.Widget'], function (require) {
+ "use strict";
+ // This code has a dependency on the addon web_kanban. This is a weird dependency issue. To fix it,
+ // we should either move this code into web_kanban, or move web_kanban into the web client.
+
+ var core = require('web.core'),
+ KanbanView = require('web_kanban.KanbanView'),
+ Widget = require('web.Widget');
+
+ var Many2ManyQuickCreate = Widget.extend({
+ template: 'Many2ManyKanban.quick_create',
+
+ /**
+ * close_btn: If true, the widget will display a "Close" button able to trigger
+ * a "close" event.
+ */
+ init: function(parent, dataset, context, buttons) {
+ this._super(parent);
+ this.m2m = this.getParent().view.m2m;
+ this.m2m.quick_create = this;
+ this._dataset = dataset;
+ this._buttons = buttons || false;
+ this._context = context || {};
+ },
+ start: function () {
+ var self = this;
+ self.$text = this.$el.find('input').css("width", "200px");
+ self.$text.textext({
+ plugins : 'arrow autocomplete',
+ autocomplete: {
+ render: function(suggestion) {
+ return $('<span class="text-label"/>').
+ data('index', suggestion.index).html(suggestion.label);
+ }
+ },
+ ext: {
+ autocomplete: {
+ selectFromDropdown: function() {
+ $(this).trigger('hideDropdown');
+ var index = Number(this.selectedSuggestionElement().children().children().data('index'));
+ var data = self.search_result[index];
+ if (data.id) {
+ self.add_id(data.id);
+ } else {
+ data.action();
+ }
+ },
+ },
+ itemManager: {
+ itemToString: function(item) {
+ return item.name;
+ },
+ },
+ },
+ }).bind('getSuggestions', function(e, data) {
+ var _this = this;
+ var str = !!data ? data.query || '' : '';
+ self.m2m.get_search_result(str).done(function(result) {
+ self.search_result = result;
+ $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
+ return _.extend(el, {index:i});
+ })});
+ });
+ });
+ self.$text.focusout(function() {
+ self.$text.val("");
+ });
+ },
+ focus: function() {
+ this.$text[0].focus();
+ },
+ add_id: function(id) {
+ var self = this;
+ self.$text.val("");
+ self.trigger('added', id);
+ this.m2m.dataset_changed();
+ },
+ });
+
+ var Many2ManyKanbanView = KanbanView.extend({
+ get_quick_create_class: function () {
+ return Many2ManyQuickCreate;
+ },
+ _is_quick_create_enabled: function() {
+ return this._super() && ! this.group_by;
+ },
+ });
+
+ core.view_registry.add('many2many_kanban', Many2ManyKanbanView);
+
+});