diff options
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_model.js | 453 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_view.js | 2 |
2 files changed, 176 insertions, 279 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 @@ -282,90 +282,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. * in x2manys, we only know the fields that are used in the inner view, but @@ -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; }); }, @@ -726,6 +642,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. * * @param {string} id the localID for a valid record element @@ -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 @@ -2382,27 +2363,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. * * @param {Object} dataPoint @@ -2737,50 +2697,6 @@ var BasicModel = AbstractModel.extend({ }); }, /** - * 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 * data that the rendering of the record won't need if it was not using @@ -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<string>} 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, - }); - - // 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 = self._computeOverrideDefaultFields( - params.parentID, - params.position - ); + var record = this._makeDataPoint({ + modelName: modelName, + fields: fields, + fieldsInfo: fieldsInfo, + context: params.context, + parentID: params.parentID, + res_ids: params.res_ids, + viewType: targetView, + }); - if (overrideDefaultFields) { - result[overrideDefaultFields.field] = overrideDefaultFields.value; - } + 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(); + } - 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}); + // 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; + } - 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, }); |
