[IMP] web: conditional create/delete options on x2many fields

After this commit, x2many fields can have options like
create/delete which accept a domain, to make create/delete on
x2many conditional, say for example x2many field can have options
like:

options="{'create:' [('foo', '=', True)]', 'delete:' [('foo', '=', True)]'}"

With this when foo field is True, Create and Delete actions will
be available, but if foo is False then they won't.

In case of one2many fields, if 'create' is false, then 'Add a line'
(list) or 'Add' button (kanban) won't be displayed.

In case of many2many fields, 'Add a line' or 'Add' button will
always be displayed even if 'create' condition is false as it
doesn't really create records (but rather links existing ones).
Same applies for delete.

Task-2092953

closes odoo/odoo#42919

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
Co-authored-by: Parth Chokshi <pch@odoo.com>
Co-authored-by: Aaron Bohy<aab@odoo.com>
This commit is contained in:
Mohammed Shekha
2020-01-22 13:01:33 +00:00
committed by Aaron Bohy
co-authored by Parth Chokshi Aaron Bohy
parent a5c4855a6b
commit 370f53d8dc
6 changed files with 486 additions and 60 deletions
@@ -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 "<strong>%s</strong>"'),
@@ -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,
});
@@ -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 <create> that are inside <control>
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 = $('<a href="#" role="button">')
.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 = $('<a href="#" role="button">')
.attr('data-context', create.context)
.text(create.string);
if (index > 0) {
$a.addClass('ml16');
}
$td.append($a);
});
}
}
return $rows;
},
+1 -1
View File
@@ -2,7 +2,7 @@
<t t-name="KanbanView.buttons">
<div>
<button type="button" t-attf-class="btn #{btnClass} o-kanban-button-new" accesskey="c">
<button t-if="!noCreate" type="button" t-attf-class="btn #{btnClass} o-kanban-button-new" accesskey="c">
<t t-esc="create_text || _t('Create')"/>
</button>
</div>
@@ -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: `
<form>
<field name="color"/>
<field name="timmy" options="{'create': [('color', '=', 'red')], 'delete': [('color', '=', 'red')]}">
<kanban>
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<span><t t-esc="record.display_name.value"/></span>
</div>
</t>
</templates>
</kanban>
</field>
</form>`,
archs: {
'partner_type,false,form': '<form><field name="name"/></form>',
'partner_type,false,list': '<tree><field name="name"/></tree>',
'partner_type,false,search': '<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: `
<form>
<field name="color"/>
<field name="timmy" options="{'create': [('color', '=', 'red')], 'delete': [('color', '=', 'red')]}">
<tree>
<field name="name"/>
</tree>
</field>
</form>`,
archs: {
'partner_type,false,list': '<tree><field name="name"/></tree>',
'partner_type,false,search': '<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: `
<form>
<field name="display_name"/>
<field name="turtle_bar"/>
<field name="partner_ids" options="{'create': [('turtle_bar', '=', True)], 'delete': [('turtle_bar', '=', True)]}" widget="many2many_tags"/>
</form>`,
archs: {
'partner,false,list': '<tree><field name="name"/></tree>',
'partner,false,search': '<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();
});
});
});
});
@@ -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: `
<form>
<field name="bar"/>
<field name="p" options="{'create': [('bar', '=', True)], 'delete': [('bar', '=', True)]}">
<tree>
<field name="display_name"/>
</tree>
</field>
</form>`,
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: `
<form>
<field name="bar"/>
<field name="p" options="{'create': [('bar', '=', True)], 'delete': [('bar', '=', True)]}">
<kanban>
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<span><t t-esc="record.display_name.value"/></span>
</div>
</t>
</templates>
</kanban>
<form>
<field name="display_name"/>
<field name="foo"/>
</form>
</field>
</form>`,
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);
+14
View File
@@ -2137,6 +2137,12 @@ Relational fields
Options:
- create: domain determining whether or not new tags can be created (default: True).
.. code-block:: xml
<field name="category_id" widget="many2many_tags" options="{'create': [['some_other_field', '>', 24]]}"/>
- 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
<field name="turtles" options="{'create': [['some_other_field', '>', 24]]}"/>
- create_text: a string that is used to customize the 'Add' label/text.
.. code-block:: xml