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 6eef41f149d..e2cc60f7fc4 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -189,6 +189,89 @@ var BasicModel = AbstractModel.extend({ return id; }); }, + /** + * 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 {Deferred} + */ + 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 || {}; + + // 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]; + if (!field) { + continue; // ignore values for unknown fields + } + 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 $.when.apply($, 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. @@ -1397,6 +1480,7 @@ var BasicModel = AbstractModel.extend({ list = self._makeDataPoint({ fields: view ? view.fields : fieldInfo.relatedFields, fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo, + limit: fieldInfo.limit, modelName: field.relation, parentID: record.id, static: true, @@ -3227,27 +3311,8 @@ var BasicModel = AbstractModel.extend({ context: params.context, }) .then(function (result) { - // fill default values for missing fields - for (var i = 0; i < fieldNames.length; i++) { - var fieldName = fieldNames[i]; - if (!(fieldName in result)) { - var field = params.fields[fieldName]; - if (field.type === 'float' || - field.type === 'integer' || - field.type === 'monetary') { - result[fieldName] = 0; - } else if (field.type === 'one2many' || field.type === 'many2many') { - result[fieldName] = []; - } else { - result[fieldName] = null; - } - } - } - - var data = {}; var record = self._makeDataPoint({ modelName: modelName, - data: data, fields: params.fields, fieldsInfo: params.fieldsInfo, context: params.context, @@ -3256,37 +3321,7 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }); - var defs = []; - _.each(fieldNames, function (name) { - var field = params.fields[name]; - data[name] = null; - record._changes = record._changes || {}; - var dp; - if (field.type === 'many2one' && result[name]) { - dp = self._makeDataPoint({ - context: record.context, - data: {id: result[name]}, - modelName: field.relation, - parentID: record.id, - }); - record._changes[name] = dp.id; - } else if (field.type === 'reference' && result[name]) { - var ref = result[name].split(','); - dp = self._makeDataPoint({ - context: record.context, - data: {id: parseInt(ref[1])}, - modelName: ref[0], - parentID: record.id, - }); - defs.push(self._fetchNameGet(dp)); - record._changes[name] = dp.id; - } else if (field.type === 'one2many' || field.type === 'many2many') { - defs.push(self._processX2ManyCommands(record, name, result[name])); - } else { - record._changes[name] = self._parseServerValue(field, result[name]); - } - }); - return $.when.apply($, defs) + return self.applyDefaultValues(record.id, result) .then(function () { var def = $.Deferred(); self._performOnChange(record, fields_key).always(function () { @@ -3469,13 +3504,17 @@ var BasicModel = AbstractModel.extend({ * @param {Object} record * @param {string} fieldName * @param {Array[Array]} commands + * @param {Object} [options] + * @param {string} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record * @returns {Deferred} */ - _processX2ManyCommands: function (record, fieldName, commands) { + _processX2ManyCommands: function (record, fieldName, commands, options) { var self = this; + options = options || {}; var defs = []; var field = record.fields[fieldName]; - var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo[options.viewType || record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; var fields = view ? view.fields : fieldInfo.relatedFields; 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 8a91e2b90c7..c76020cacac 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -120,7 +120,12 @@ var BasicView = AbstractView.extend({ // (because those fields were unknow at that time). So we ask // the model to process them. def = this.model.applyRawChanges(record.id, viewType).then(function () { - if (!self.model.isNew(record.id)) { + if (self.model.isNew(record.id)) { + return self.model.applyDefaultValues(record.id, {}, { + fieldNames: fieldNames, + viewType: viewType, + }); + } else { return self.model.reload(record.id, { fieldNames: fieldNames, keepChanges: true, diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 4f7041ad2d5..89764ce319b 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3063,6 +3063,108 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchange on one2many containing x2many in form view', function (assert) { + assert.expect(16); + + this.data.partner.onchanges = { + foo: function (obj) { + obj.turtles = [[0, false, {turtle_foo: 'new record'}]]; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + archs: { + 'partner,false,list': '', + 'partner,false,search': '', + }, + }); + + assert.strictEqual(form.$('.o_data_row').length, 1, + "the onchange should have created one record in the relation"); + + // open the created o2m record in a form view, and add a m2m subrecord + // in its relation + form.$('.o_data_row').click(); + + assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); + assert.strictEqual($('.modal .o_data_row').length, 0, + "there should be no record in the one2many in the dialog"); + + // add a many2many subrecord + $('.modal .o_field_x2many_list_row_add a').click(); + + assert.strictEqual($('.modal').length, 2, + "should have opened a second dialog"); + + // select a many2many subrecord + $('.modal:nth(1) .o_list_view .o_data_cell:first').click(); + + assert.strictEqual($('.modal').length, 1, + "second dialog should be closed"); + assert.strictEqual($('.modal .o_data_row').length, 1, + "there should be one record in the one2many in the dialog"); + assert.notOk($('.modal .o_x2m_control_panel .o_cp_pager div').is(':visible'), + 'm2m pager should be hidden'); + + // click on 'Save & Close' + $('.modal .modal-footer .btn-primary:first').click(); + + assert.strictEqual($('.modal').length, 0, "dialog should be closed"); + + // reopen o2m record, and another m2m subrecord in its relation, but + // discard the changes + form.$('.o_data_row').click(); + + assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); + assert.strictEqual($('.modal .o_data_row').length, 1, + "there should be one record in the one2many in the dialog"); + + // add another m2m subrecord + $('.modal .o_field_x2many_list_row_add a').click(); + + assert.strictEqual($('.modal').length, 2, + "should have opened a second dialog"); + + $('.modal:nth(1) .o_list_view .o_data_cell:first').click(); + + assert.strictEqual($('.modal').length, 1, + "second dialog should be closed"); + assert.strictEqual($('.modal .o_data_row').length, 2, + "there should be two records in the one2many in the dialog"); + + // click on 'Discard' + $('.modal .modal-footer .btn-default').click(); + + assert.strictEqual($('.modal').length, 0, "dialog should be closed"); + + // reopen o2m record to check that second changes have properly been discarded + form.$('.o_data_row').click(); + + assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); + assert.strictEqual($('.modal .o_data_row').length, 1, + "there should be one record in the one2many in the dialog"); + + form.destroy(); + }); + QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) { var done = assert.async(); assert.expect(20);