summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorMohammed Shekha <msh@odoo.com>2019-12-19 05:14:01 +0000
committerAaron Bohy <aab@odoo.com>2020-01-22 13:01:33 +0000
commit370f53d8dcc88b4c6960fc321199df688d1d0810 (patch)
tree3062d5774444821c7516a34b9dc357c8528ac20c /addons/web/static/src
parenta5c4855a6b5c05a27184a55261ca01966a91248e (diff)
[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>
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js148
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js68
-rw-r--r--addons/web/static/src/xml/kanban.xml2
3 files changed, 158 insertions, 60 deletions
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 "<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();
+ });
},
/**
@@ -1142,6 +1154,19 @@ var FieldX2Many = 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;
+ this.canDelete = 'delete' in this.nodeOptions ?
+ new Domain(this.nodeOptions.delete, evalContext).compute(evalContext) :
+ true;
+ },
+ /**
* Evaluates the 'column_invisible' modifier for the parent record.
*
* @return {Object} Object containing fieldName as key and the evaluated
@@ -1156,6 +1181,18 @@ var FieldX2Many = AbstractField.extend({
});
},
/**
+ * 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
* another renderer instead (eg. section_and_note_one2many).
@@ -1172,6 +1209,20 @@ var FieldX2Many = AbstractField.extend({
}
},
/**
+ * @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.
*
* @override
@@ -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));
}
},
@@ -1348,6 +1400,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
* 'parent.field' is replaced with 'field' in order to be evaluated
@@ -1730,6 +1799,15 @@ var FieldOne2Many = FieldX2Many.extend({
//--------------------------------------------------------------------------
/**
+ * @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();
}
@@ -2252,6 +2335,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 <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;
},
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 @@
<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>