From 38b7462dcf7541bbcb51ac75cc1f7b6715f13ace Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Fri, 7 Aug 2020 08:48:55 +0000 Subject: [PATCH] [REF] web: BasicModel: combine default_get and onchange This commit adapts the BasicModel to combine calls to `default_get` and (first) `onchange`. When creating a new record, we now only call `onchange`, which thus return default values and potential onchange values. Tests (and the MockServer) have been adapted accordingly. NOTE 1. If the `default_get` within the `onchange` returns a value for a field that is not in the view, we ignore it, and it won't be saved. Before, that value was kept and sent upon save. This change in behavior may prove problematic, although the overall risk is small. Decision has been made to keep heavy comments and code snippets if we were to revert back somehow to the previous situation. NOTE 2. Putting a context on a many2one field may change the value returned by `name_get` for that field. By default, the calls to `name_get` are done by `onchange`. If the context on a field must be used for `name_get`, one has to set the option `always_reload` to `True` on the field. In that case, every `onchange` that changes the value will trigger an extra `name_get`. NOTE 3. Suppose that a one2many field has a list view with field A, and a form view without field A. When adding a line, we now send all known fields (main view and inline views) to the `onchange`, which may return a default value for A. The value will appear on the list view, but not in the form view. The former behavior was to call `default_get` with the fields that occur in the form view only, and therefore field A would be left to value `False`. NOTE 4. A test surprisingly adds an extra call to `read`. The test was actually wrong before. With the changes in MockServer, we now correctly receive a command `[6, false, [1]]`, whereas before we received `[1]`, which isn't a valid command, and which was ignored. As a consequence, an extra `read` is done, whereas the test asserts it shouldn't. But it already didn't work before (I checked by sending the correct command). This needs to be bugfixed elsewhere (task-id-2323491): in a o2m with a onchange and default order records on an other page than the first should not trigger a `read`. Task 2261084 Co-authored-by: Raphael Collet Co-authored-by: Aaron Bohy --- .../static/src/js/views/basic/basic_model.js | 453 +++++++----------- .../static/src/js/views/basic/basic_view.js | 2 +- .../tests/chrome/action_manager_tests.js | 16 +- .../relational_fields/field_many2one_tests.js | 130 ++--- .../relational_fields/field_one2many_tests.js | 157 +++--- .../tests/fields/relational_fields_tests.js | 20 +- .../web/static/tests/helpers/mock_server.js | 99 +++- .../static/tests/views/basic_model_tests.js | 37 +- addons/web/static/tests/views/form_tests.js | 224 ++++----- addons/web/static/tests/views/kanban_tests.js | 21 +- addons/web/static/tests/views/list_tests.js | 28 +- .../base/static/tests/base_settings_tests.js | 4 +- 12 files changed, 525 insertions(+), 666 deletions(-) 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 8878de55a3e..24373e3b753 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -281,90 +281,6 @@ var BasicModel = AbstractModel.extend({ }); }, - /** - * Add and process default values for a given record. Those values are - * parsed and stored in the '_changes' key of the record. For relational - * fields, sub-dataPoints are created, and missing relational data is - * fetched. Also generate default values for fields with no given value. - * Typically, this function is called with the result of a 'default_get' - * RPC, to populate a newly created dataPoint. It may also be called when a - * one2many subrecord is open in a form view (dialog), to generate the - * default values for the fields displayed in the o2m form view, but not in - * the list or kanban (mainly to correctly create sub-dataPoints for - * relational fields). - * - * @param {string} recordID local id for a record - * @param {Object} values dict of default values for the given record - * @param {Object} [options] - * @param {string} [options.viewType] current viewType. If not set, we will - * assume main viewType from the record - * @param {Array} [options.fieldNames] list of field names for which a - * default value must be generated (used to complete the values dict) - * @returns {Promise} - */ - applyDefaultValues: function (recordID, values, options) { - options = options || {}; - var record = this.localData[recordID]; - var viewType = options.viewType || record.viewType; - var fieldNames = options.fieldNames || Object.keys(record.fieldsInfo[viewType]); - var field; - var fieldName; - record._changes = record._changes || {}; - - // ignore values for non requested fields (for instance, fields that are - // not in the view) - values = _.pick(values, fieldNames); - - // fill default values for missing fields - for (var i = 0; i < fieldNames.length; i++) { - fieldName = fieldNames[i]; - if (!(fieldName in values) && !(fieldName in record._changes)) { - field = record.fields[fieldName]; - if (field.type === 'float' || - field.type === 'integer' || - field.type === 'monetary') { - values[fieldName] = 0; - } else if (field.type === 'one2many' || field.type === 'many2many') { - values[fieldName] = []; - } else { - values[fieldName] = null; - } - } - } - - // parse each value and create dataPoints for relational fields - var defs = []; - for (fieldName in values) { - field = record.fields[fieldName]; - record.data[fieldName] = null; - var dp; - if (field.type === 'many2one' && values[fieldName]) { - dp = this._makeDataPoint({ - context: record.context, - data: {id: values[fieldName]}, - modelName: field.relation, - parentID: record.id, - }); - record._changes[fieldName] = dp.id; - } else if (field.type === 'reference' && values[fieldName]) { - var ref = values[fieldName].split(','); - dp = this._makeDataPoint({ - context: record.context, - data: {id: parseInt(ref[1])}, - modelName: ref[0], - parentID: record.id, - }); - defs.push(this._fetchNameGet(dp)); - record._changes[fieldName] = dp.id; - } else if (field.type === 'one2many' || field.type === 'many2many') { - defs.push(this._processX2ManyCommands(record, fieldName, values[fieldName], options)); - } else { - record._changes[fieldName] = this._parseServerValue(field, values[fieldName]); - } - } - - return Promise.all(defs); - }, /** * Onchange RPCs may return values for fields that are not in the current * view. Those fields might even be unknown when the onchange returns (e.g. @@ -384,7 +300,7 @@ var BasicModel = AbstractModel.extend({ */ applyRawChanges: function (recordID, viewType) { var record = this.localData[recordID]; - return this._applyOnChange(record._rawChanges, record, viewType).then(function () { + return this._applyOnChange(record._rawChanges, record, { viewType }).then(function () { return record.id; }); }, @@ -725,6 +641,46 @@ var BasicModel = AbstractModel.extend({ } return list; }, + /** + * Generate default values for a given record. Those values are stored in + * the '_changes' key of the record. For relational fields, sub-dataPoints + * are created, and missing relational data is fetched. + * Typically, this function is called when a new record is created. It may + * also be called when a one2many subrecord is open in a form view (dialog), + * to generate the default values for the fields displayed in the o2m form + * view, but not in the list or kanban (mainly to correctly create + * sub-dataPoints for relational fields). + * + * @param {string} recordID local id for a record + * @param {Object} [options] + * @param {string} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record + * @param {Array} [options.fieldNames] list of field names for which a + * default value must be generated (used to complete the values dict) + * @returns {Promise} + */ + generateDefaultValues(recordID, options = {}) { + const record = this.localData[recordID]; + const viewType = options.viewType || record.viewType; + const fieldNames = options.fieldNames || Object.keys(record.fieldsInfo[viewType]); + const numericFields = ['float', 'integer', 'monetary']; + const proms = []; + record._changes = record._changes || {}; + fieldNames.forEach(fieldName => { + record.data[fieldName] = null; + if (!(fieldName in record._changes)) { + const field = record.fields[fieldName]; + if (numericFields.includes(field.type)) { + record._changes[fieldName] = 0; + } else if (field.type === 'one2many' || field.type === 'many2many') { + proms.push(this._processX2ManyCommands(record, fieldName, [], options)); + } else { + record._changes[fieldName] = null; + } + } + }); + return Promise.all(proms); + }, /** * Returns the current display_name for the record. * @@ -1575,7 +1531,7 @@ var BasicModel = AbstractModel.extend({ } return new Promise(function (resolve, reject) { if (onChangeFields.length) { - self._performOnChange(record, onChangeFields, options.viewType) + self._performOnChange(record, onChangeFields, { viewType: options.viewType }) .then(function (result) { delete record._warning; resolve(_.keys(changes).concat(Object.keys(result && result.value || {}))); @@ -1693,16 +1649,20 @@ var BasicModel = AbstractModel.extend({ * @param {Object} values the result of the onchange RPC (a mapping of * fieldnames to their value) * @param {Object} record - * @param {string} [viewType] current viewType. If not set, we will assume + * @param {Object} [options={}] + * @param {string} [options.viewType] current viewType. If not set, we will assume * main viewType from the record + * @param {string} [options.firstOnChange] set to true if this is the first + * onchange (if so, some initialization will need to be done) * @returns {Promise} */ - _applyOnChange: function (values, record, viewType) { + _applyOnChange: function (values, record, options = {}) { var self = this; var defs = []; var rec; - viewType = viewType || record.viewType; + const viewType = options.viewType || record.viewType; record._changes = record._changes || {}; + _.each(values, function (val, name) { var field = record.fields[name]; if (!field) { @@ -1715,6 +1675,13 @@ var BasicModel = AbstractModel.extend({ // in the dict of values, there is a value for a field that is // not in the one2many list, but that is in the one2many form. record._rawChanges[name] = val; + // LPE TODO 1 taskid-2261084: remove this entire comment including code snippet + // when the change in behavior has been thoroughly tested. + // It is impossible to distinguish between values returned by the default_get + // and those returned by the onchange. Since those are not in _changes, they won't be saved. + // if (options.firstOnChange) { + // record._changes[name] = val; + // } return; } if (record._rawChanges[name]) { @@ -1832,6 +1799,14 @@ var BasicModel = AbstractModel.extend({ } rec = self._makeDataPoint(params); list._cache[rec.res_id] = rec.id; + if (options.firstOnChange) { + // this is necessary so the fields are initialized + rec.getFieldNames().forEach(fieldName => { + if (!(fieldName in rec.data)) { + rec.data[fieldName] = null; + } + }); + } } // Do not abandon the record if it has been created // from `default_get`. The list has a savepoint only @@ -1841,7 +1816,9 @@ var BasicModel = AbstractModel.extend({ if (command[0] === 1) { list._changes.push({operation: 'UPDATE', id: rec.id}); } - defs.push(self._applyOnChange(command[2], rec)); + defs.push(self._applyOnChange(command[2], rec, { + firstOnChange: options.firstOnChange, + })); } else if (command[0] === 4) { // LINK TO linkRecord(list, command[1]); @@ -2200,29 +2177,33 @@ var BasicModel = AbstractModel.extend({ * An onchange spec is necessary as an argument to the /onchange route. It * looks like this: { field: "1", anotherField: "", relation.subField: "1"} * + * The first onchange call will fill up the record with default values, so + * we need to send every field name known to us in this case. + * * @see _performOnChange * * @param {Object} record resource object of type 'record' * @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 + * @returns {Object} with two keys + * - 'hasOnchange': true iff there is at least a field with onchange + * - 'onchangeSpec': the onchange spec */ _buildOnchangeSpecs: function (record, viewType) { - var hasOnchange = false; - var specs = {}; + let hasOnchange = false; + const onchangeSpec = {}; var fieldsInfo = record.fieldsInfo[viewType || record.viewType]; generateSpecs(fieldsInfo, record.fields); // recursively generates the onchange specs for fields in fieldsInfo, // and their subviews - function generateSpecs (fieldsInfo, fields, prefix) { + function generateSpecs(fieldsInfo, fields, prefix) { prefix = prefix || ''; _.each(Object.keys(fieldsInfo), function (name) { var field = fields[name]; var fieldInfo = fieldsInfo[name]; var key = prefix + name; - specs[key] = (field.onChange) || ""; + onchangeSpec[key] = (field.onChange) || ""; if (field.onChange) { hasOnchange = true; } @@ -2233,7 +2214,7 @@ var BasicModel = AbstractModel.extend({ } }); } - return hasOnchange ? specs : false; + return { hasOnchange, onchangeSpec }; }, /** * Ensures that dataPoint ids are always synchronized between the main and @@ -2381,27 +2362,6 @@ var BasicModel = AbstractModel.extend({ } return evaluated; }, - /** - * Fetch all name_gets for the many2ones in a group - * - * @param {Object[]} groups a list of object with context and record sub keys - * @returns {Promise} - */ - _fetchMany2OneGroup: function (groups) { - var ids = _.uniq(_.pluck(_.pluck(groups, 'record'), 'res_id')); - return this._rpc({ - model: groups[0].record.model, - method: 'name_get', - args: [ids], - context: groups[0].context - }) - .then(function (name_gets) { - _.each(groups, function (obj) { - var nameGet = _.find(name_gets, function (n) { return n[0] === obj.record.res_id;}); - obj.record.data.display_name = nameGet[1]; - }); - }); - }, /** * Fetch name_get for a record datapoint. * @@ -2736,50 +2696,6 @@ var BasicModel = AbstractModel.extend({ self._updateRecordsData(records, fieldName, result); }); }, - /** - * This method is incorrectly named. It should be named something like - * _fetchMany2OneData. - * - * For a given record, this method fetches all many2ones information, - * batching the requests if possible (for example, if 3 many2ones are in - * relation on the same model, then we can probably fetch them in one rpc) - * - * This method is currently only called by _makeDefaultRecord, it should be - * called by the onchange methods at some point. - * - * @todo fix bug: returns a list of promise, not a promise - * - * @param {Object} record a valid resource object - * @returns {Promise} - */ - _fetchRelationalData: function (record) { - var self = this; - var toBeFetched = []; - - // find all many2one related records to be fetched - _.each(record.getFieldNames(), function (name) { - var field = record.fields[name]; - if (field.type === 'many2one' && !record.fieldsInfo[record.viewType][name].__no_fetch) { - var localId = (record._changes && record._changes[name]) || record.data[name]; - var relatedRecord = self.localData[localId]; - if (!relatedRecord || relatedRecord.data.display_name) { - return; - } - toBeFetched.push({ - context: record.getContext({fieldName: name, viewType: record.viewType}), - record: relatedRecord - }); - } - }); - - // group them by model and context. Using the context as key is - // necessary to make sure the correct context is used for the rpc; - var groups = _.groupBy(toBeFetched, function (elem) { - return [elem.record.model, JSON.stringify(elem.context)].join(); - }); - - return Promise.all(_.map(groups, this._fetchMany2OneGroup.bind(this))); - }, /** * Check the AbstractField specializations that are (will be) used by the * given record and fetch the special data they will need. Special data are @@ -3267,7 +3183,6 @@ var BasicModel = AbstractModel.extend({ changes[fieldName] = false; } } - return changes; }, /** @@ -3284,17 +3199,20 @@ var BasicModel = AbstractModel.extend({ */ _generateOnChangeData: function (record, options) { options = _.extend({}, options || {}, {withReadonly: true}); - var commands = this._generateX2ManyCommands(record, options); - var data = _.extend(this.get(record.id, {raw: true}).data, commands); - // 'display_name' is automatically added to the list of fields to fetch, - // when fetching a record, even if it doesn't appear in the view. However, - // only the fields in the view must be passed to the onchange RPC, so we - // remove it from the data sent by RPC if it isn't in the view. - var hasDisplayName = _.some(record.fieldsInfo, function (fieldsInfo) { - return 'display_name' in fieldsInfo; - }); - if (!hasDisplayName) { - delete data.display_name; + var data = {}; + if (!options.firstOnChange) { + var commands = this._generateX2ManyCommands(record, options); + data = _.extend(this.get(record.id, {raw: true}).data, commands); + // 'display_name' is automatically added to the list of fields to fetch, + // when fetching a record, even if it doesn't appear in the view. However, + // only the fields in the view must be passed to the onchange RPC, so we + // remove it from the data sent by RPC if it isn't in the view. + var hasDisplayName = _.some(record.fieldsInfo, function (fieldsInfo) { + return 'display_name' in fieldsInfo; + }); + if (!hasDisplayName) { + delete data.display_name; + } } // one2many records have a parentID @@ -4093,14 +4011,11 @@ var BasicModel = AbstractModel.extend({ * @param {string} params.viewType the key in fieldsInfo of the fields to load * @returns {Promise} resolves to the id for the created resource */ - _makeDefaultRecord: function (modelName, params) { - var self = this; - + async _makeDefaultRecord(modelName, params) { var targetView = params.viewType; var fields = params.fields; var fieldsInfo = params.fieldsInfo; var fieldNames = Object.keys(fieldsInfo[targetView]); - var fields_key = _.without(fieldNames, '__last_update'); // Fields that are present in the originating view, that need to be initialized // Hence preventing their value to crash when getting back to the originating view @@ -4113,69 +4028,42 @@ var BasicModel = AbstractModel.extend({ fields = _.defaults({}, fields, parentRecord.fields); } - return this._rpc({ - model: modelName, - method: 'default_get', - args: [fields_key], - context: params.context, - }) - .then(function (result) { - var record = self._makeDataPoint({ - modelName: modelName, - fields: fields, - fieldsInfo: fieldsInfo, - context: params.context, - parentID: params.parentID, - res_ids: params.res_ids, - viewType: targetView, - }); + var record = this._makeDataPoint({ + modelName: modelName, + fields: fields, + fieldsInfo: fieldsInfo, + context: params.context, + parentID: params.parentID, + res_ids: params.res_ids, + viewType: targetView, + }); - // We want to overwrite the default value of the handle field (if any), - // in order for new lines to be added at the correct position. - // -> This is a rare case where the defaul_get from the server - // will be ignored by the view for a certain field (usually "sequence"). + await this.generateDefaultValues(record.id, {}, { fieldNames }); + try { + await this._performOnChange(record, [], { firstOnChange: true }); + } finally { + if (record._warning && params.allowWarning) { + delete record._warning; + } + } + if (record._warning) { + return Promise.reject(); + } - var overrideDefaultFields = self._computeOverrideDefaultFields( - params.parentID, - params.position - ); + // We want to overwrite the default value of the handle field (if any), + // in order for new lines to be added at the correct position. + // -> This is a rare case where the defaul_get from the server + // will be ignored by the view for a certain field (usually "sequence"). + var overrideDefaultFields = this._computeOverrideDefaultFields(params.parentID, params.position); + if (overrideDefaultFields.field) { + record._changes[overrideDefaultFields.field] = overrideDefaultFields.value; + } - if (overrideDefaultFields) { - result[overrideDefaultFields.field] = overrideDefaultFields.value; - } - - return self.applyDefaultValues(record.id, result, {fieldNames: fieldNames}) - .then(function () { - var def = new Promise(function (resolve, reject) { - var always = function () { - if (record._warning) { - if (params.allowWarning) { - delete record._warning; - } else { - reject(); - } - } - resolve(); - }; - self._performOnChange(record, fields_key) - .then(always).guardedCatch(always); - }); - return def; - }) - .then(function () { - return self._fetchRelationalData(record); - }) - .then(function () { - return self._postprocess(record); - }) - .then(function () { - // save initial changes, so they can be restored later, - // if we need to discard. - self.save(record.id, {savePoint: true}); - - return record.id; - }); - }); + // fetch additional data (special data and many2one namegets for "always_reload" fields) + await this._postprocess(record); + // save initial changes, so they can be restored later, if we need to discard + this.save(record.id, { savePoint: true }); + return record.id; }, /** * parse the server values to javascript framwork @@ -4224,54 +4112,58 @@ var BasicModel = AbstractModel.extend({ * applied to the record. * * @param {Object} record - * @param {string[]} fields changed fields - * @param {string} [viewType] current viewType. If not set, we will assume + * @param {string[]} fields changed fields (empty list in the case of first + * onchange) + * @param {Object} [options={}] + * @param {string} [options.viewType] current viewType. If not set, we will assume * main viewType from the record + * @param {boolean} [options.firstOnChange=false] set to true if this is the + * first onchange * @returns {Promise} */ - _performOnChange: function (record, fields, viewType) { - var self = this; - var onchangeSpec = this._buildOnchangeSpecs(record, viewType); - if (!onchangeSpec) { - return Promise.resolve(); + async _performOnChange(record, fields, options = {}) { + const firstOnChange = options.firstOnChange; + let { hasOnchange, onchangeSpec } = this._buildOnchangeSpecs(record, options.viewType); + if (!firstOnChange && !hasOnchange) { + return; } var idList = record.data.id ? [record.data.id] : []; - var options = { + const ctxOptions = { full: true, }; if (fields.length === 1) { fields = fields[0]; // if only one field changed, add its context to the RPC context - options.fieldName = fields; + ctxOptions.fieldName = fields; } - var context = this._getContext(record, options); - var currentData = this._generateOnChangeData(record, {changesOnly: false}); + var context = this._getContext(record, ctxOptions); + var currentData = this._generateOnChangeData(record, { + changesOnly: false, + firstOnChange, + }); - return self._rpc({ - model: record.model, - method: 'onchange', - args: [idList, currentData, fields, onchangeSpec], - context: context, - }) - .then(function (result) { - if (!record._changes) { - // if the _changes key does not exist anymore, it means that - // it was removed by discarding the changes after the rpc - // to onchange. So, in that case, the proper response is to - // ignore the onchange. - return; - } - if (result.warning) { - self.trigger_up('warning', result.warning); - record._warning = true; - } - if (result.domain) { - record._domains = _.extend(record._domains, result.domain); - } - return self._applyOnChange(result.value, record).then(function () { - return result; - }); - }); + const result = await this._rpc({ + model: record.model, + method: 'onchange', + args: [idList, currentData, fields, onchangeSpec], + context: context, + }); + if (!record._changes) { + // if the _changes key does not exist anymore, it means that + // it was removed by discarding the changes after the rpc + // to onchange. So, in that case, the proper response is to + // ignore the onchange. + return; + } + if (result.warning) { + this.trigger_up('warning', result.warning); + record._warning = true; + } + if (result.domain) { + record._domains = Object.assign(record._domains, result.domain); + } + await this._applyOnChange(result.value, record, { firstOnChange }); + return result; }, /** * This function accumulates RPC requests done in the same call stack, and @@ -4376,16 +4268,21 @@ var BasicModel = AbstractModel.extend({ var fieldInfo = record.fieldsInfo[viewType][name] || {}; var options = fieldInfo.options || {}; if (options.always_reload) { - if (record.fields[name].type === 'many2one' && record.data[name]) { - var element = self.localData[record.data[name]]; + if (record.fields[name].type === 'many2one') { + const _changes = record._changes || {}; + const relRecordId = _changes[name] || record.data[name]; + if (!relRecordId) { + return; // field is unset, no need to do the name_get + } + var relRecord = self.localData[relRecordId]; defs.push(self._rpc({ model: field.relation, method: 'name_get', - args: [element.data.id], + args: [relRecord.data.id], context: self._getContext(record, {fieldName: name, viewType: viewType}), }) .then(function (result) { - element.data.display_name = result[0][1]; + relRecord.data.display_name = result[0][1]; })); } } diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index eb6ef5a2549..5e3938fb3ce 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -184,7 +184,7 @@ var BasicView = AbstractView.extend({ var def; if (fieldNames.length) { if (model.isNew(record.id)) { - def = model.applyDefaultValues(record.id, {}, { + def = model.generateDefaultValues(record.id, { fieldNames: fieldNames, viewType: viewType, }); diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 02c6fbda8e4..6f26376b736 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -833,7 +833,7 @@ QUnit.module('ActionManager', { assert.verifySteps([ '/web/action/load', 'load_views', - 'default_get', + 'onchange', ]); actionManager.destroy(); @@ -2062,7 +2062,7 @@ QUnit.module('ActionManager', { assert.verifySteps([ '/web/action/load', // action 5 'load_views', - 'default_get', + 'onchange', '/web/action/load', // action 2 '/web/action/run', 'close handler', @@ -2863,7 +2863,7 @@ QUnit.module('ActionManager', { '/web/action/load', 'load_views', '/web/dataset/search_read', // list - 'default_get', // form + 'onchange', // form '/web/dataset/search_read', // list ]); @@ -3649,7 +3649,7 @@ QUnit.module('ActionManager', { }); QUnit.test('execute action from dirty, new record, and come back', async function (assert) { - assert.expect(19); + assert.expect(17); this.data.partner.fields.bar.default = 1; this.archs['partner,false,form'] = '
' + @@ -3710,13 +3710,11 @@ QUnit.module('ActionManager', { '/web/action/load', // action 3 'load_views', // views of action 3 '/web/dataset/search_read', // list - 'default_get', // form (create) - 'name_get', // m2o in form + 'onchange', // form (create) 'get_formview_action', // click on m2o 'load_views', // form view of dynamic action 'read', // form - 'default_get', // form (create) - 'name_get', // m2o in form + 'onchange', // form (create) ]); actionManager.destroy(); @@ -3791,7 +3789,7 @@ QUnit.module('ActionManager', { assert.verifySteps([ '/web/action/load', 'load_views', - 'default_get', + 'onchange', ]); actionManager.destroy(); diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js index 54abc553c8c..50e7ae2e6cb 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js @@ -88,6 +88,7 @@ QUnit.module('fields', {}, function () { }, partner_type: { fields: { + display_name: { string: "Partner Type", type: "char" }, name: { string: "Partner Type", type: "char" }, color: { string: "Color index", type: "integer" }, }, @@ -266,7 +267,7 @@ QUnit.module('fields', {}, function () { data: this.data, arch: '' + '' + - '' + + '' + '', mockRPC: function (route, args) { if (args.method === 'name_get') { @@ -1393,6 +1394,8 @@ QUnit.module('fields', {}, function () { QUnit.test('list in form: create with one2many with many2one', async function (assert) { assert.expect(1); + this.data.partner.fields.p.default = [[0, 0, { display_name: 'new record', p: [] }]]; + var form = await createView({ View: FormView, model: 'partner', @@ -1408,13 +1411,8 @@ QUnit.module('fields', {}, function () { '' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ p: [[0, 0, { display_name: 'new record' }]] }); - } else if (args.method === 'name_get') { - // This should not be called at all and thus is not accounted for - // in the assert.expect. If this is called, you broke this test. - assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'), - "should not call name_get for the m2o inside o2m which has no value"); + if (args.method === 'name_get') { + throw new Error('Nameget should not be called'); } return this._super.apply(this, arguments); }, @@ -1432,6 +1430,10 @@ QUnit.module('fields', {}, function () { // which is stupid, but this happens, so we have to handle it assert.expect(1); + this.data.partner.fields.p.default = [ + [0, 0, { display_name: 'new record', trululu: false, p: [] }] + ]; + var form = await createView({ View: FormView, model: 'partner', @@ -1447,13 +1449,8 @@ QUnit.module('fields', {}, function () { '' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ p: [[0, 0, { display_name: 'new record', trululu: false }]] }); - } else if (args.method === 'name_get') { - // This should not be called at all and thus is not accounted for - // in the assert.expect. If this is called, you broke this test. - assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'), - "should not call name_get for the m2o inside o2m which has no value"); + if (args.method === 'name_get') { + throw new Error('Nameget should not be called'); } return this._super.apply(this, arguments); }, @@ -1472,6 +1469,10 @@ QUnit.module('fields', {}, function () { // abandonned, since it has been added (even though it is a new record). assert.expect(8); + this.data.partner.fields.p.default = [ + [0, 0, { display_name: 'new record', trululu: false, p: [] }] + ]; + var form = await createView({ View: FormView, model: 'partner', @@ -1486,12 +1487,6 @@ QUnit.module('fields', {}, function () { '' + '' + '', - mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ p: [[0, 0, { display_name: 'new record', trululu: false }]] }); - } - return this._super.apply(this, arguments); - }, }); assert.strictEqual($('tr.o_data_row').length, 1, @@ -1524,9 +1519,13 @@ QUnit.module('fields', {}, function () { }); QUnit.test('list in form: name_get with unique ids (default_get)', async function (assert) { - assert.expect(2); + assert.expect(1); this.data.partner.records[0].display_name = "MyTrululu"; + this.data.partner.fields.p.default = [ + [0, 0, { trululu: 1, p: [] }], + [0, 0, { trululu: 1, p: [] }] + ]; var form = await createView({ View: FormView, @@ -1542,17 +1541,8 @@ QUnit.module('fields', {}, function () { '' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ - p: [ - [0, 0, { trululu: 1 }], - [0, 0, { trululu: 1 }] - ] - }); - } if (args.method === 'name_get') { - assert.deepEqual(args.args[0], _.uniq(args.args[0]), - "should not have duplicates in name_get rpc"); + throw new Error('should not call name_get'); } return this._super.apply(this, arguments); }, @@ -1567,6 +1557,11 @@ QUnit.module('fields', {}, function () { QUnit.test('list in form: show name of many2one fields in multi-page (default_get)', async function (assert) { assert.expect(4); + this.data.partner.fields.p.default = [ + [0, 0, { display_name: 'record1', trululu: 1, p: [] }], + [0, 0, { display_name: 'record2', trululu: 2, p: [] }] + ]; + var form = await createView({ View: FormView, model: 'partner', @@ -1581,17 +1576,6 @@ QUnit.module('fields', {}, function () { '' + '' + '', - mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ - p: [ - [0, 0, { display_name: 'record1', trululu: 1 }], - [0, 0, { display_name: 'record2', trululu: 2 }] - ] - }); - } - return this._super.apply(this, arguments); - }, }); assert.strictEqual(form.$('td.o_data_cell').first().text(), @@ -1620,6 +1604,7 @@ QUnit.module('fields', {}, function () { var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + this.data.partner.fields.product_id.default = 37; this.data.partner.onchanges = { product_id: function (obj) { if (obj.product_id === 37) { @@ -1641,14 +1626,6 @@ QUnit.module('fields', {}, function () { '' + '
' + '', - mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ - product_id: 37, - }); - } - return this._super.apply(this, arguments); - }, }); // check that there is a record in the editable list with empty string as required field @@ -1859,17 +1836,12 @@ QUnit.module('fields', {}, function () { }); QUnit.test('list in form: default_get with x2many create', async function (assert) { - assert.expect(5); - + assert.expect(3); + this.data.partner.fields.timmy.default = [ + [0, 0, { display_name: 'brandon is the new timmy', name: 'brandon' }] + ]; var displayName = 'brandon is the new timmy'; this.data.partner.onchanges.timmy = function (obj) { - assert.deepEqual( - obj.timmy, - [ - [6, false, []], - [0, obj.timmy[1][1], { display_name: displayName, name: 'brandon' }] - ], - "should have properly created the x2many command list"); obj.int_field = obj.timmy.length; }; @@ -1888,15 +1860,18 @@ QUnit.module('fields', {}, function () { '' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ timmy: [[0, 0, { display_name: 'brandon is the new timmy', name: 'brandon' }]] }); - } if (args.method === 'create') { assert.deepEqual(args.args[0], { int_field: 2, timmy: [ [6, false, []], - [0, args.args[0].timmy[1][1], { display_name: displayName, name: 'brandon' }], + // LPE TODO 1 taskid-2261084: remove this entire comment including code snippet + // when the change in behavior has been thoroughly tested. + // We can't distinguish a value coming from a default_get + // from one coming from the onchange, and so we can either store and + // send it all the time, or never. + // [0, args.args[0].timmy[1][1], { display_name: displayName, name: 'brandon' }], + [0, args.args[0].timmy[1][1], { display_name: displayName }], ], }, "should send the correct values to create"); } @@ -1906,7 +1881,7 @@ QUnit.module('fields', {}, function () { assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy', "should have created the new record in the m2m with the correct name"); - assert.strictEqual($('input.o_field_integer').val(), '2', + assert.strictEqual($('input.o_field_integer').val(), '1', "should have called and executed the onchange properly"); // edit the subrecord and save @@ -1919,17 +1894,9 @@ QUnit.module('fields', {}, function () { }); QUnit.test('list in form: default_get with x2many create and onchange', async function (assert) { - assert.expect(2); + assert.expect(1); - this.data.partner.onchanges.turtles = function (obj) { - assert.deepEqual( - obj.turtles, - [ - [4, 2, false], - [4, 3, false], - ], - "should have properly created the x2many command list"); - }; + this.data.partner.fields.turtles.default = [[6, 0, [2, 3]]]; var form = await createView({ View: FormView, @@ -1946,9 +1913,6 @@ QUnit.module('fields', {}, function () { '' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ turtles: [[6, 0, [2, 3]]] }); - } if (args.method === 'create') { assert.deepEqual(args.args[0].turtles, [ [4, 2, false], @@ -2187,14 +2151,14 @@ QUnit.module('fields', {}, function () { '', mockRPC: function (route, args) { count++; - if (args.method === 'name_get' && args.args[0][0] === 2) { + if (args.method === 'name_get' && args.args[0] === 2) { return Promise.resolve([[2, "hello world\nso much noise"]]); } return this._super(route, args); }, }); - assert.strictEqual(count, 3, "should have done 3 rpcs (default_get, onchange, name_get)"); + assert.strictEqual(count, 2, "should have done 2 rpcs (onchange and name_get)"); assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'hello world', "should have taken the correct display name"); form.destroy(); @@ -2896,7 +2860,7 @@ QUnit.module('fields', {}, function () { }); assert.verifySteps([ - 'default_get', + 'onchange', 'name_search', // to display results in the dropdown 'load_views', // list view in dialog '/web/dataset/search_read', // to display results in the dialog @@ -2950,7 +2914,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click($('.modal .o_cp_searchview .o_facet_remove')); assert.verifySteps([ - 'default_get', + 'onchange', 'name_search', // empty search, triggered when the user clicks in the input 'name_search', // to display results in the dropdown 'name_search', // to get preselected ids matching the search @@ -3063,7 +3027,7 @@ QUnit.module('fields', {}, function () { $modal.find('tbody tr').first(), { position: 'top' }); assert.verifySteps([ - 'default_get', + 'onchange', 'name_search', // to display results in the dropdown 'load_views', // list view in dialog '/web/dataset/search_read', // to display results in the dialog diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index c3f43289c40..6cb765b6feb 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -164,7 +164,7 @@ QUnit.module('fields', {}, function () { QUnit.module('FieldOne2Many'); QUnit.test('New record with a o2m also with 2 new records, ordered, and resequenced', async function (assert) { - assert.expect(3); + assert.expect(2); // Needed to have two new records in a single stroke this.data.partner.onchanges = { @@ -208,9 +208,7 @@ QUnit.module('fields', {}, function () { { position: 'top' } ); - // Only those two should have been called - // name_get on trululu would trigger an traceback - assert.verifySteps(['default_get partner', 'onchange partner']); + assert.verifySteps(['onchange partner']); form.destroy(); }); @@ -1077,21 +1075,20 @@ QUnit.module('fields', {}, function () { model: 'partner', data: data, arch: '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + '
', res_id: 1, }); - await testUtils.form.clickEdit(form); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29", @@ -2298,27 +2295,27 @@ QUnit.module('fields', {}, function () { model: 'partner', data: this.data, arch: '
' + - '' + - '' + - // color will be in the kanban but not in the form - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - // foo will be in the form but not in the kanban - '' + - '' + - '' + + '' + + '' + + // color will be in the kanban but not in the form + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '' + + // foo will be in the form but not in the kanban + '' + + '' + + '' + '', res_id: 1, mockRPC: function (route, args) { @@ -2375,7 +2372,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2, 'should contain 2 records'); - assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1', + assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1Red', 'value of newly created subrecord should be "new subrecord 1"'); // create two new subrecords @@ -2867,7 +2864,7 @@ QUnit.module('fields', {}, function () { assert.containsNone(form, 'tr.o_data_row', "should still have 0 data rows"); - assert.verifySteps(['read', 'default_get', 'default_get']); + assert.verifySteps(['read', 'onchange', 'onchange']); form.destroy(); }); @@ -3133,12 +3130,12 @@ QUnit.module('fields', {}, function () { await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'enter'); assert.hasClass(form.$('input[name="turtle_foo"]'), 'o_field_invalid', "input should be marked invalid"); - assert.verifySteps(['read', 'default_get']); + assert.verifySteps(['read', 'onchange']); form.destroy(); }); QUnit.test('editing a o2m, with required field and onchange', async function (assert) { - assert.expect(12); + assert.expect(11); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; @@ -3189,7 +3186,7 @@ QUnit.module('fields', {}, function () { assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1, "should have one row with turtle_int value"); - assert.verifySteps(['read', 'default_get', 'onchange', 'onchange', 'write', 'read', 'read']); + assert.verifySteps(['read', 'onchange', 'onchange', 'write', 'read', 'read']); form.destroy(); }); @@ -3222,7 +3219,7 @@ QUnit.module('fields', {}, function () { await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'escape'); assert.containsNone(form, 'tr.o_data_row', "data row should have been discarded"); - assert.verifySteps(['read', 'default_get']); + assert.verifySteps(['read', 'onchange']); form.destroy(); }); @@ -3257,7 +3254,7 @@ QUnit.module('fields', {}, function () { await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'escape'); assert.containsNone(form, 'tr.o_data_row', "data row should have been discarded"); - assert.verifySteps(['read', 'default_get']); + assert.verifySteps(['read', 'onchange']); form.destroy(); }); @@ -3921,6 +3918,7 @@ QUnit.module('fields', {}, function () { QUnit.test('one2many list with a many2one', async function (assert) { assert.expect(5); + let checkOnchange = false; this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; this.data.partner.onchanges.p = function (obj) { @@ -3949,7 +3947,7 @@ QUnit.module('fields', {}, function () { '
', }, mockRPC: function (route, args) { - if (args.method === 'onchange') { + if (args.method === 'onchange' && checkOnchange) { assert.deepEqual(args.args[1].p, [[4, 2, false], [0, args.args[1].p[1][1], { product_id: 41 }]], "should trigger onchange with correct parameters"); } @@ -3966,6 +3964,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); + checkOnchange = true; await testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickItem('product_id', 'xpad'); @@ -4006,7 +4005,7 @@ QUnit.module('fields', {}, function () { mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], { - foo: false, int_field: 123, product_id: 41, + foo: "My little Foo Value", int_field: 123, product_id: 41, }]]); } return this._super(route, args); @@ -4202,7 +4201,7 @@ QUnit.module('fields', {}, function () { '', res_id: 2, mockRPC: function (route, args) { - if (args.method === 'default_get') { + if (args.method === 'onchange') { assert.strictEqual(args.kwargs.context.date, '2017-01-25', "should have properly evaluated date key in context"); } @@ -4236,7 +4235,7 @@ QUnit.module('fields', {}, function () { '', res_id: 1, mockRPC: function (route, args) { - if (args.method === 'default_get') { + if (args.method === 'onchange') { var expected = counter === 0 ? [[4, 2, false]] : [[4, 2, false], [0, args.kwargs.context.turtles[1][1], { turtle_foo: 'hammer' }]]; @@ -4310,7 +4309,7 @@ QUnit.module('fields', {}, function () { '', res_id: 1, mockRPC: function (route, args) { - if (args.method === 'default_get') { + if (args.method === 'onchange') { var context = args.kwargs.context; assert.strictEqual(context.hello, "world"); assert.strictEqual(context.abc, 10); @@ -4497,7 +4496,7 @@ QUnit.module('fields', {}, function () { '
' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { + if (args.method === 'onchange') { n++; if (n === 2) { var context = args.kwargs.context; @@ -4546,7 +4545,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('parent data is properly sent on an onchange rpc, new record', async function (assert) { - assert.expect(6); + assert.expect(4); this.data.turtle.onchanges = { turtle_bar: function () { } }; var form = await createView({ @@ -4572,7 +4571,7 @@ QUnit.module('fields', {}, function () { }, }); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); - assert.verifySteps(['default_get', 'onchange', 'default_get', 'onchange']); + assert.verifySteps(['onchange', 'onchange']); form.destroy(); }); @@ -5370,6 +5369,11 @@ QUnit.module('fields', {}, function () { QUnit.test('nested x2many default values', async function (assert) { assert.expect(3); + this.data.partner.fields.turtles.default = [ + [0, 0, { partner_ids: [[6, 0, [4]]] }], + [0, 0, { partner_ids: [[6, 0, [1]]] }], + ]; + var form = await createView({ View: FormView, model: 'partner', @@ -5381,17 +5385,6 @@ QUnit.module('fields', {}, function () { '' + '
' + '', - mockRPC: function (route, args) { - if (args.model === 'partner' && args.method === 'default_get') { - return Promise.resolve({ - turtles: [ - [0, 0, { partner_ids: [[6, 0, [4]]] }], - [0, 0, { partner_ids: [[6, 0, [1]]] }], - ], - }); - } - return this._super.apply(this, arguments); - }, }); assert.containsN(form, '.o_list_view .o_data_row', 2, @@ -5978,7 +5971,7 @@ QUnit.module('fields', {}, function () { form.destroy(); }); - QUnit.test('one2many list editable, no onchange when required field is not set', async function (assert) { + QUnit.test('one2many list editable, onchange and required field', async function (assert) { assert.expect(8); this.data.turtle.fields.turtle_foo.required = true; @@ -6017,7 +6010,7 @@ QUnit.module('fields', {}, function () { assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0", "int_field should still be 0 (no onchange should have been done yet"); - assert.verifySteps(['read', 'default_get'], "no onchange should have been applied"); + assert.verifySteps(['read', 'onchange']); await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_foo"]'), "some text"); assert.verifySteps(['onchange']); @@ -6080,7 +6073,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0", "int_field should still be 0 (no onchange should have been done yet)"); - assert.verifySteps(['load_views', 'read', 'default_get'], "no onchange should have been applied"); + assert.verifySteps(['load_views', 'read', 'onchange']); // fill turtle_foo field await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_foo"]'), "some text"); @@ -6578,7 +6571,7 @@ QUnit.module('fields', {}, function () { // triggers an onchange on partner, because the new record is valid await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); - assert.verifySteps(['read', 'default_get', 'onchange']); + assert.verifySteps(['read', 'onchange', 'onchange']); form.destroy(); }); @@ -6670,7 +6663,7 @@ QUnit.module('fields', {}, function () { assert.containsN(form, 'tr.o_data_row', 40); - assert.verifySteps(['read', 'read', 'default_get', 'write', 'read', 'read']); + assert.verifySteps(['read', 'read', 'onchange', 'write', 'read', 'read']); form.destroy(); }); @@ -6765,7 +6758,7 @@ QUnit.module('fields', {}, function () { assert.containsN(form, 'tr.o_data_row', 40); - assert.verifySteps(['read', 'read', 'read', 'default_get', 'write', 'read', 'read']); + assert.verifySteps(['read', 'read', 'read', 'onchange', 'write', 'read', 'read']); form.destroy(); }); @@ -7058,7 +7051,7 @@ QUnit.module('fields', {}, function () { mockRPC: function (route, args) { assert.strictEqual(args.kwargs.context.flutter, 'shy', 'view context key should be used for every rpcs'); - if (args.method === 'default_get') { + if (args.method === 'onchange') { if (args.model === 'partner') { assert.strictEqual(args.kwargs.context.default_flutter, 'why', "should have default_* values in context for form view RPCs"); @@ -7506,7 +7499,7 @@ QUnit.module('fields', {}, function () { form.destroy(); }); - QUnit.test('one2many with several pages, onchange and default order', async function (assert) { + QUnit.skip('one2many with several pages, onchange and default order', async function (assert) { // This test reproduces a specific scenario where a one2many is displayed // over several pages, and has a default order such that a record that // would normally be on page 1 is actually on another page. Moreover, @@ -7575,6 +7568,14 @@ QUnit.module('fields', {}, function () { 'read [2,3]', // one2many (all fields in view, records of first page) 'read [2,4]', // many2many inside one2many (partner_ids), first page only 'onchange', + 'read [1]', // AAB FIXME 4 (draft fixing taskid-2323491): + // this test's purpose is to assert that this rpc isn't + // done, but yet it is. Actually, it wasn't before because mockOnChange + // returned [1] as command list, instead of [[6, false, [1]]], so basically + // this value was ignored. Now that mockOnChange properly works, the value + // is taken into account but the basicmodel doesn't care it concerns a + // record of the second page, and does the read. I don't think we + // introduced a regression here, this test was simply wrong... ]); form.destroy(); @@ -7689,7 +7690,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('onchange in a one2many with non inline view on a new record', async function (assert) { - assert.expect(8); + assert.expect(6); this.data.turtle.onchanges = { display_name: function (obj) { @@ -7725,9 +7726,7 @@ QUnit.module('fields', {}, function () { assert.verifySteps([ 'load_views', // load sub list - 'default_get', // main record 'onchange', // main record - 'default_get', // sub record 'onchange', // sub record 'onchange', // edition of display_name of sub record ]); @@ -8314,9 +8313,9 @@ QUnit.module('fields', {}, function () { assert.strictEqual(form.$('.o_data_row').text(), 'some foo value', "foo field should have correct value"); assert.verifySteps([ - 'default_get', // main record - 'default_get', // line 1 - 'default_get', // line 2 + 'onchange', // main record + 'onchange', // line 1 + 'onchange', // line 2 'create', 'read', // main record 'read', // line 1 @@ -8770,7 +8769,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('many2manys inside a one2many are fetched in batch after onchange', async function (assert) { - assert.expect(7); + assert.expect(6); this.data.partner.onchanges = { turtles: function (obj) { @@ -8815,7 +8814,7 @@ QUnit.module('fields', {}, function () { assert.strictEqual(form.$('.o_field_widget[name="partner_ids"]').text().replace(/\s/g, ''), "secondrecordsecondrecordaaa"); - assert.verifySteps(['default_get', 'onchange', 'read']); + assert.verifySteps(['onchange', 'read']); form.destroy(); }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index bbbf74f5b7b..56bb564880f 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -197,16 +197,12 @@ QUnit.module('relational_fields', { }); QUnit.test('do not call name_get if display_name already known', async function (assert) { - // default_get only returns the id for many2one fields - // onchange returns an array with the id and the display_name - // thus, when an onchange is performed, there is no need to call - // name_get as the display_name is alreay available - assert.expect(6); + assert.expect(4); this.data.partner.fields.product_id.default = 37; this.data.partner.onchanges = { trululu: function (obj) { - obj.trululu = [1, 'first record']; + obj.trululu = 1; }, }; @@ -223,11 +219,7 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'first record'); assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone'); - assert.verifySteps([ - 'default_get on partner', - 'onchange on partner', - 'name_get on product', - ]); + assert.verifySteps(['onchange on partner']); form.destroy(); }); @@ -530,9 +522,9 @@ QUnit.module('relational_fields', { assert.verifySteps([ 'read partner', 'read turtle', - 'default_get turtle', + 'onchange turtle', 'onchange partner', - 'default_get turtle', + 'onchange turtle', 'onchange partner', ]); form.destroy(); @@ -2784,7 +2776,7 @@ QUnit.module('relational_fields', { this.data.partner.fields.reference.default = 'product,37'; this.data.partner.onchanges = { int_field: function (obj) { - if (obj.int_field !== 0) { + if (obj.int_field) { obj.reference = 'partner_type,' + obj.int_field; } }, diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 2b64025c8fd..78c6c6864c9 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -203,6 +203,43 @@ var MockServer = Class.extend({ } } }, + /** + * Converts an Object representing a record to actual return Object of the + * python `onchange` method. + * Specifically, it applies `name_get` on many2one's and transforms raw id + * list in orm command lists for x2many's. + * For x2m fields that add or update records (ORM commands 0 and 1), it is + * recursive. + * + * @private + * @param {string} model: the model's name + * @param {Object} values: an object representing a record + * @returns {Object} + */ + _convertToOnChange(model, values) { + Object.entries(values).forEach(([fname, val]) => { + const field = this.data[model].fields[fname]; + if (field.type === 'many2one' && typeof val === 'number') { + // implicit name_get + const m2oRecord = this.data[field.relation].records.find(r => r.id === val); + values[fname] = [val, m2oRecord.display_name]; + } else if (field.type === 'one2many' || field.type === 'many2many') { + // TESTS ONLY + // one2many_ids = [1,2,3] is a simpler way to express it than orm commands + const isCommandList = val.length && Array.isArray(val[0]); + if (!isCommandList) { + values[fname] = [[6, false, val]]; + } else { + val.forEach(cmd => { + if (cmd[0] === 0 || cmd[0] === 1) { + cmd[2] = this._convertToOnChange(field.relation, cmd[2]); + } + }); + } + } + }); + return values; + }, /** * helper to evaluate a domain for given field values. * Currently, this is only a wrapper of the Domain.compute function in @@ -520,7 +557,7 @@ var MockServer = Class.extend({ * values * @returns {Object} */ - _mockDefaultGet: function (modelName, args, kwargs={}) { + _mockDefaultGet: function (modelName, args, kwargs = {}) { const fields = args[0]; const model = this.data[modelName]; const result = {}; @@ -1218,29 +1255,60 @@ var MockServer = Class.extend({ * * @private * @param {string} model - * @param {string|string[]} args a list of field names, or just a field name + * @param {Object} args + * @param {Object} args[1] the current record data + * @param {string|string[]} [args[2]] a list of field names, or just a field name + * @param {Object} args[3] the onchange spec + * @param {Object} [kwargs] * @returns {Object} */ - _mockOnchange: function (model, args) { + _mockOnchange: function (model, args, kwargs) { + const currentData = args[1]; + let fields = args[2]; + const onChangeSpec = args[3]; var onchanges = this.data[model].onchanges || {}; - var record = args[1]; - var fields = args[2]; - if (!(fields instanceof Array)) { + + if (fields && !(fields instanceof Array)) { fields = [fields]; } - var result = {}; - _.each(fields, function (field) { + const firstOnChange = !fields || !fields.length; + const onchangeVals = {}; + let defaultVals; + let nullValues; + if (firstOnChange) { + const fieldsFromView = Object.keys(onChangeSpec).reduce((acc, fname) => { + fname = fname.split('.', 1)[0]; + if (!acc.includes(fname)) { + acc.push(fname); + } + return acc; + }, []); + const defaultingFields = fieldsFromView.filter(fname => !(fname in currentData)); + defaultVals = this._mockDefaultGet(model, [defaultingFields], kwargs); + // It is the new semantics: no field in arguments means we are in + // a default_get + onchange situation + fields = fieldsFromView; + nullValues = {}; + fields.filter(fName => !Object.keys(defaultVals).includes(fName)).forEach(fName => { + nullValues[fName] = false; + }); + } + Object.assign(currentData, defaultVals); + fields.forEach(field => { if (field in onchanges) { - var changes = _.clone(record); + const changes = Object.assign({}, nullValues, currentData); onchanges[field](changes); - _.each(changes, function (value, key) { - if (record[key] !== value) { - result[key] = value; + Object.entries(changes).forEach(([key, value]) => { + if (currentData[key] !== value) { + onchangeVals[key] = value; } }); } }); - return {value: result}; + + return { + value: this._convertToOnChange(model, Object.assign({}, defaultVals, onchangeVals)), + }; }, /** * Simulate a 'read' operation. @@ -1771,9 +1839,6 @@ var MockServer = Class.extend({ case 'create': return Promise.resolve(this._mockCreate(args.model, args.args[0])); - case 'default_get': - return Promise.resolve(this._mockDefaultGet(args.model, args.args, args.kwargs)); - case 'fields_get': return Promise.resolve(this._mockFieldsGet(args.model, args.args)); @@ -1796,7 +1861,7 @@ var MockServer = Class.extend({ return Promise.resolve(this._mockNameSearch(args.model, args.args, args.kwargs)); case 'onchange': - return Promise.resolve(this._mockOnchange(args.model, args.args)); + return Promise.resolve(this._mockOnchange(args.model, args.args, args.kwargs)); case 'read': return Promise.resolve(this._mockRead(args.model, args.args, args.kwargs)); diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index abd91ca17cf..909cdfaa905 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -81,7 +81,9 @@ odoo.define('web.basic_model_tests', function (require) { QUnit.module('BasicModel'); QUnit.test('can process x2many commands', async function (assert) { - assert.expect(5); + assert.expect(6); + + this.data.partner.fields.product_ids.default = [[0, 0, { category: [] }]]; const form = await createView({ View: FormView, @@ -111,22 +113,16 @@ odoo.define('web.basic_model_tests', function (require) { }, mockRPC(route, args) { assert.step(args.method); - if (args.method === 'default_get' && args.model === 'partner') { - return Promise.resolve({ - product_ids: [ - [0, 0, {category: []}], - ] - }); - } return this._super.apply(this, arguments); }, }); assert.verifySteps([ 'load_views', - 'default_get', + 'onchange', ]); assert.containsOnce(form, '.o_field_x2many_list', 'should have rendered a x2many list'); + assert.containsOnce(form, '.o_data_row', 'should have added 1 record as default'); assert.containsOnce(form, '.o_field_x2many_list_row_add', 'should have rendered a x2many add row on list'); form.destroy(); }); @@ -691,7 +687,7 @@ odoo.define('web.basic_model_tests', function (require) { model.destroy(); }); - QUnit.test('can make a default_record, no onchange', async function (assert) { + QUnit.test('can make a default_record with the help of onchange', async function (assert) { assert.expect(5); this.params.context = {}; @@ -714,8 +710,7 @@ odoo.define('web.basic_model_tests', function (require) { assert.deepEqual(record.data.product_ids.data, [], "o2m default should be []"); assert.deepEqual(record.data.category.data, [], "m2m default should be []"); - assert.verifySteps(['default_get'], - "there should be default_get"); + assert.verifySteps(['onchange']); model.destroy(); }); @@ -756,7 +751,7 @@ odoo.define('web.basic_model_tests', function (require) { }); QUnit.test('can make a default_record with default relational values', async function (assert) { - assert.expect(7); + assert.expect(6); this.data.partner.fields.product_id.default = 37; this.data.partner.fields.product_ids.default = [ @@ -805,8 +800,7 @@ odoo.define('web.basic_model_tests', function (require) { assert.deepEqual(record.data.category.res_ids, [12, 14], "m2m default should be [12, 14]"); - assert.verifySteps(['default_get', 'name_get'], - "there should be default_get and name_get"); + assert.verifySteps(['onchange']); model.destroy(); }); @@ -872,7 +866,7 @@ odoo.define('web.basic_model_tests', function (require) { "should have fetched correct name"); assert.strictEqual(record.data.other_product_id.data.display_name, "xpad", "should have fetched correct name"); - assert.strictEqual(rpcCount, 2, "should have done 2 rpcs: default_get and 1 name_get"); + assert.strictEqual(rpcCount, 1, "should have done 1 rpc: onchange"); model.destroy(); }); @@ -1402,7 +1396,7 @@ odoo.define('web.basic_model_tests', function (require) { }); QUnit.test('default_get: fetch many2one with default (empty & not) inside x2manys', async function (assert) { - assert.expect(4); + assert.expect(3); this.data.partner.fields.category_m2o = { type: 'many2one', @@ -1419,12 +1413,6 @@ odoo.define('web.basic_model_tests', function (require) { var model = await createModel({ Model: BasicModel, data: this.data, - mockRPC: function (route, args) { - if (args.method === 'name_get' && args.model === 'partner_type') { - assert.deepEqual(args.args, [[12]], "should name_get on category 12"); - } - return this._super(route, args); - }, }); var params = { @@ -2199,8 +2187,7 @@ odoo.define('web.basic_model_tests', function (require) { var recordID = await model.load(this.params); recordID = await model.reload(recordID); - assert.verifySteps(['default_get', 'default_get'], - "two default_get RPCs should have been done"); + assert.verifySteps(['onchange', 'onchange']); var record = model.get(recordID); assert.strictEqual(record.data.product_id, false, "m2o default value should be false"); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 8bbdae1e91f..39d7c35aa01 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -86,6 +86,7 @@ QUnit.module('Views', { }, product: { fields: { + display_name: {string: "Product Name", type: "char"}, name: {string: "Product Name", type: "char"}, partner_type_id: {string: "Partner type", type: "many2one", relation: "partner_type"}, }, @@ -1539,7 +1540,7 @@ QUnit.module('Views', { assert.strictEqual(form.$('input[name="qux"]').val(), "1.0", "qux input is properly formatted"); - assert.verifySteps(['default_get', 'create', 'read', 'write', 'read']); + assert.verifySteps(['onchange', 'create', 'read', 'write', 'read']); form.destroy(); }); @@ -1797,7 +1798,7 @@ QUnit.module('Views', { }); await testUtils.dom.click('.o_form_statusbar button.p', form); - assert.verifySteps(['default_get', 'create', 'read', 'execute_action', 'read']); + assert.verifySteps(['onchange', 'create', 'read', 'execute_action', 'read']); form.destroy(); }); @@ -1847,7 +1848,7 @@ QUnit.module('Views', { }); await testUtils.dom.click('.o_form_statusbar button.p', form); - assert.verifySteps(['default_get', 'create', 'read', 'execute_action', 'read']); + assert.verifySteps(['onchange', 'create', 'read', 'execute_action', 'read']); form.destroy(); }); @@ -2063,6 +2064,7 @@ QUnit.module('Views', { assert.expect(1); this.data.partner.onchanges.foo = function (obj) {}; + let checkOnchange = false; var form = await createView({ View: FormView, model: 'partner', @@ -2079,7 +2081,7 @@ QUnit.module('Views', { '', res_id: 1, mockRPC: function (route, args) { - if (args.method === "onchange") { + if (args.method === "onchange" && checkOnchange) { assert.deepEqual(args.args[1], { display_name: "first record", foo: "tralala", @@ -2103,6 +2105,7 @@ QUnit.module('Views', { await testUtils.fields.editInput(form.$('.o_field_one2many input[name=qux]'), '12.4'); // trigger an onchange by modifying foo + checkOnchange = true; await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala'); form.destroy(); @@ -2176,7 +2179,7 @@ QUnit.module('Views', { }); QUnit.test('default record with a one2many and an onchange on sub field', async function (assert) { - assert.expect(4); + assert.expect(3); this.data.partner.onchanges.foo = function () {}; @@ -2202,7 +2205,7 @@ QUnit.module('Views', { return this._super.apply(this, arguments); }, }); - assert.verifySteps(['default_get', 'onchange']); + assert.verifySteps(['onchange']); form.destroy(); }); @@ -2340,19 +2343,18 @@ QUnit.module('Views', { "should have new foo2 value in one2many"); assert.containsOnce(form, 'td:contains(xphone)', "should have a cell with the name field 'product_id', set to xphone"); - assert.strictEqual(nameGetCount, 1, "should have done only 1 nameget"); + assert.strictEqual(nameGetCount, 0, "should have done no nameget"); form.destroy(); }); QUnit.test('make default record with non empty many2one', async function (assert) { - var done = assert.async(); assert.expect(2); this.data.partner.fields.trululu.default = 4; var nameGetCount = 0; - createView({ + const form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2360,20 +2362,16 @@ QUnit.module('Views', { mockRPC: function (route, args) { if (args.method === 'name_get') { nameGetCount++; - var result = this._super.apply(this, arguments); - return concurrency.delay(1).then(function () { - return result; - }); } return this._super.apply(this, arguments); }, - }).then(function (form) { - assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'aaa', - "default value should be correctly displayed"); - assert.strictEqual(nameGetCount, 1, "should have done one name_get"); - form.destroy(); - done(); }); + + assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'aaa', + "default value should be correctly displayed"); + assert.strictEqual(nameGetCount, 0, "should have done no name_get"); + + form.destroy(); }); QUnit.test('form view properly change its title', async function (assert) { @@ -3720,44 +3718,6 @@ QUnit.module('Views', { form.destroy(); }); - QUnit.test('default_get, onchange which fails, should still work', async function (assert) { - assert.expect(1); - - this.data.partner.onchanges.foo = true; - - var form = await createView({ - View: FormView, - model: 'partner', - data: this.data, - arch: '
' + - '' + - '', - mockRPC: function (route, args) { - if (args.method === 'onchange') { - // we simulate a validation error. In the 'real' web client, - // the server error will be used by the session to display - // an error dialog. From the point of view of the basic - // model, the promise is just rejected. - return Promise.reject(); - } - return this._super.apply(this, arguments); - }, - }); - - // this test checks that a form view is still rendered when the server - // onchange fails (for example, because of a validation error, or, more - // likely, a bug in the onchange code). This is quite rare, but if the - // onchange fails, we still want to display the form view (with the error - // dialog from the session/crashmanager). Otherwise, we could be in the - // situation where a user clicks on a button, it should open a wizard, - // but something fails and the wizard is not even rendered. In that - // case, there is nothing that the user could do. - assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), 'My little Foo Value', - "should display proper default value"); - - form.destroy(); - }); - QUnit.test('button box is rendered in create mode', async function (assert) { assert.expect(3); @@ -3919,7 +3879,7 @@ QUnit.module('Views', { [5], [1, 4, { display_name: "gold", - timmy: [5] + timmy: [[5]], }], ]; }, @@ -5079,6 +5039,9 @@ QUnit.module('Views', { assert.expect(1); this.data.partner.records[0].product_ids = [37]; + this.data.partner.fields.product_ids.default = [ + [0, 0, { name: 'xdroid', partner_type_id: 12 }] + ]; var form = await createView({ View: FormView, @@ -5096,14 +5059,6 @@ QUnit.module('Views', { '' + '', mockRPC: function (route, args) { - if (args.method === 'default_get') { - return Promise.resolve({ - product_ids: [[0, 0, { - name: 'xdroid', - partner_type_id: 12, - }]] - }); - } if (args.method === 'create') { var command = args.args[0].product_ids[0]; assert.strictEqual(command[2].partner_type_id, 12, @@ -5468,6 +5423,7 @@ QUnit.module('Views', { QUnit.test('check if id and active_id are defined', async function (assert) { assert.expect(2); + let checkOnchange = false; var form = await createView({ View: FormView, model: 'partner', @@ -5485,7 +5441,7 @@ QUnit.module('Views', { "partner,false,form": '
' }, mockRPC: function (route, args) { - if (args.method === 'default_get' && args.args[0][0] === 'trululu') { + if (args.method === 'onchange' && checkOnchange) { assert.strictEqual(args.kwargs.context.current_id, false, "current_id should be false"); assert.strictEqual(args.kwargs.context.default_trululu, false, @@ -5495,6 +5451,7 @@ QUnit.module('Views', { }, }); + checkOnchange = true; await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); form.destroy(); }); @@ -5784,63 +5741,61 @@ QUnit.module('Views', { form.destroy(); }); - QUnit.module('focus and scroll test', async function () { - QUnit.test('no focus set on form when closing many2one modal if lastActivatedFieldIndex is not set', async function (assert) { - assert.expect(8); + QUnit.test('no focus set on form when closing many2one modal if lastActivatedFieldIndex is not set', async function (assert) { + assert.expect(8); - var form = await createView({ - View: FormView, - model: 'partner', - data: this.data, - arch: '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '', - res_id: 2, - archs: { - 'partner,false,list': '', - 'partner_type,false,list': '', - 'partner,false,form': '
', - 'product,false,list': '', - }, - mockRPC: function (route, args) { - if (args.method === 'get_formview_id') { - return Promise.resolve(false); - } - return this._super(route, args); - }, - }); - - // set max-height to have scroll forcefully so that we can test scroll position after modal close - $('.o_content').css({'overflow': 'auto', 'max-height': '300px'}); - // Open many2one modal, lastActivatedFieldIndex will not set as we directly click on external button - await testUtils.form.clickEdit(form); - assert.strictEqual($(".o_content").scrollTop(), 0, "scroll position should be 0"); - - form.$(".o_field_many2one[name='trululu'] .o_input").focus(); - assert.notStrictEqual($(".o_content").scrollTop(), 0, "scroll position should not be 0"); - - await testUtils.dom.click(form.$('.o_external_button')); - // Close modal - await testUtils.dom.click($('.modal').last().find('button[class="close"]')); - assert.notStrictEqual($(".o_content").scrollTop(), 0, - "scroll position should not be 0 after closing modal"); - assert.containsNone(document.body, '.modal', 'There should be no modal'); - assert.doesNotHaveClass($('body'), 'modal-open', 'Modal is not said opened'); - assert.strictEqual(form.renderer.lastActivatedFieldIndex, -1, - "lastActivatedFieldIndex is -1"); - assert.equal(document.activeElement, $('body')[0], - 'body is focused, should not set focus on form widget'); - assert.notStrictEqual(document.activeElement, form.$('.o_field_many2one[name="trululu"] .o_input'), - 'field widget should not be focused when lastActivatedFieldIndex is -1'); - - form.destroy(); + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 2, + archs: { + 'partner,false,list': '', + 'partner_type,false,list': '', + 'partner,false,form': '
', + 'product,false,list': '', + }, + mockRPC: function (route, args) { + if (args.method === 'get_formview_id') { + return Promise.resolve(false); + } + return this._super(route, args); + }, }); + + // set max-height to have scroll forcefully so that we can test scroll position after modal close + $('.o_content').css({'overflow': 'auto', 'max-height': '300px'}); + // Open many2one modal, lastActivatedFieldIndex will not set as we directly click on external button + await testUtils.form.clickEdit(form); + assert.strictEqual($(".o_content").scrollTop(), 0, "scroll position should be 0"); + + form.$(".o_field_many2one[name='trululu'] .o_input").focus(); + assert.notStrictEqual($(".o_content").scrollTop(), 0, "scroll position should not be 0"); + + await testUtils.dom.click(form.$('.o_external_button')); + // Close modal + await testUtils.dom.click($('.modal').last().find('button[class="close"]')); + assert.notStrictEqual($(".o_content").scrollTop(), 0, + "scroll position should not be 0 after closing modal"); + assert.containsNone(document.body, '.modal', 'There should be no modal'); + assert.doesNotHaveClass($('body'), 'modal-open', 'Modal is not said opened'); + assert.strictEqual(form.renderer.lastActivatedFieldIndex, -1, + "lastActivatedFieldIndex is -1"); + assert.equal(document.activeElement, $('body')[0], + 'body is focused, should not set focus on form widget'); + assert.notStrictEqual(document.activeElement, form.$('.o_field_many2one[name="trululu"] .o_input'), + 'field widget should not be focused when lastActivatedFieldIndex is -1'); + + form.destroy(); }); QUnit.test('in create mode, autofocus fields are focused', async function (assert) { @@ -6262,6 +6217,9 @@ QUnit.module('Views', { }); QUnit.test('do not perform extra RPC to read invisible many2one fields', async function (assert) { + // This test isn't really meaningful anymore, since default_get and (first) onchange rpcs + // have been merged in a single onchange rpc, returning nameget for many2one fields. But it + // isn't really costly, and it still checks rpcs done when creating a new record with a m2o. assert.expect(2); this.data.partner.fields.trululu.default = 2; @@ -6281,7 +6239,7 @@ QUnit.module('Views', { }, }); - assert.verifySteps(['default_get'], "only one RPC should have been done"); + assert.verifySteps(['onchange'], "only one RPC should have been done"); form.destroy(); }); @@ -7067,7 +7025,7 @@ QUnit.module('Views', { assert.ok(!form.$('.o_statusbar_buttons button').prop('disabled'), 'button should no longer be disabled'); - assert.verifySteps(['default_get']); + assert.verifySteps(['onchange']); // click on button, and click on ok in confirm dialog await testUtils.dom.click(form.$('.o_statusbar_buttons button')); @@ -8512,7 +8470,7 @@ QUnit.module('Views', { assert.containsOnce(form, '.o_form_readonly', "form view should be in readonly"); assert.strictEqual(form.$('.o_form_view').text().trim(), 'some foo value', "foo field should have correct value"); - assert.verifySteps(['default_get', 'create', 'read']); + assert.verifySteps(['onchange', 'create', 'read']); form.destroy(); }); @@ -9284,7 +9242,7 @@ QUnit.module('Views', { form.destroy(); }); QUnit.test('if the focus is on the save button, hitting ESCAPE should discard', async function (assert) { - assert.expect(1); + assert.expect(0); var form = await createView({ View: FormView, @@ -9297,8 +9255,8 @@ QUnit.module('Views', { mode: 'edit', }, mockRPC: function (route, args) { - if (args.method === 'default_get') { - assert.ok(true, "should call the /create route"); + if (args.method === 'create') { + throw new Error('Create should not be called'); } return this._super(route, args); }, @@ -9319,7 +9277,7 @@ QUnit.module('Views', { this.data.partner.onchanges = { p: function (obj) { onchangeNum++; - obj.foo = obj.p.length.toString(); + obj.foo = obj.p ? obj.p.length.toString() : "0"; }, }; @@ -9377,7 +9335,7 @@ QUnit.module('Views', { }); QUnit.test('if the focus is on the discard button, hitting ESCAPE should discard', async function (assert) { - assert.expect(1); + assert.expect(0); var form = await createView({ View: FormView, @@ -9390,8 +9348,8 @@ QUnit.module('Views', { mode: 'edit', }, mockRPC: function (route, args) { - if (args.method === 'default_get') { - assert.ok(true, "should call the /create route"); + if (args.method === 'create') { + throw new Error('Create should not be called'); } return this._super(route, args); }, @@ -9438,7 +9396,7 @@ QUnit.module('Views', { await testUtils.form.clickSave(form); assert.verifySteps([ - 'default_get', + 'onchange', 'create', 'reload company', 'read', diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 81f3f90f383..8c3f1b90029 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -551,10 +551,10 @@ QUnit.module('Views', { 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) - 'default_get', // quick create + 'onchange', // quick create 'name_create', // should perform a name_create to create the record 'read', // read the created record - 'default_get', // reopen the quick create automatically + 'onchange', // reopen the quick create automatically ]); kanban.destroy(); @@ -625,11 +625,11 @@ QUnit.module('Views', { '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'load_views', // form view in quick create - 'default_get', // quick create + 'onchange', // quick create 'create', // should perform a create to create the record 'read', // read the created record 'load_views', // form view in quick create (is actually in cache) - 'default_get', // reopen the quick create automatically + 'onchange', // reopen the quick create automatically ]); kanban.destroy(); @@ -682,10 +682,10 @@ QUnit.module('Views', { 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) - 'default_get', // quick create + 'onchange', // quick create 'name_create', // should perform a name_create to create the record 'read', // read the created record - 'default_get', // reopen the quick create automatically + 'onchange', // reopen the quick create automatically ]); kanban.destroy(); @@ -750,18 +750,18 @@ QUnit.module('Views', { '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'load_views', // form view in quick create - 'default_get', // quick create + 'onchange', // quick create 'create', // should perform a create to create the record 'read', // read the created record 'load_views', // form view in quick create (is actually in cache) - 'default_get', // reopen the quick create automatically + 'onchange', // reopen the quick create automatically ]); kanban.destroy(); }); QUnit.test('quick create record with default values and onchanges', async function (assert) { - assert.expect(11); + assert.expect(10); this.data.partner.fields.int_field.default = 4; this.data.partner.onchanges = { @@ -814,8 +814,7 @@ QUnit.module('Views', { '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'load_views', // form view in quick create - 'default_get', // quick create - 'onchange', // default_get's onchange + 'onchange', // quick create 'onchange', // onchange due to 'foo' field change ]); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index de8c0dbed23..28fb058d96d 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -864,7 +864,7 @@ QUnit.module('Views', { }); await testUtils.dom.click(list.$buttons.find('.o_list_button_add')); - assert.verifySteps(['search_read', 'default_get'], "no nameget should be done"); + assert.verifySteps(['search_read', 'onchange'], "no nameget should be done"); list.destroy(); }); @@ -5565,7 +5565,7 @@ QUnit.module('Views', { await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input[name="foo"]'), 'enter'); assert.containsN(list, 'tr.o_data_row', 5, "should have created a 5th row"); - assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/default_get']); + assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/onchange']); list.destroy(); }); @@ -5604,14 +5604,14 @@ QUnit.module('Views', { assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/write', '/web/dataset/call_kw/foo/read', - '/web/dataset/call_kw/foo/default_get']); + '/web/dataset/call_kw/foo/onchange']); list.destroy(); }); QUnit.test('navigation with tab and read completes after default_get', async function (assert) { assert.expect(8); - var defaultGetPromise = testUtils.makeTestPromise(); + var onchangeGetPromise = testUtils.makeTestPromise(); var readPromise = testUtils.makeTestPromise(); var list = await createView({ @@ -5629,8 +5629,8 @@ QUnit.module('Views', { return result; }); } - if (args.method === 'default_get') { - return defaultGetPromise.then(function () { + if (args.method === 'onchange') { + return onchangeGetPromise.then(function () { return result; }); } @@ -5643,7 +5643,7 @@ QUnit.module('Views', { await testUtils.fields.editInput(list.$('tr.o_selected_row input[name="int_field"]'), '1234'); await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input[name="int_field"]'), 'tab'); - defaultGetPromise.resolve(); + onchangeGetPromise.resolve(); assert.containsN(list, 'tbody tr.o_data_row', 4, "should have 4 data rows"); @@ -5661,7 +5661,7 @@ QUnit.module('Views', { assert.hasClass(list.$('tr.o_data_row:eq(4)'),'o_selected_row', "5th row should be selected"); - assert.verifySteps(['write', 'read', 'default_get']); + assert.verifySteps(['write', 'read', 'onchange']); list.destroy(); }); @@ -7215,7 +7215,7 @@ QUnit.module('Views', { // create a record and edit its value await testUtils.dom.click($('.o_list_button_add')); - assert.verifySteps(['default_get']); + assert.verifySteps(['onchange']); await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=int_field]'), 123); assert.containsNone(document.body, '.modal', "the multi edition should not be triggered during creation"); @@ -9176,10 +9176,10 @@ QUnit.module('Views', { '/web/dataset/search_read', 'write', 'read', - 'default_get', + 'onchange', 'create', 'read', - 'default_get', + 'onchange', ]); list.destroy(); @@ -9255,7 +9255,7 @@ QUnit.module('Views', { 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', - 'default_get', + 'onchange', ]); list.destroy(); @@ -9814,7 +9814,7 @@ QUnit.module('Views', { '', groupBy: ['priority'], mockRPC: function (route, args) { - if (args.method === 'default_get') { + if (args.method === 'onchange') { assert.step(args.kwargs.context.default_priority.toString()); } return this._super.apply(this, arguments); @@ -9851,7 +9851,7 @@ QUnit.module('Views', { '', groupBy: ['m2o'], mockRPC: function (route, args) { - if (args.method === 'default_get') { + if (args.method === 'onchange') { assert.step(args.kwargs.context.default_m2o.toString()); } return this._super.apply(this, arguments); diff --git a/odoo/addons/base/static/tests/base_settings_tests.js b/odoo/addons/base/static/tests/base_settings_tests.js index ec08e7ee17f..2c3439d8082 100644 --- a/odoo/addons/base/static/tests/base_settings_tests.js +++ b/odoo/addons/base/static/tests/base_settings_tests.js @@ -132,12 +132,12 @@ QUnit.module('base_settings_tests', { assert.hasClass(actionManager.$('.o_form_view'), 'o_form_editable'); assert.verifySteps([ 'load_views', // initial setting action - 'default_get', // this is a setting view => create new record + 'onchange', // this is a setting view => create new record 'create', // when we click on action button => save 'read', // with save, we have a reload... (not necessary actually) 'load_views', // for other action in breadcrumb, // with a searchread (not shown here since it is a route) - 'default_get', // when we come back, we want to restart from scratch + 'onchange', // when we come back, we want to restart from scratch ]); actionManager.destroy();