diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index deff2fbebcf..500ebf1f609 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -22,6 +22,7 @@ var data = require('web.data');
var Dialog = require('web.Dialog');
var dialogs = require('web.view_dialogs');
var dom = require('web.dom');
+const Domain = require('web.Domain');
var KanbanRecord = require('web.KanbanRecord');
var KanbanRenderer = require('web.KanbanRenderer');
var ListRenderer = require('web.ListRenderer');
@@ -117,21 +118,24 @@ var FieldMany2One = AbstractField.extend({
* @override
* @param {boolean} [options.noOpen=false] if true, there is no external
* button to open the related record in a dialog
+ * @param {boolean} [options.noCreate=false] if true, the many2one does not
+ * allow to create records
*/
init: function (parent, name, record, options) {
+ options = options || {};
this._super.apply(this, arguments);
this.limit = 7;
this.orderer = new concurrency.DropMisordered();
- // should normally also be set, except in standalone M20
- this.can_create = ('can_create' in this.attrs ? JSON.parse(this.attrs.can_create) : true) &&
- !this.nodeOptions.no_create;
+ // should normally be set, except in standalone M20
+ const canCreate = 'can_create' in this.attrs ? JSON.parse(this.attrs.can_create) : true;
+ this.can_create = canCreate && !this.nodeOptions.no_create && !options.noCreate;
this.can_write = 'can_write' in this.attrs ? JSON.parse(this.attrs.can_write) : true;
this.nodeOptions = _.defaults(this.nodeOptions, {
quick_create: true,
});
- this.noOpen = 'noOpen' in (options || {}) ? options.noOpen : this.nodeOptions.no_open;
+ this.noOpen = 'noOpen' in options ? options.noOpen : this.nodeOptions.no_open;
this.m2o_value = this._formatValue(this.value);
// 'recordParams' is a dict of params used when calling functions
// 'getDomain' and 'getContext' on this.record
@@ -591,10 +595,9 @@ var FieldMany2One = AbstractField.extend({
if (values.length > self.limit) {
values = self._manageSearchMore(values, search_val, domain, context);
}
- var create_enabled = self.can_create && !self.nodeOptions.no_create;
// quick create
var raw_result = _.map(result, function (x) { return x[1]; });
- if (create_enabled && !self.nodeOptions.no_quick_create &&
+ if (self.can_create && !self.nodeOptions.no_quick_create &&
search_val.length > 0 && !_.contains(raw_result, search_val)) {
values.push({
label: _.str.sprintf(_t('Create "%s"'),
@@ -604,7 +607,7 @@ var FieldMany2One = AbstractField.extend({
});
}
// create and edit ...
- if (create_enabled && !self.nodeOptions.no_create_edit) {
+ if (self.can_create && !self.nodeOptions.no_create_edit) {
var createAndEditAction = function () {
// Clear the value in case the user clicks on discard
self.$('input').val('');
@@ -1012,6 +1015,7 @@ var FieldX2Many = AbstractField.extend({
true;
this.editable = arch.attrs.editable;
}
+ this._computeAvailableActions(record);
if (this.attrs.columnInvisibleFields) {
this._processColumnInvisibleFields();
}
@@ -1079,15 +1083,21 @@ var FieldX2Many = AbstractField.extend({
* @returns {Promise}
*/
reset: function (record, ev, fieldChanged) {
+ // re-evaluate available actions
+ const oldCanCreate = this.canCreate;
+ const oldCanDelete = this.canDelete;
+ this._computeAvailableActions(record);
+ const actionsChanged = this.canCreate !== oldCanCreate || this.canDelete !== oldCanDelete;
+
// If 'fieldChanged' is false, it means that the reset was triggered by
- // the 'resetOnAnyFieldChange' mechanism. If it is the case the
- // modifiers are evaluated and if there is no change in the modifiers
- // values, the reset is skipped.
- if (!fieldChanged) {
- var newEval = this._evalColumnInvisibleFields();
- if (_.isEqual(this.currentColInvisibleFields, newEval)) {
- return Promise.resolve();
- }
+ // the 'resetOnAnyFieldChange' mechanism. If it is the case, if neither
+ // the modifiers (so the visible columns) nor the available actions
+ // changed, the reset is skipped.
+ if (!fieldChanged && !actionsChanged) {
+ var newEval = this._evalColumnInvisibleFields();
+ if (_.isEqual(this.currentColInvisibleFields, newEval)) {
+ return Promise.resolve();
+ }
} else if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') {
var command = ev.data.changes[this.name];
// Here, we only consider 'UPDATE' commands with data, which occur
@@ -1104,7 +1114,9 @@ var FieldX2Many = AbstractField.extend({
return this.renderer.confirmUpdate(state, command.id, fieldNames, ev.initialEvent);
}
}
- return this._super.apply(this, arguments);
+ return this._super.apply(this, arguments).then(() => {
+ this._updateControlPanel();
+ });
},
/**
@@ -1141,6 +1153,19 @@ var FieldX2Many = AbstractField.extend({
// Private
//--------------------------------------------------------------------------
+ /**
+ * @private
+ * @param {Object} record
+ */
+ _computeAvailableActions: function (record) {
+ const evalContext = record.evalContext;
+ this.canCreate = 'create' in this.nodeOptions ?
+ new Domain(this.nodeOptions.create, evalContext).compute(evalContext) :
+ true;
+ this.canDelete = 'delete' in this.nodeOptions ?
+ new Domain(this.nodeOptions.delete, evalContext).compute(evalContext) :
+ true;
+ },
/**
* Evaluates the 'column_invisible' modifier for the parent record.
*
@@ -1155,6 +1180,18 @@ var FieldX2Many = AbstractField.extend({
}).column_invisible;
});
},
+ /**
+ * Returns qweb context to render buttons.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _getButtonsRenderingContext() {
+ return {
+ btnClass: 'btn-secondary',
+ create_text: this.nodeOptions.create_text,
+ };
+ },
/**
* Computes the default renderer to use depending on the view type.
* We create this as a method so we can override it if we want to use
@@ -1171,6 +1208,20 @@ var FieldX2Many = AbstractField.extend({
return KanbanRenderer;
}
},
+ /**
+ * @private
+ * @returns {boolean} true iff the list should contain a 'create' line.
+ */
+ _hasCreateLine: function () {
+ return !this.isReadonly && this.activeActions.create && (this.isMany2Many || this.canCreate);
+ },
+ /**
+ * @private
+ * @returns {boolean} true iff the list should add a trash icon on each row.
+ */
+ _hasTrashIcon: function () {
+ return !this.isReadonly && this.activeActions.delete && (this.isMany2Many || this.canDelete);
+ },
/**
* Instanciates or updates the adequate renderer.
*
@@ -1183,9 +1234,12 @@ var FieldX2Many = AbstractField.extend({
if (!this.view) {
return this._super();
}
+
if (this.renderer) {
this.currentColInvisibleFields = this._evalColumnInvisibleFields();
return this.renderer.updateState(this.value, {
+ addCreateLine: this._hasCreateLine(),
+ addTrashIcon: this._hasTrashIcon(),
columnInvisibleFields: this.currentColInvisibleFields,
keepWidths: true,
}).then(function () {
@@ -1203,8 +1257,8 @@ var FieldX2Many = AbstractField.extend({
this.currentColInvisibleFields = this._evalColumnInvisibleFields();
_.extend(rendererParams, {
editable: this.mode === 'edit' && arch.attrs.editable,
- addCreateLine: !this.isReadonly && this.activeActions.create,
- addTrashIcon: !this.isReadonly && this.activeActions.delete,
+ addCreateLine: this._hasCreateLine(),
+ addTrashIcon: this._hasTrashIcon(),
isMany2Many: this.isMany2Many,
columnInvisibleFields: this.currentColInvisibleFields,
});
@@ -1301,10 +1355,8 @@ var FieldX2Many = AbstractField.extend({
*/
_renderButtons: function () {
if (!this.isReadonly && this.view.arch.tag === 'kanban') {
- this.$buttons = $(qweb.render('KanbanView.buttons', {
- btnClass: 'btn-secondary',
- create_text: this.nodeOptions.create_text,
- }));
+ const renderingContext = this._getButtonsRenderingContext();
+ this.$buttons = $(qweb.render('KanbanView.buttons', renderingContext));
this.$buttons.on('click', 'button.o-kanban-button-new', this._onAddRecord.bind(this));
}
},
@@ -1347,6 +1399,23 @@ var FieldX2Many = AbstractField.extend({
}
});
},
+ /**
+ * Re-renders buttons and updates the control panel. This method is called
+ * when the widget is reset, as the available buttons might have changed.
+ *
+ * @private
+ */
+ _updateControlPanel: function () {
+ if (this._controlPanel) {
+ this._renderButtons();
+ const params = {
+ cp_content: {
+ $buttons: this.$buttons,
+ }
+ };
+ this._controlPanel.updateContents(params, { clear: false });
+ }
+ },
/**
* Parses the 'columnInvisibleFields' attribute to search for the domains
* containing the key 'parent'. If there are such domains, the string
@@ -1729,6 +1798,15 @@ var FieldOne2Many = FieldX2Many.extend({
// Private
//--------------------------------------------------------------------------
+ /**
+ * @override
+ * @private
+ */
+ _getButtonsRenderingContext() {
+ const renderingContext = this._super(...arguments);
+ renderingContext.noCreate = !this.canCreate;
+ return renderingContext;
+ },
/**
* @override
* @private
@@ -1769,7 +1847,7 @@ var FieldOne2Many = FieldX2Many.extend({
fields_view: this.attrs.views && this.attrs.views.form,
parentID: this.value.id,
viewInfo: this.view,
- deletable: this.activeActions.delete && params.deletable,
+ deletable: this.activeActions.delete && params.deletable && this.canDelete,
}));
},
@@ -1874,7 +1952,7 @@ var FieldOne2Many = FieldX2Many.extend({
on_remove: function () {
self._setValue({operation: 'DELETE', ids: [id]});
},
- deletable: this.activeActions.delete && this.view.arch.tag !== 'tree',
+ deletable: this.activeActions.delete && this.view.arch.tag !== 'tree' && this.canDelete,
readonly: this.mode === 'readonly',
});
},
@@ -1900,7 +1978,7 @@ var FieldMany2Many = FieldX2Many.extend({
domain: domain.concat(["!", ["id", "in", this.value.res_ids]]),
context: this.record.getContext(this.recordParams),
title: _t("Add: ") + this.string,
- no_create: this.nodeOptions.no_create || !this.activeActions.create,
+ no_create: this.nodeOptions.no_create || !this.activeActions.create || !this.canCreate,
fields_view: this.attrs.views.form,
kanban_view_ref: this.attrs.kanban_view_ref,
on_selected: function (records) {
@@ -1960,7 +2038,7 @@ var FieldMany2Many = FieldX2Many.extend({
self._setValue({operation: 'FORGET', ids: [ev.data.id]});
},
readonly: this.mode === 'readonly',
- deletable: this.activeActions.delete && this.view.arch.tag !== 'tree',
+ deletable: this.activeActions.delete && this.view.arch.tag !== 'tree' && this.canDelete,
string: this.string,
});
},
@@ -2195,6 +2273,10 @@ var FieldMany2ManyTags = AbstractField.extend({
this.colorField = this.nodeOptions.color_field;
this.hasDropdown = false;
+
+ this._computeAvailableActions(this.record);
+ // have listen to react to other fields changes to re-evaluate 'create' option
+ this.resetOnAnyFieldChange = this.resetOnAnyFieldChange || 'create' in this.nodeOptions;
},
//--------------------------------------------------------------------------
@@ -2228,7 +2310,8 @@ var FieldMany2ManyTags = AbstractField.extend({
*/
reset: function (record, event) {
var self = this;
- return this._super.apply(this, arguments).then(function(){
+ this._computeAvailableActions(record);
+ return this._super.apply(this, arguments).then(function () {
if (event && event.target === self) {
self.activate();
}
@@ -2251,6 +2334,16 @@ var FieldMany2ManyTags = AbstractField.extend({
});
}
},
+ /**
+ * @private
+ * @param {Object} record
+ */
+ _computeAvailableActions: function (record) {
+ const evalContext = record.evalContext;
+ this.canCreate = 'create' in this.nodeOptions ?
+ new Domain(this.nodeOptions.create, evalContext).compute(evalContext) :
+ true;
+ },
/**
* Get the QWeb rendering context used by the tag template; this computation
* is placed in a separate function for other tags to override it.
@@ -2290,6 +2383,7 @@ var FieldMany2ManyTags = AbstractField.extend({
this.many2one = new FieldMany2One(this, this.name, this.record, {
mode: 'edit',
noOpen: true,
+ noCreate: !this.canCreate,
viewType: this.viewType,
attrs: this.attrs,
});
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 62f715d3967..53c63d3fbb9 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -42,7 +42,6 @@ ListRenderer.include({
* @param {boolean} params.isMultiEditable
*/
init: function (parent, state, params) {
- var self = this;
this._super.apply(this, arguments);
this.editable = params.editable;
@@ -62,33 +61,27 @@ ListRenderer.include({
// The following code will browse the arch to find
// all the that are inside
-
- if (this.addCreateLine) {
- this.creates = [];
-
- _.each(this.arch.children, function (child) {
- if (child.tag !== 'control') {
+ this.creates = [];
+ this.arch.children.forEach(child => {
+ if (child.tag !== 'control') {
+ return;
+ }
+ child.children.forEach(child => {
+ if (child.tag !== 'create' || child.attrs.invisible) {
return;
}
-
- _.each(child.children, function (child) {
- if (child.tag !== 'create' || child.attrs.invisible) {
- return;
- }
-
- self.creates.push({
- context: child.attrs.context,
- string: child.attrs.string,
- });
+ this.creates.push({
+ context: child.attrs.context,
+ string: child.attrs.string,
});
});
+ });
- // Add the default button if we didn't find any custom button.
- if (this.creates.length === 0) {
- this.creates.push({
- string: _t("Add a line"),
- });
- }
+ // Add the default button if we didn't find any custom button.
+ if (this.creates.length === 0) {
+ this.creates.push({
+ string: _t("Add a line"),
+ });
}
// if addTrashIcon is true, there will be a small trash icon at the end
@@ -532,6 +525,15 @@ ListRenderer.include({
// them to be recomputed at next (sub-)rendering
this.allModifiersData = [];
}
+ if ('addTrashIcon' in params) {
+ if (this.addTrashIcon !== params.addTrashIcon) {
+ this.columnWidths = false; // columns changed, so forget stored widths
+ }
+ this.addTrashIcon = params.addTrashIcon;
+ }
+ if ('addCreateLine' in params) {
+ this.addCreateLine = params.addCreateLine;
+ }
return this._super.apply(this, arguments);
},
@@ -1200,15 +1202,17 @@ ListRenderer.include({
$tr.append($td);
$rows.push($tr);
- _.each(this.creates, function (create, index) {
- var $a = $('')
- .attr('data-context', create.context)
- .text(create.string);
- if (index > 0) {
- $a.addClass('ml16');
- }
- $td.append($a);
- });
+ if (this.addCreateLine) {
+ _.each(this.creates, function (create, index) {
+ var $a = $('')
+ .attr('data-context', create.context)
+ .text(create.string);
+ if (index > 0) {
+ $a.addClass('ml16');
+ }
+ $td.append($a);
+ });
+ }
}
return $rows;
},
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index 9d998677dc4..1e7f36f8ef3 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -2,7 +2,7 @@
-
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
index 487cd8647b8..7af0216e20e 100644
--- a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
@@ -350,6 +350,74 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test('many2many kanban: conditional create/delete actions', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ `,
+ archs: {
+ 'partner_type,false,form': '',
+ 'partner_type,false,list': '',
+ 'partner_type,false,search': '',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red
+ assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available');
+
+ await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)'));
+ assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove',
+ 'remove button should be visible in modal');
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons available in the modal');
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+ assert.containsOnce(form, '.o-kanban-button-new',
+ '"Add" button should still be available even after color field changed');
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ // only select and cancel button should be available, create
+ // button should be removed based on color field condition
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ '"Create" button should not be available in the modal after color field changed');
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)'));
+ assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove',
+ 'remove button should be visible in modal');
+
+ form.destroy();
+ });
+
QUnit.test('many2many list (non editable): edition', async function (assert) {
assert.expect(29);
@@ -642,6 +710,64 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test('many2many list: conditional create/delete actions', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ `,
+ archs: {
+ 'partner_type,false,list': '',
+ 'partner_type,false,search': '',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red -> create and delete actions are available
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2,
+ "should have two remove icons");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons available in the modal');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> create and delete actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ // add a line and remove icon should still be there as they don't create/delete records,
+ // but rather add/remove links
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ '"Add a line" button should still be available even after color field changed');
+ assert.containsN(form, '.o_list_record_remove', 2,
+ "should still have remove icon even after color field changed");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ '"Create" button should not be available in the modal after color field changed');
+
+ form.destroy();
+ });
+
QUnit.test('many2many list: list of id as default value', async function (assert) {
assert.expect(1);
@@ -1232,6 +1358,95 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+
+ QUnit.test('many2many_tags widget: conditional create/delete actions', async function (assert) {
+ assert.expect(10);
+
+ this.data.turtle.records[0].partner_ids = [2];
+ for (var i = 1; i <= 10; i++) {
+ this.data.partner.records.push({ id: 100 + i, display_name: "Partner" + i });
+ }
+
+ const form = await createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch: `
+ `,
+ archs: {
+ 'partner,false,list': '',
+ 'partner,false,search': '',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // turtle_bar is true -> create and delete actions are available
+ assert.containsOnce(form, '.o_field_many2manytags.o_field_widget .badge .o_delete',
+ 'X icon on badges should not be available');
+
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+
+ const $dropdown1 = form.$('.o_field_many2one input').autocomplete('widget');
+ assert.containsOnce($dropdown1, 'li.o_m2o_dropdown_option:contains(Create and Edit...)',
+ 'autocomplete should contain Create and Edit...');
+
+ await testUtils.fields.many2one.clickItem('partner_ids', 'Search More');
+
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons (Select, Create and Cancel) available in the modal footer');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // type something that doesn't exist
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'Something that does not exist', 'keydown');
+ // await testUtils.nextTick();
+ assert.containsN($dropdown1, 'li.o_m2o_dropdown_option', 2,
+ 'autocomplete should contain Create and Create and Edit... options');
+
+ // set turtle_bar false -> create and delete actions are no longer available
+ await testUtils.dom.click(form.$('.o_field_widget[name="turtle_bar"] input').first());
+
+ // remove icon should still be there as it doesn't delete records but rather remove links
+ assert.containsOnce(form, '.o_field_many2manytags.o_field_widget .badge .o_delete',
+ 'X icon on badge should still be there even after turtle_bar is not checked');
+
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+ const $dropdown2 = form.$('.o_field_many2one input').autocomplete('widget');
+
+ // only Search More option should be available
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option',
+ 'autocomplete should contain only one option');
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option:contains(Search More)',
+ 'autocomplete option should be Search More');
+
+ await testUtils.fields.many2one.clickItem('partner_ids', 'Search More');
+
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ 'there should be 2 buttons (Select and Cancel) available in the modal footer');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // type something that doesn't exist
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'Something that does not exist', 'keyup');
+ // await testUtils.nextTick();
+
+ // only Search More option should be available
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option',
+ 'autocomplete should contain only one option');
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option:contains(Search More)',
+ 'autocomplete option should be Search More');
+
+ form.destroy();
+ });
});
});
});
diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
index 8aacf099de4..ad68ede00d2 100644
--- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
@@ -2142,6 +2142,46 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test('one2many list: conditional create/delete actions', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2, 4];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ `,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // bar is true -> create and delete action are available
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ '"Add an item" link should be available');
+ assert.hasClass(form.$('td.o_list_record_remove button').first(), 'fa fa-trash-o',
+ "should have trash bin icons");
+
+ // set bar to false -> create and delete action are no longer available
+ await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first());
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add an item" link should not be available if bar field is False');
+ assert.containsNone(form, 'td.o_list_record_remove button',
+ "should not have trash bin icons if bar field is False");
+
+ form.destroy();
+ });
+
QUnit.test('one2many list: unlink two records', async function (assert) {
assert.expect(8);
this.data.partner.records[0].p = [1, 2, 4];
@@ -2441,6 +2481,65 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test('one2many kanban: conditional create/delete actions', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2, 4];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ `,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // bar is initially true -> create and delete actions are available
+ assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available');
+
+ await testUtils.dom.click(form.$('.oe_kanban_global_click').first());
+
+ assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove',
+ 'There should be a Remove Button inside modal');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set bar false -> create and delete actions are no longer available
+ await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first());
+
+ assert.containsNone(form, '.o-kanban-button-new',
+ '"Add" button should not be available as bar is False');
+
+ await testUtils.dom.click(form.$('.oe_kanban_global_click').first());
+
+ assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove',
+ 'There should not be a Remove Button as bar field is False');
+
+ form.destroy();
+ });
+
QUnit.test('editable one2many list, pager is updated', async function (assert) {
assert.expect(1);
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 912c4db25da..f0db5cd40b2 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -2137,6 +2137,12 @@ Relational fields
Options:
+ - create: domain determining whether or not new tags can be created (default: True).
+
+ .. code-block:: xml
+
+
+
- color_field: the name of a numeric field, which should be present in the
view. A color will be chosen depending on its value.
@@ -2176,6 +2182,14 @@ Relational fields
Options:
+ - create: domain determining whether or not related records can be created (default: True).
+
+ - delete: domain determining whether or not related records can be deleted (default: True).
+
+ .. code-block:: xml
+
+
+
- create_text: a string that is used to customize the 'Add' label/text.
.. code-block:: xml