diff --git a/addons/google_spreadsheet/models/google_drive.py b/addons/google_spreadsheet/models/google_drive.py index 08c0774cc83..53b5a5830ab 100644 --- a/addons/google_spreadsheet/models/google_drive.py +++ b/addons/google_spreadsheet/models/google_drive.py @@ -32,7 +32,7 @@ class GoogleDrive(models.Model): for node in doc.xpath("//field"): if node.get('modifiers'): modifiers = json.loads(node.get('modifiers')) - if not modifiers.get('invisible') and not modifiers.get('tree_invisible'): + if not modifiers.get('invisible') and not modifiers.get('column_invisible'): display_fields.append(node.get('name')) fields = " ".join(display_fields) domain = domain.replace("'", r"\'").replace('"', "'") diff --git a/addons/web/static/src/js/core/domain.js b/addons/web/static/src/js/core/domain.js index 7b873926727..75aaf0105ed 100644 --- a/addons/web/static/src/js/core/domain.js +++ b/addons/web/static/src/js/core/domain.js @@ -51,13 +51,30 @@ var Domain = collections.Tree.extend({ } else if (_.isArray(this._data)) { // The domain is a [name, operator, value] entity // First check if we have the field value in the field values set - if (!(this._data[0] in values)) { + // and if the first part of the domain contains 'parent.field' + // get the value from the parent record. + var isParentField = false; + var fieldName = this._data[0]; + // We split the domain first part and check if it's a match + // for the syntax 'parent.field'. + var parentField = this._data[0].split('.'); + if ('parent' in values && parentField.length === 2) { + fieldName = parentField[1]; + isParentField = parentField[0] === 'parent' && + fieldName in values.parent; + } + if (!(this._data[0] in values) && !(isParentField)) { throw new Error(_.str.sprintf( "Unknown field %s in domain", this._data[0] )); } - var fieldValue = values[this._data[0]]; + var fieldValue; + if (!isParentField) { + fieldValue = values[fieldName]; + } else { + fieldValue = values.parent[fieldName]; + } switch (this._data[1]) { case "=": diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 2851f31c673..dada7653e3b 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -604,6 +604,11 @@ var FieldX2Many = AbstractField.extend({ toggle_column_order: '_onToggleColumnOrder', }), + // We need to trigger the reset on every changes to be aware of the parent changes + // and then evaluate the 'column_invisible' modifier in case a evaluated value + // changed. + resetOnAnyFieldChange: true, + /** * useSubview is used in form view to load view of the related model of the x2many field */ @@ -629,6 +634,9 @@ var FieldX2Many = AbstractField.extend({ true; this.editable = arch.attrs.editable; } + if (this.attrs.columnInvisibleFields) { + this._processColumnInvisibleFields(); + } }, /** * @override @@ -673,10 +681,20 @@ var FieldX2Many = AbstractField.extend({ * @override * @param {Object} record * @param {OdooEvent} [ev] an event that triggered the reset action + * @param {Boolean} [fieldChanged] if true, the widget field has changed * @returns {Deferred} */ - reset: function (record, ev) { - if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') { + reset: function (record, ev, fieldChanged) { + // 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 $.when(); + } + } 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 // with editable list view. In order to keep the current line in @@ -700,6 +718,20 @@ var FieldX2Many = AbstractField.extend({ // Private //-------------------------------------------------------------------------- + /** + * Evaluates the 'column_invisible' modifier for the parent record. + * + * @return {Object} Object containing fieldName as key and the evaluated + * column_invisible modifier + */ + _evalColumnInvisibleFields: function () { + var self = this; + return _.mapObject(this.columnInvisibleFields, function (domains) { + return self.record.evalModifiers({ + column_invisible: domains, + }).column_invisible; + }); + }, /** * Instanciates or updates the adequate renderer. * @@ -713,19 +745,25 @@ var FieldX2Many = AbstractField.extend({ } if (this.renderer) { this.renderer.updateState(this.value, {}); - this.pager.updateState({ size: this.value.count }); + this.currentColInvisibleFields = this._evalColumnInvisibleFields(); + this.pager.updateState({ + size: this.value.count, + columnInvisibleFields: this.currentColInvisibleFields, + }); return $.when(); } var arch = this.view.arch; var viewType; if (arch.tag === 'tree') { viewType = 'list'; + this.currentColInvisibleFields = this._evalColumnInvisibleFields(); this.renderer = new ListRenderer(this, this.value, { arch: arch, mode: this.mode, addCreateLine: !this.isReadonly && this.activeActions.create, addTrashIcon: !this.isReadonly && this.activeActions.delete, viewType: viewType, + columnInvisibleFields: this.currentColInvisibleFields, }); } if (arch.tag === 'kanban') { @@ -826,6 +864,30 @@ var FieldX2Many = AbstractField.extend({ } return def; }, + /** + * 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 + * with the right field name in the parent context. + * + * @private + */ + _processColumnInvisibleFields: function () { + var columnInvisibleFields = {}; + _.each(this.attrs.columnInvisibleFields, function (domains, fieldName) { + if (_.isArray(domains)) { + columnInvisibleFields[fieldName] = _.map(domains, function (domain) { + // We check if the domain is an array to avoid processing + // the '|' and '&' cases + if (_.isArray(domain)) { + return [domain[0].split('.')[1]].concat(domain.slice(1)); + } + return domain; + }); + } + }); + this.columnInvisibleFields = columnInvisibleFields; + }, //-------------------------------------------------------------------------- // Handlers diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js index 22c7e86ba7d..48a0e2676ae 100644 --- a/addons/web/static/src/js/services/data_manager.js +++ b/addons/web/static/src/js/services/data_manager.js @@ -283,15 +283,15 @@ return core.Class.extend({ // the invisible attribute of a field is supposed to be static ("1" in // general), but not totally as it may use keys of the context // ("context.get('some_key')"). It is evaluated server-side, and the - // result is put inside the modifiers as a value of the '(tree_)invisible' + // result is put inside the modifiers as a value of the '(column_)invisible' // key, and the raw value is left in the invisible attribute (it is used // in debug mode for informational purposes). // this should change, for instance the server might set the evaluated // value in invisible, which could then be seen as static by the client, // and add another key in debug mode containing the raw value. - // for now, we do an hack to detect if the value is static and retrieve - // it from the modifiers, - if (attrs.invisible && attrs.modifiers.match('"(?:tree_)?invisible": ?true')) { + // for now, we look inside the modifiers and consider the value only if + // it is static (=== true), + if (attrs.modifiers.invisible === true || attrs.modifiers.column_invisible === true) { attrs.__no_fetch = true; } @@ -357,6 +357,15 @@ return core.Class.extend({ attrs.orderedBy = [{name: handleField.attrs.name, asc: true}]; } } + + attrs.columnInvisibleFields = {}; + _.each(view.arch.children, function (child) { + if (child.attrs && child.attrs.modifiers) { + attrs.columnInvisibleFields[child.attrs.name] = + child.attrs.modifiers.column_invisible || false; + } + }); + // detect editables list has they behave differently with respect // to the sorting (changes are not sorted directly) if (mode === 'list' && view.arch.attrs.editable) { @@ -396,6 +405,9 @@ return core.Class.extend({ if (typeof node === 'string') { return false; } + if (!_.isObject(node.attrs.modifiers)) { + node.attrs.modifiers = node.attrs.modifiers ? JSON.parse(node.attrs.modifiers) : {}; + } if (node.tag === 'field') { fieldsInfo[node.attrs.name] = self._processField(viewType, fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {}); diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index d85f55ddf36..6500fb43b85 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1689,6 +1689,9 @@ var BasicModel = AbstractModel.extend({ if ('invisible' in modifiers) { result.invisible = evalModifier(modifiers.invisible); } + if ('column_invisible' in modifiers) { + result.column_invisible = evalModifier(modifiers.column_invisible); + } if ('readonly' in modifiers) { result.readonly = evalModifier(modifiers.readonly); } @@ -2847,7 +2850,7 @@ var BasicModel = AbstractModel.extend({ _isFieldProtected: function (record, fieldName, viewType) { var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; if (fieldInfo) { - var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); + var rawModifiers = fieldInfo.modifiers || {}; var modifiers = this._evalModifiers(record, rawModifiers); return modifiers.readonly && !fieldInfo.force_save; } else { @@ -2899,7 +2902,7 @@ var BasicModel = AbstractModel.extend({ _.each(element.getFieldNames(), function (fieldName) { var field = element.fields[fieldName]; var fieldInfo = element.fieldsInfo[element.viewType][fieldName]; - var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); + var rawModifiers = fieldInfo.modifiers || {}; var modifiers = self._evalModifiers(record, rawModifiers); if (modifiers.required && !self._isFieldSet(recordData[fieldName], field.type)) { isValid = false; diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index 93dd1f4fb73..d335999582e 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -101,8 +101,9 @@ var BasicRenderer = AbstractRenderer.extend({ // which are configured to always be reset on any change var resetWidgets = []; _.each(this.allFieldWidgets[id], function (widget) { - if (_.contains(fields, widget.name) || widget.resetOnAnyFieldChange) { - defs.push(widget.reset(record, ev)); + var fieldChanged = _.contains(fields, widget.name); + if (fieldChanged || widget.resetOnAnyFieldChange) { + defs.push(widget.reset(record, ev, fieldChanged)); resetWidgets.push(widget); } }); @@ -406,7 +407,7 @@ var BasicRenderer = AbstractRenderer.extend({ // If not, check the modifiers to see if it needs registration var modifiersData = this._getModifiersData(node); if (!modifiersData) { - var modifiers = JSON.parse(node.attrs.modifiers || "{}"); // FIXME parsed multiple times (record switching, no modifiers, ...) + var modifiers = node.attrs.modifiers || {}; modifiersData = { node: node, modifiers: modifiers, diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index b46d1f7c5fd..c9239d97d03 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -38,7 +38,7 @@ function qwebAddIf(node, condition) { function transformQwebTemplate(node, fields) { // Process modifiers if (node.tag && node.attrs.modifiers) { - var modifiers = JSON.parse(node.attrs.modifiers || "{}"); + var modifiers = node.attrs.modifiers || {}; if (modifiers.invisible) { qwebAddIf(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible))); } diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 75633629e5e..3ba80210714 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -47,16 +47,9 @@ var ListRenderer = BasicRenderer.extend({ */ init: function (parent, state, params) { this._super.apply(this, arguments); - var self = this; this.hasHandle = false; this.handleField = 'sequence'; - this.columns = _.reject(this.arch.children, function (c) { - if (c.attrs.widget === 'handle') { - self.hasHandle = true; - self.handleField = c.attrs.name; - } - return !!JSON.parse(c.attrs.modifiers || "{}").tree_invisible; - }); + this._processColumns(params.columnInvisibleFields || {}); this.rowDecorations = _.chain(this.arch.attrs) .pick(function (value, key) { return DECORATIONS.indexOf(key) >= 0; @@ -68,6 +61,18 @@ var ListRenderer = BasicRenderer.extend({ this.pagers = []; // instantiated pagers (only for grouped lists) }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + updateState: function (state, params) { + this._processColumns(params.columnInvisibleFields || {}); + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -160,6 +165,29 @@ var ListRenderer = BasicRenderer.extend({ var n = this.columns.length; return this.hasSelectors ? n+1 : n; }, + /** + * Removes the columns which should be invisible. + * + * @param {Object} columnInvisibleFields contains the column invisible modifier values + */ + _processColumns: function (columnInvisibleFields) { + var self = this; + self.hasHandle = false; + self.handleField = null; + this.columns = _.reject(this.arch.children, function (c) { + var reject = c.attrs.modifiers.column_invisible; + // If there is an evaluated domain for the field we override the node + // attribute to have the evaluated modifier value. + if (c.attrs.name in columnInvisibleFields) { + reject = columnInvisibleFields[c.attrs.name]; + } + if (!reject && c.attrs.widget === 'handle') { + self.hasHandle = true; + self.handleField = c.attrs.name; + } + return reject; + }); + }, /** * Render a list of