From 0f9e986c468702ebe55860895ef8d8b84c90a48c Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 16 May 2017 15:02:58 +0200 Subject: [FIX] web: BasicModel: discard changes when reloading ... from a new record. Steps to reproduce the issue: - go to a multi-record view (e.g. list) of basically any model - click on 'Create' -> it opens a form view in edit mode - click on 'Discard' -> it goes back to the list - click on an existing record -> it should open the selected record in readonly. However, it didn't display the data of the selected record, but the default data of a new record (the result of a default_get). This was because the changes weren't properly discarded when reloading from a new record (it rather restored the record to its initial state, i.e. to the result of the default_get). --- addons/web/static/src/js/views/basic/basic_model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 5e5b36472f5..a55d2b2723b 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -228,7 +228,7 @@ var BasicModel = AbstractModel.extend({ options = options || {}; var element = this.localData[id]; var isNew = this.isNew(id); - var rollback = options.rollback || isNew; + var rollback = 'rollback' in options ? options.rollback : isNew; this._visitChildren(element, function (elem) { if (rollback && elem._savePoint) { if (elem._savePoint instanceof Array) { @@ -644,7 +644,7 @@ var BasicModel = AbstractModel.extend({ return this._makeDefaultRecord(element.model, params); } if (!options.keepChanges) { - this.discardChanges(id); + this.discardChanges(id, {rollback: false}); } } -- cgit v1.2.3 From 3dd97db07fcf22bf1dd7fcaa912a20438483bc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 18 May 2017 10:45:36 +0200 Subject: [FIX] web: properly loads default values for many2manys Many2many default value can be a list of ids, and in that case, we did not properly load data. It could be seen by selecting a few contacts (in list view), then opening the 'Merge contact' wizard in the sidebar (action menu). Before this commit, the wizard did show a list with some empty rows (for the field partner_ids). --- .../web/static/src/js/views/basic/basic_model.js | 78 +++++++++------------- 1 file changed, 33 insertions(+), 45 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 a55d2b2723b..046c810c437 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2300,60 +2300,48 @@ var BasicModel = AbstractModel.extend({ record._changes[name] = x2manyList.id; var many2ones = {}; var r; + var isCommandList = result[name].length && _.isArray(result[name][0]); + if (!isCommandList) { + result[name] = [[6, false, result[name]]]; + } _.each(result[name], function (value) { - if (_.isArray(value)) { - // value is a command - if (value[0] === 0) { - // CREATE - r = self._makeDataPoint({ - modelName: x2manyList.model, - context: x2manyList.context, - fieldsInfo: fieldsInfo, - fields: fields, - viewType: viewType, - }); - x2manyList._changes = x2manyList._changes || []; - x2manyList._changes.push(r.id); - - // this is necessary so the fields are initialized - _.each(r.getFieldNames(), function (fieldName) { - r.data[fieldName] = null; - }); - - r._changes = _.defaults(value[2], r.data); - for (var name in r._changes) { - var isFieldInView = name in r.fields; - if (isFieldInView && r.fields[name].type === 'many2one') { - var rec = self._makeDataPoint({ - context: r.context, - modelName: r.fields[name].relation, - data: {id: r._changes[name]} - }); - r._changes[name] = rec.id; - many2ones[name] = true; - } - } - } - if (value[0] === 6) { - // REPLACE_WITH - x2manyList.res_ids = value[2]; - x2manyList.count = x2manyList.res_ids.length; - defs.push(self._readUngroupedList(x2manyList)); - } - } else { - // value is an id + // value is a command + if (value[0] === 0) { + // CREATE r = self._makeDataPoint({ modelName: x2manyList.model, context: x2manyList.context, fieldsInfo: fieldsInfo, fields: fields, - res_id: value, viewType: viewType, }); - if (!x2manyList._changes) { - x2manyList._changes = []; - } + x2manyList._changes = x2manyList._changes || []; x2manyList._changes.push(r.id); + + // this is necessary so the fields are initialized + _.each(r.getFieldNames(), function (fieldName) { + r.data[fieldName] = null; + }); + + r._changes = _.defaults(value[2], r.data); + for (var name in r._changes) { + var isFieldInView = name in r.fields; + if (isFieldInView && r.fields[name].type === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: r.fields[name].relation, + data: {id: r._changes[name]} + }); + r._changes[name] = rec.id; + many2ones[name] = true; + } + } + } + if (value[0] === 6) { + // REPLACE_WITH + x2manyList.res_ids = value[2]; + x2manyList.count = x2manyList.res_ids.length; + defs.push(self._readUngroupedList(x2manyList)); } }); -- cgit v1.2.3 From 94c91374e33f80256398f7ac2f892500aaf7e685 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 18 May 2017 09:36:14 +0200 Subject: [FIX] web: selection field with value 0 The server doesn't make a distinction between an unset selection field, and a value 0 (integer). So when 0 is a valid key for the field, the server returns false as value if the field is unset, or if its value is 0, which is not really convenient. As a consequence, when the value is 0, it appears as unset in the interface. To circumvent this, we automatically converts false value of selection fields to 0, if 0 is a key of the selection. --- .../web/static/src/js/views/basic/basic_model.js | 48 +++++++++++++--------- 1 file changed, 28 insertions(+), 20 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 046c810c437..eef42f19bc3 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1093,14 +1093,8 @@ var BasicModel = AbstractModel.extend({ }); defs.push(def); } - } else if (field.type === 'date') { - // process data: convert into a moment instance - record._changes[name] = fieldUtils.parse.date(val); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record._changes[name] = fieldUtils.parse.datetime(val); } else { - record._changes[name] = val; + record._changes[name] = self._parseServerValue(field, val); } }); return $.when.apply($, defs); @@ -2349,14 +2343,8 @@ var BasicModel = AbstractModel.extend({ _.each(_.keys(many2ones), function (name) { defs.push(self._fetchNameGets(x2manyList, name)); }); - } else if (field.type === 'date') { - // process date: convert into a moment instance - record._changes[name] = fieldUtils.parse.date(result[name], field, {isUTC: true}); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record._changes[name] = fieldUtils.parse.datetime(result[name], field, {isUTC: true}); } else { - record._changes[name] = result[name]; + record._changes[name] = self._parseServerValue(field, result[name]); } }); return $.when.apply($, defs) @@ -2426,15 +2414,35 @@ var BasicModel = AbstractModel.extend({ // no value for the many2one record[fieldName] = false; } - } else if (field.type === 'date') { - // process data: convert into a moment instance - record[fieldName] = fieldUtils.parse.date(val, field, {isUTC: true}); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record[fieldName] = fieldUtils.parse.datetime(val, field, {isUTC: true}); + } else { + record[fieldName] = self._parseServerValue(field, val); } }); }, + /** + * Processes date(time) and selection field values sent by the server. + * Converts data(time) values to moment instances. + * Converts false values of selection fields to 0 if 0 is a valid key, + * because the server doesn't make a distinction between false and 0, and + * always sends false when value is 0. + * + * @param {Object} field the field description + * @param {*} value + * @returns {*} the processed value + */ + _parseServerValue: function (field, value) { + if (field.type === 'date' || field.type === 'datetime') { + // process date(time): convert into a moment instance + value = fieldUtils.parse[field.type](value, field, {isUTC: true}); + } else if (field.type === 'selection' && value === false) { + // process selection: convert false to 0, if 0 is a valid key + var hasKey0 = _.find(field.selection, function (option) { + return option[0] === 0; + }); + value = hasKey0 ? 0 : value; + } + return value; + }, /** * This method is quite important: it is supposed to perform the /onchange * rpc and apply the result. -- cgit v1.2.3 From 8af3d452f414135bd897026859958a9660bb5aa4 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 18 May 2017 13:16:48 +0200 Subject: [FIX] web: BasicModel: read_group on selection field When grouping on a selection field, the read_group may return a group for value false (containing records with the selection field unset, typically). When this happened, there was a crash because the BasicModel didn't handle that case (it assumed that the value of the groups was always a valid key of the selection). --- addons/web/static/src/js/views/basic/basic_model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 eef42f19bc3..37adddfcd75 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2576,7 +2576,7 @@ var BasicModel = AbstractModel.extend({ var choice = _.find(list.fields[rawGroupBy].selection, function (c) { return c[0] === value; }); - value = choice[1]; + value = choice ? choice[1] : false; } var newGroup = self._makeDataPoint({ modelName: list.model, -- cgit v1.2.3 From 92409cdfe5812c72f03ac96fab955db865f390c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 19 May 2017 11:01:06 +0200 Subject: [FIX] web: apply onchanges for sub fields for new records Before this commit, when creating a new record, we only looked at the main fields to determine if a call to onchange was needed. This was not enough, since sub fields (fields in a one2many for example) can have an onchange. This was an issue only for new records with onchanges in sub fields, but no onchange on main fields. For example, the merge contact wizard. To solve this issue, we had to propagate the current view type from the field to the basic model, because otherwise, the main viewtype was used to compute the onchangeSpec, and that is not always correct. --- .../web/static/src/js/views/basic/basic_model.js | 69 ++++++++++++---------- 1 file changed, 39 insertions(+), 30 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 37adddfcd75..76dc10d0238 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -615,10 +615,12 @@ var BasicModel = AbstractModel.extend({ * * @param {string} record_id * @param {Object} changes a map field => new value + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {string[]} list of changed fields */ - notifyChanges: function (record_id, changes) { - return this.mutex.exec(this._applyChange.bind(this, record_id, changes)); + notifyChanges: function (record_id, changes, viewType) { + return this.mutex.exec(this._applyChange.bind(this, record_id, changes, viewType)); }, /** * Reload all data for a given resource @@ -902,9 +904,11 @@ var BasicModel = AbstractModel.extend({ * * @param {string} recordID * @param {Object} changes + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} */ - _applyChange: function (recordID, changes) { + _applyChange: function (recordID, changes, viewType) { var self = this; var record = this.localData[recordID]; var field; @@ -937,7 +941,7 @@ var BasicModel = AbstractModel.extend({ } var onchangeDef; if (onChangeFields.length) { - onchangeDef = self._performOnChange(record, onChangeFields).then(function (result) { + onchangeDef = self._performOnChange(record, onChangeFields, viewType).then(function (result) { delete record._warning; return _.keys(changes).concat(Object.keys(result && result.value || {})); }); @@ -1237,22 +1241,34 @@ var BasicModel = AbstractModel.extend({ * @see _performOnChange * * @param {Object} record resource object of type 'record' - * @returns {Object} an onchange spec + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record + * @returns {Object|false} an onchange spec, or false if no onchange should + * be applied */ - _buildOnchangeSpecs: function (record) { - // TODO: replace this function by some generic tree function in utils + _buildOnchangeSpecs: function (record, viewType) { + var hasOnchange = false; var specs = {}; - _.each(record.getFieldNames(), function (name) { + var fieldsInfo = record.fieldsInfo[viewType || record.viewType]; + + _.each(Object.keys(fieldsInfo), function (name) { var field = record.fields[name]; - var fieldInfo = record.fieldsInfo[record.viewType][name]; + var fieldInfo = fieldsInfo[name]; specs[name] = (field.onChange) || ""; + if (field.onChange) { + hasOnchange = true; + } _.each(fieldInfo.views, function (view) { _.each(view.fieldsInfo[view.type], function (field, subname) { - specs[name + '.' + subname] = (view.fields[subname].onChange) || ""; + var onChange = view.fields[subname].onChange; + specs[name + '.' + subname] = onChange || ""; + if (onChange) { + hasOnchange = true; + } }); }); }); - return specs; + return hasOnchange ? specs : false; }, /** * Fetch all name_gets for the many2ones in a group @@ -2349,23 +2365,11 @@ var BasicModel = AbstractModel.extend({ }); return $.when.apply($, defs) .then(function () { - var shouldApplyOnchange = false; - var field; - for (var field_name in record.data) { - field = record.fields[field_name]; - if (field.onChange) { - shouldApplyOnchange = true; + return self._performOnChange(record, fields_key).then(function () { + if (record._warning) { + return $.Deferred().reject(); } - } - if (shouldApplyOnchange) { - return self._performOnChange(record, fields_key).then(function () { - if (record._warning) { - return $.Deferred().reject(); - } - }); - } else { - return $.when(); - } + }); }) .then(function () { return self._fetchRelationalData(record); @@ -2449,12 +2453,17 @@ var BasicModel = AbstractModel.extend({ * * @param {Object} record * @param {string[]} fields changed fields + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} The returned deferred can fail, in which case the * fail value will be the warning message received from the server */ - _performOnChange: function (record, fields) { + _performOnChange: function (record, fields, viewType) { var self = this; - var onchange_spec = this._buildOnchangeSpecs(record); + var onchangeSpec = this._buildOnchangeSpecs(record, viewType); + if (!onchangeSpec) { + return $.when(); + } var idList = record.data.id ? [record.data.id] : []; var options = { full: true, @@ -2470,7 +2479,7 @@ var BasicModel = AbstractModel.extend({ return self._rpc({ model: record.model, method: 'onchange', - args: [idList, currentData, fields, onchange_spec, context], + args: [idList, currentData, fields, onchangeSpec, context], }) .then(function (result) { if (!record._changes) { -- cgit v1.2.3 From af021f223b2ef3801fa012e82775054c48054b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 19 May 2017 14:22:28 +0200 Subject: [FIX] web: apply onchange when m2o or m2m is edited Before this commit, when editing a many2one (with an onchange) by clicking on the small form button on the right, then changing something, then saving the modal dialog, no onchange was triggered, because, technically, the value of the many2one was not changed, only the values from the record pointed by the many2one were. The same remarks applies to many2manys. To see this issue in odoo, you can open a product (technically, a product.template) from the 'Catalog' menu in sales, then edit it, then click on the small form view icon next to Internal Category. This is a many2one (categ_id) with an onchange. In the form view dialog, change something, for example costing method, then save. The res_id of the internal category was not changed, but an onchange should still be triggered. --- addons/web/static/src/js/views/basic/basic_model.js | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 76dc10d0238..2895e5b2ff9 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -980,6 +980,17 @@ var BasicModel = AbstractModel.extend({ record._changes[fieldName] = false; return $.when(); } + + // here, we check that the many2one really changed. If the res_id is the + // same, we do not need to do any extra work. It can happen when the + // user edited a manyone (with the small form view button) with an + // onchange. In that case, the onchange is triggered, but the actual + // value did not change. + var relatedID = (record._changes && record._changes[fieldName]) || record.data[fieldName]; + var relatedRecord = this.localData[relatedID]; + if (relatedRecord && (data.id === this.localData[relatedID].res_id)) { + return $.when(); + } var rel_data = _.pick(data, 'id', 'display_name'); var def; if (rel_data.display_name === undefined) { -- cgit v1.2.3 From 4b8c34be15f355958bbdb64c440018bdbe72ad3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 22 May 2017 13:05:25 +0200 Subject: [FIX] web: properly evaluate context with x2m The motivation for this commit was to fix a wrong evaluation context for x2m fields: it was using a list of res_id, which may contain virtual ids. For example, it could be seen in the form view for journal entries: adding the first line (field line_ids) was fine, but clicking on 'Add an item' a second time caused a crash. The reason was that the line_ids was explicitely added to the context in the view, and the second time we add a line, the web client sent a list looking like this [35, 'virtual_43'] to the server, in the nameget context. The proper behaviour is to send a list of commands for those fields. This means that we had to do some non trivial changes in the getEvalContext method: we stop using the get method with raw: true. It was nice to use the same method, but the eval context is actually different that the values returned by the get method. That was the motivation to introduce the 'noUnsetNumeric' flag, but now, we simply create another method and let each method do its own work. TLDR: eval contexts are hard... --- .../web/static/src/js/views/basic/basic_model.js | 83 ++++++++++++++++------ 1 file changed, 63 insertions(+), 20 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 2895e5b2ff9..5d6a3271692 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -284,8 +284,6 @@ var BasicModel = AbstractModel.extend({ * @param {boolean} [options.env=false] if true, will only return res_id * (if record) or res_ids (if list) * @param {boolean} [options.raw=false] if true, will not follow relations - * @param {boolean} [options.noUnsetNumeric=false] if true, will set numeric - * values to 0 if not set * @returns {Object} */ get: function (id, options) { @@ -308,7 +306,6 @@ var BasicModel = AbstractModel.extend({ return env; } - if (element.type === 'record') { var data = _.extend({}, element.data, element._changes); @@ -321,13 +318,6 @@ var BasicModel = AbstractModel.extend({ if (!field) { continue; } - if (options.noUnsetNumeric) { - if (field.type === 'float' || - field.type === 'integer' || - field.type === 'monetary') { - data[fieldName] = data[fieldName] || 0; - } - } // get relational datapoint if (field.type === 'many2one') { @@ -2067,19 +2057,23 @@ var BasicModel = AbstractModel.extend({ * @returns {Object} */ _getEvalContext: function (element) { - var evalContext = this.get(element.id, {raw: true, noUnsetNumeric: true}).data; - evalContext.active_model = element.model; - evalContext.id = evalContext.id || false; - evalContext.active_id = evalContext.id; - evalContext.active_ids = evalContext.id ? [evalContext.id] : []; + var evalContext = element.type === 'record' ? this._getRecordEvalContext(element) : {}; + if (element.parentID) { - var parent = this.get(element.parentID, {raw: true}); - if (parent.type === 'list' && this.localData[element.parentID].parentID) { - parent = this.get(this.localData[element.parentID].parentID, {raw: true}); + var parent = this.localData[element.parentID]; + if (parent.type === 'list' && parent.parentID) { + parent = this.localData[parent.parentID]; + } + if (parent.type === 'record') { + evalContext.parent = this._getRecordEvalContext(parent); } - _.extend(evalContext, {parent: parent.data}); } - return _.extend({}, session.user_context, element.context, evalContext); + return _.extend({ + active_model: element.model, + id: evalContext.id || false, + active_id: evalContext.id || false, + active_ids: evalContext.id ? [evalContext.id] : [], + }, session.user_context, element.context, evalContext); }, /** * Returns the list of field names of the given element according to its @@ -2092,6 +2086,55 @@ var BasicModel = AbstractModel.extend({ var fieldsInfo = element.fieldsInfo; return Object.keys(fieldsInfo && fieldsInfo[element.viewType] || {}); }, + /** + * Evaluate the record evaluation context. This method is supposed to be + * called by _getEvalContext. It basically only generates a dictionary of + * current values for the record, with commands for x2manys fields. + * + * @param {Object} record an element of type 'record' + * @returns Object + */ + _getRecordEvalContext: function (record) { + var self = this; + var relDataPoint; + var context = _.extend({}, record.data, record._changes); + for (var fieldName in context) { + var field = record.fields[fieldName]; + if (context[fieldName] === null) { + context[fieldName] = false; + } + if (!field) { + continue; + } + if (field.type === 'float' || + field.type === 'integer' || + field.type === 'monetary') { + context[fieldName] = context[fieldName] || 0; + continue; + } + if (field.type === 'many2one') { + relDataPoint = this.localData[context[fieldName]]; + context[fieldName] = relDataPoint ? relDataPoint.res_id : false; + continue; + } + if (field.type === 'one2many' || field.type === 'many2many') { + relDataPoint = this.localData[context[fieldName]]; + var relData = relDataPoint._changes || relDataPoint.data; + var commands = _.map(relData, function (id) { + var resID = self.localData[id].res_id; + if (typeof resID === 'string') { + var changes = self._generateChanges(self.localData[id]); + return x2ManyCommands.create(changes); + } else { + return x2ManyCommands.link_to(resID); + } + }); + context[fieldName] = commands; + } + + } + return context; + }, /** * Returns true iff value is considered to be set for the given field's type. * -- cgit v1.2.3 From 460c21f76578cceec364f0431d03e4f4515d6acc Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 23 May 2017 15:31:44 +0200 Subject: [FIX] web: Kanban: various bugs in grouped mode This rev. fixes the next three bugs: - the dataPoints representing each group didn't have the parentID attribute set - when quick creating a record, the res_ids and count attributes of the group, and of the parent weren't correctly updated - when quick creating a record, the environment (view_manager) wasn't correctly updated The combination of thoses bugs produced the following crash. Open a grouped kanban view (e.g. project.task), quick create a record, open that record in form view (note that the pager was wrong, it indicated 0/x, where x number of records), delete the record -> the form is supposed to load the next record, but it crashed because it couldn't get its id (trying to read id null). --- addons/web/static/src/js/views/basic/basic_model.js | 1 + 1 file changed, 1 insertion(+) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 5d6a3271692..9d4d29b7635 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2654,6 +2654,7 @@ var BasicModel = AbstractModel.extend({ orderedBy: list.orderedBy, limit: list.limit, openGroupByDefault: list.openGroupByDefault, + parentID: list.id, type: 'list', viewType: list.viewType, }); -- cgit v1.2.3 From a3441cfb566f7274e049ec3eab803a909bd40f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 23 May 2017 17:20:43 +0200 Subject: [FIX] web: handle 'required' attribute in x2many+onchange Before doing an onchange in a x2many, the basic model tries to evaluate if the line is valid or not. In that computation, we used the field.required property instead of using the required modifier. This commit had to move the modifier handling from the BasicRenderer to the BasicModel, in order to use it in the _isX2ManyValid method. --- .../web/static/src/js/views/basic/basic_model.js | 53 ++++++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 9d4d29b7635..85150061075 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -27,7 +27,6 @@ odoo.define('web.BasicModel', function (require) { * fieldsInfo: {Object}, * getContext: {function}, * getDomain: {function}, - * getEvalContext: {function}, * getFieldNames: {function}, * groupedBy: {string[]}, * id: {integer}, @@ -346,11 +345,11 @@ var BasicModel = AbstractModel.extend({ count: element.count, data: data, domain: element.domain.slice(0), + evalModifiers: element.evalModifiers, fields: element.fields, fieldsInfo: element.fieldsInfo, getContext: element.getContext, getDomain: element.getDomain, - getEvalContext: element.getEvalContext, getFieldNames: element.getFieldNames, id: element.id, limit: element.limit, @@ -400,7 +399,6 @@ var BasicModel = AbstractModel.extend({ fields: element.fields, getContext: element.getContext, getDomain: element.getDomain, - getEvalContext: element.getEvalContext, getFieldNames: element.getFieldNames, groupedBy: element.groupedBy, id: element.id, @@ -1271,6 +1269,37 @@ var BasicModel = AbstractModel.extend({ }); return hasOnchange ? specs : false; }, + /** + * Evaluate modifiers + * + * @private + * @param {Object} element a valid element object, which will serve as eval + * context. + * @param {Object} modifiers + * @returns {Object} + */ + _evalModifiers: function (element, modifiers) { + var result = {}; + var self = this; + var evalContext; + function evalModifier (mod) { + if (mod === undefined || mod === false || mod === true) { + return !!mod; + } + evalContext = evalContext || self._getEvalContext(element); + return new Domain(mod, evalContext).compute(evalContext); + } + if ('invisible' in modifiers) { + result.invisible = evalModifier(modifiers.invisible); + } + if ('readonly' in modifiers) { + result.readonly = evalModifier(modifiers.readonly); + } + if ('required' in modifiers) { + result.required = evalModifier(modifiers.required); + } + return result; + }, /** * Fetch all name_gets for the many2ones in a group * @@ -2168,11 +2197,17 @@ var BasicModel = AbstractModel.extend({ _isX2ManyValid: function (id) { var self = this; var isValid = true; - var element = this.get(id, {raw: true}); - _.each(element.getFieldNames(), function (fieldName) { - var field = element.fields[fieldName]; - _.each(element.data, function (rec) { - if (field.required && !self._isFieldSet(rec.data[fieldName], field.type)) { + var element = this.localData[id]; + _.each(element._changes || element.data, function (recordID) { + var recordData = self.get(recordID, {raw: true}).data; + var record = self.localData[recordID]; + _.each(element.getFieldNames(), function (fieldName) { + var field = element.fields[fieldName]; + var fieldInfo = element.fieldsInfo[element.viewType][fieldName]; + var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); + var modifiers = self._evalModifiers(record, rawModifiers); + var required = 'required' in modifiers ? modifiers.required : field.required; + if (required && !self._isFieldSet(recordData[fieldName], field.type)) { isValid = false; } }); @@ -2260,9 +2295,9 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }; + dataPoint.evalModifiers = this._evalModifiers.bind(this, dataPoint); dataPoint.getContext = this._getContext.bind(this, dataPoint); dataPoint.getDomain = this._getDomain.bind(this, dataPoint); - dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint); dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint); this.localData[dataPoint.id] = dataPoint; -- cgit v1.2.3 From 9009c5375ee0dec0fdf5c6257065066ef5a3f6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 26 May 2017 13:16:01 +0200 Subject: [FIX] web: fix various issues with eval contexts Eval contexts are not easy... They should be different when sent to the server and when evaluated in the web client. In this commit, we fix some small issues: - when evaluated on the web client, x2manys should be a list of ids, when sent to the server, it should be a list of commands - dates should be converted to string - always add current_date in the evaluation context, as a magic key (it was previously only available in row decoration context evaluations) --- .../web/static/src/js/views/basic/basic_model.js | 35 +++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 85150061075..84ae999ac88 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2098,10 +2098,11 @@ var BasicModel = AbstractModel.extend({ } } return _.extend({ - active_model: element.model, - id: evalContext.id || false, active_id: evalContext.id || false, active_ids: evalContext.id ? [evalContext.id] : [], + active_model: element.model, + current_date: moment().format('YYYY-MM-DD'), + id: evalContext.id || false, }, session.user_context, element.context, evalContext); }, /** @@ -2141,6 +2142,12 @@ var BasicModel = AbstractModel.extend({ context[fieldName] = context[fieldName] || 0; continue; } + if (field.type === 'date' || field.type === 'datetime') { + if (context[fieldName]) { + context[fieldName] = JSON.parse(JSON.stringify(context[fieldName])); + } + continue; + } if (field.type === 'many2one') { relDataPoint = this.localData[context[fieldName]]; context[fieldName] = relDataPoint ? relDataPoint.res_id : false; @@ -2149,16 +2156,22 @@ var BasicModel = AbstractModel.extend({ if (field.type === 'one2many' || field.type === 'many2many') { relDataPoint = this.localData[context[fieldName]]; var relData = relDataPoint._changes || relDataPoint.data; - var commands = _.map(relData, function (id) { - var resID = self.localData[id].res_id; - if (typeof resID === 'string') { - var changes = self._generateChanges(self.localData[id]); - return x2ManyCommands.create(changes); - } else { - return x2ManyCommands.link_to(resID); - } + var ids = _.map(relData, function (id) { + return self.localData[id].res_id; }); - context[fieldName] = commands; + + ids.toJSON = function () { + return _.map(relData, function (id) { + var resID = self.localData[id].res_id; + if (typeof resID === 'string') { + var changes = self._generateChanges(self.localData[id]); + return x2ManyCommands.create(changes); + } else { + return x2ManyCommands.link_to(resID); + } + }); + }; + context[fieldName] = ids; } } -- cgit v1.2.3 From 1d57f89380b3b23aed9404fdd70a63274d3ca99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 26 May 2017 14:19:09 +0200 Subject: [FIX] web: properly react when save operation fails (form) Before this commit, the save method in the basic model did not properly react when the rpc fails. In that case, the returned deferred stayed indefinitely in the 'pending' state. This is a big problem, because the basic controller put that deferred in a mutex. As a result, every subsequent operations (including field changes!) were indefinitely delayed. To reproduce the issue, you can find any form view where some python business logic will cause a fail in some cases. For example, create a new inventory adjustement, click on 'Start inventory', then add a line in the x2many with the same product as another line. Saving that record will fail, but then, the form view is broken: no interaction will work. --- addons/web/static/src/js/views/basic/basic_model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 84ae999ac88..9af32adda93 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -778,7 +778,7 @@ var BasicModel = AbstractModel.extend({ _.extend(record.data, _changes); def.resolve(changedFields); } - }); + }).fail(def.reject.bind(def)); } else { def.resolve(changedFields); } -- cgit v1.2.3 From 45bce324f34eeb8ee69e028e0d20b08d998c0eef Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 29 May 2017 11:22:35 +0200 Subject: [FIX] web: BasicModel: correctly apply x2many changes Suppose that we have a form view with a one2many list (non editable) containing a record. In edit mode, the user clicks on that record to edit it in a form view (in a dialog). In this form view, there is an x2many field that isn't in the list. Before this rev., it crashed when the user clicked on 'Save' in the dialog after editing the value of the x2many. This was because the basic model didn't use the correct fieldsInfo in that case (it use the fieldsInfo of the list view instead of the one of the form view, and the x2many field isn't in the list). Also ensures that the record is correctly saved even if editing the x2many is the only change done, as this wasn't the case before. --- addons/web/static/src/js/views/basic/basic_model.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 9af32adda93..11961e6fecf 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -908,7 +908,7 @@ var BasicModel = AbstractModel.extend({ for (var fieldName in changes) { field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { - defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName])); + defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType)); } else if (field.type === 'many2one') { defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName])); } else { @@ -1112,13 +1112,15 @@ var BasicModel = AbstractModel.extend({ * @param {string} fieldName * @param {Object} command A command object. It should have a 'operation' * key. For example, it looks like {operation: ADD, id: 'partner_1'} + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} */ - _applyX2ManyChange: function (record, fieldName, command) { + _applyX2ManyChange: function (record, fieldName, command, viewType) { var self = this; var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; var field = record.fields[fieldName]; - var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var rec; var defs = []; @@ -1939,8 +1941,8 @@ var BasicModel = AbstractModel.extend({ // generate update commands for records that have been // updated (it may happen with editable lists) _.each(relData, function (relRecord) { - if (!_.isEmpty(relRecord._changes)) { - var changes = self._generateChanges(relRecord); + var changes = self._generateChanges(relRecord); + if (!_.isEmpty(changes)) { var command = x2ManyCommands.update(relRecord.res_id, changes); commands[fieldName].push(command); } @@ -1958,8 +1960,8 @@ var BasicModel = AbstractModel.extend({ if (_.contains(keptIds, relIds[i])) { // this is an id that already existed relRecord = _.findWhere(relData, {res_id: relIds[i]}); - if (!_.isEmpty(relRecord._changes)) { - changes = this._generateChanges(relRecord); + changes = this._generateChanges(relRecord); + if (!_.isEmpty(changes)) { command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; } else { -- cgit v1.2.3 From 27463eadabb40747c53c51817cf853fdd91d192f Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 29 May 2017 15:14:49 +0200 Subject: [FIX] web: FieldMany2Many: don't save if nothing changed Suppose that there is a many2many list (non editable) in a form view. When one of the many2many subrecords is edited (from the form view in a dialog), it is saved directly when the user clicks on 'Save' in the dialog. Before this rev., if the user clicked on 'Save' in the main form view after editing a m2m subrecord, a 'write' RPC was done, whereas there were nothing left to save (as the subrecord had already been saved). --- addons/web/static/src/js/views/basic/basic_model.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 11961e6fecf..595c39103d3 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1117,6 +1117,14 @@ var BasicModel = AbstractModel.extend({ * @returns {Deferred} */ _applyX2ManyChange: function (record, fieldName, command, viewType) { + if (command.operation === 'TRIGGER_ONCHANGE') { + // the purpose of this operation is to trigger an onchange RPC, so + // there is no need to apply any change on the record (the changes + // have probably been already applied and saved, usecase: many2many + // edition in a dialog) + return $.when(); + } + var self = this; var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; var field = record.fields[fieldName]; -- cgit v1.2.3 From a5f139a5041acf3160b07c64d316daf9c033ac5c Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Tue, 30 May 2017 15:19:05 +0200 Subject: [FIX] web: client-side sort of x2many values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a ´default_order´ is set on an embedded view for a x2many field, the sort was only applied server-side, after saving the record. After adding a record in a x2many, the records were thus not correctly sorted. We reintroduce here the client-side sort after the x2many modification. --- addons/web/static/src/js/views/basic/basic_model.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') 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 595c39103d3..742e59a61c5 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1151,6 +1151,7 @@ var BasicModel = AbstractModel.extend({ newRecord.fieldsInfo = list.fieldsInfo; newRecord.viewType = list.viewType; list._changes.push(newRecord.id); + this._sortList(list); break; case 'ADD_M2M': // force to use link command instead of create command @@ -1170,6 +1171,7 @@ var BasicModel = AbstractModel.extend({ }); list_records[d.id] = rec; list._changes.push(rec.id); + self._sortList(list); }); // read list's records as we only have their ids and optionally their display_name // (we can't use function readUngroupedList because those records are only in the @@ -2905,9 +2907,12 @@ var BasicModel = AbstractModel.extend({ if (list.orderedBy.length) { // sort records according to ordered_by[0] var order = list.orderedBy[0]; - list.data.sort(function (r1, r2) { - var data1 = self.localData[r1].data; - var data2 = self.localData[r2].data; + var data = list._changes || list.data; + data.sort(function (id1, id2) { + var r1 = self.localData[id1]; + var r2 = self.localData[id2]; + var data1 = r1._changes || r1.data; + var data2 = r2._changes || r2.data; if (data1[order.name] < data2[order.name]) { return order.asc ? -1 : 1; } -- cgit v1.2.3