diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index fac5d69d2c1..6f83108725d 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1212,8 +1212,14 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
// re-evaluate available actions
const oldCanCreate = this.canCreate;
const oldCanDelete = this.canDelete;
+ const oldCanLink = this.canLink;
+ const oldCanUnlink = this.canUnlink;
this._computeAvailableActions(record);
- const actionsChanged = this.canCreate !== oldCanCreate || this.canDelete !== oldCanDelete;
+ const actionsChanged =
+ this.canCreate !== oldCanCreate ||
+ this.canDelete !== oldCanDelete ||
+ this.canLink !== oldCanLink ||
+ this.canUnlink !== oldCanUnlink;
// If 'fieldChanged' is false, it means that the reset was triggered by
// the 'resetOnAnyFieldChange' mechanism. If it is the case, if neither
@@ -1290,6 +1296,12 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
this.canDelete = 'delete' in this.nodeOptions ?
new Domain(this.nodeOptions.delete, evalContext).compute(evalContext) :
true;
+ this.canLink = 'link' in this.nodeOptions ?
+ new Domain(this.nodeOptions.link, evalContext).compute(evalContext) :
+ true;
+ this.canUnlink = 'unlink' in this.nodeOptions ?
+ new Domain(this.nodeOptions.unlink, evalContext).compute(evalContext) :
+ true;
},
/**
* @private
@@ -1378,8 +1390,8 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
*/
_hasCreateLine: function () {
return !this.hasReadonlyModifier && (
- (this.activeActions.create && this.canCreate) ||
- (this.isMany2Many)
+ (!this.isMany2Many && this.activeActions.create && this.canCreate) ||
+ (this.isMany2Many && this.canLink)
);
},
/**
@@ -1388,8 +1400,8 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
*/
_hasTrashIcon: function () {
return !this.isReadonly && (
- (this.activeActions.delete && this.canDelete) ||
- (this.isMany2Many)
+ (!this.isMany2Many && this.activeActions.delete && this.canDelete) ||
+ (this.isMany2Many && this.canUnlink)
);
},
/**
@@ -2167,6 +2179,20 @@ var FieldMany2Many = FieldX2Many.extend({
}).open();
},
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _getButtonsRenderingContext() {
+ const renderingContext = this._super(...arguments);
+ renderingContext.noCreate = !this.canLink;
+ return renderingContext;
+ },
+
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
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 4bacfee8862..b6329a22aad 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
@@ -828,6 +828,212 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test('many2many field with link/unlink options (list)', async function (assert) {
+ assert.expect(5);
+
+ 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 -> link and unlink 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 (Create action is available)');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add a line" should no longer be available after color field changed');
+ assert.containsNone(form, '.o_list_record_remove',
+ "should no longer have remove icon after color field changed");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link/unlink options (list, create="0")', async function (assert) {
+ assert.expect(5);
+
+ 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 -> link and unlink 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', 2,
+ 'there should be 2 buttons available in the modal (Create action is not available)');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add a line" should no longer be available after color field changed');
+ assert.containsNone(form, '.o_list_record_remove',
+ "should no longer have remove icon after color field changed");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link option (kanban)', async function (assert) {
+ assert.expect(3);
+
+ 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 -> link and unlink actions are available
+ assert.containsOnce(form, '.o-kanban-button-new', "should have the 'Add' button");
+
+ 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 (Create action is available');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o-kanban-button-new',
+ '"Add" should no longer be available after color field changed');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link option (kanban, create="0")', async function (assert) {
+ assert.expect(3);
+
+ 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 -> link and unlink actions are available
+ assert.containsOnce(form, '.o-kanban-button-new', "should have the 'Add' button");
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ 'there should be 2 buttons available in the modal (Create action is not available');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o-kanban-button-new',
+ '"Add" should no longer be available after color field changed');
+
+ form.destroy();
+ });
+
QUnit.test('many2many list: list of id as default value', async function (assert) {
assert.expect(1);
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 7f33d6c8d97..eaa8c927dcf 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -2417,6 +2417,8 @@ many2many (FieldMany2Many)
- create_text: allow the customization of the text displayed when adding a
new record
+ - link: domain determining whether or not records can be added to the relation (default: True).
+ - unlink: domain determining whether or not records can be removed from the relation (default: True).
many2many_binary (FieldMany2ManyBinaryMultiFiles)
This widget helps the user to upload or delete one or more files at the same