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 dc0beb96942..0642fd6842c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -589,28 +589,7 @@ var BasicModel = AbstractModel.extend({ } var shouldReload = 'reload' in options ? options.reload : true; var method = self.isNew(record_id) ? 'create' : 'write'; - var changes = _.extend({}, record._changes); - var commands = self._generateX2ManyCommands(record); - for (var fieldName in commands) { - if (commands[fieldName] === null) { - delete changes[fieldName]; - } else { - changes[fieldName] = commands[fieldName]; - } - } - - // replace local ids by actual ids in relational changes - for (fieldName in changes) { - var field = record.fields[fieldName]; - if (field.type === 'many2one') { - if (changes[fieldName]) { - var res_id = self.localData[changes[fieldName]].res_id; - changes[fieldName] = res_id; - } else { - changes[fieldName] = false; - } - } - } + var changes = self._generateChanges(record); if (method === 'create') { var fieldNames = record.getFieldNames(); @@ -864,7 +843,7 @@ var BasicModel = AbstractModel.extend({ var context = this._getContext(record); var rawData = this.get(record.id, {raw: true}).data; - var commands = this._generateX2ManyCommands(record); + var commands = this._generateX2ManyCommands(record, false); // compute field values var fieldNames = record.getFieldNames(); @@ -1622,41 +1601,65 @@ var BasicModel = AbstractModel.extend({ } return $.when.apply($, defs); }, + /** + * Generates an object mapping field names to their changed value in a given + * record (i.e. maps to the new value for basic fields, to the res_id for + * many2ones and to commands for x2manys). + * + * @private + * @param {Object} record + * @returns {Object} a map from changed fields to their new value + */ + _generateChanges: function (record) { + var changes = _.extend({}, record._changes); + var commands = this._generateX2ManyCommands(record, true); + for (var fieldName in record.fields) { + var type = record.fields[fieldName].type; + if (type === 'one2many' || type === 'many2many') { + if (commands[fieldName].length) { // replace localId by commands + changes[fieldName] = commands[fieldName]; + } else { // no command -> no change for that field + delete changes[fieldName]; + } + } else if (type === 'many2one' && fieldName in changes) { + var value = changes[fieldName]; + changes[fieldName] = value ? this.localData[value].res_id : false; + } + } + return changes; + }, /** * Read all x2many fields and generate the commands for the server to create * or write them... * * @param {Object} record + * @param {boolean} [changesOnly=false] if true, only generates commands for + * fields that have changed * @returns {Object} a map from some field names to commands */ - _generateX2ManyCommands: function (record) { + _generateX2ManyCommands: function (record, changesOnly) { var self = this; var commands = {}; - var type, list, relData, relIds, removedIds, addedIds, keptIds, relRecord, i; - var data = _.extend({}, record.data, record._changes); - + var type; for (var fieldName in record.fields) { type = record.fields[fieldName].type; if (type === 'many2many' || type === 'one2many') { - // skip if this field is empty + commands[fieldName] = []; if (!data[fieldName]) { - commands[fieldName] = null; + // skip if this field is empty continue; } - list = this.localData[data[fieldName]]; - if (!list._changes) { - commands[fieldName] = null; - // skip if this field hasn't changed + var list = this.localData[data[fieldName]]; + if (changesOnly && !list._changes) { + // if only changes are requested, skip if there is no change continue; } - relData = _.map(list._changes, function (localId) { + var relData = _.map(list._changes || list.data, function (localId) { return self.localData[localId]; }); - relIds = _.pluck(relData, 'res_id'); - commands[fieldName] = []; - + var relIds = _.pluck(relData, 'res_id'); if (type === 'many2many' || list._forceM2MLink) { // deliberately generate a single 'replace' command instead // of a 'delete' and a 'link' commands with the exact diff @@ -1665,20 +1668,17 @@ var BasicModel = AbstractModel.extend({ // an complete override of the actual value of the m2m) commands[fieldName].push(x2ManyCommands.replace_with(relIds)); } else if (type === 'one2many') { - removedIds = _.difference(list.res_ids, relIds); - addedIds = _.difference(relIds, list.res_ids); - keptIds = _.intersection(list.res_ids, relIds); - - // the didChange variable keep track of the fact that at + var removedIds = _.difference(list.res_ids, relIds); + var addedIds = _.difference(relIds, list.res_ids); + var keptIds = _.intersection(list.res_ids, relIds); + // the didChange variable keeps track of the fact that at // least one id was updated var didChange = false; - + var command, i, relRecord; for (i = 0; i < keptIds.length; i++) { relRecord = _.findWhere(relData, {res_id: keptIds[i]}); - var command; if (!_.isEmpty(relRecord._changes)) { - var r = this.get(relRecord.id, {raw: true}); - var changes = _.pick(r.data, Object.keys(relRecord._changes)); + var changes = this._generateChanges(relRecord); command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; } else { @@ -1686,11 +1686,11 @@ var BasicModel = AbstractModel.extend({ } commands[fieldName].push(command); } - if (!didChange && addedIds.length === 0 && removedIds.length === 0) { + if (changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) { // in this situation, we have no changed ids, no added // ids and no removed ids, so we can safely ignore the // last changes - commands[fieldName] = null; + commands[fieldName] = []; } for (i = 0; i < addedIds.length; i++) { relRecord = _.findWhere(relData, {res_id: addedIds[i]}); @@ -1703,7 +1703,6 @@ var BasicModel = AbstractModel.extend({ } } } - return commands; }, /** diff --git a/addons/web/static/test/fields/relational_fields_tests.js b/addons/web/static/test/fields/relational_fields_tests.js index fff3cb20e5b..7b497ca943b 100644 --- a/addons/web/static/test/fields/relational_fields_tests.js +++ b/addons/web/static/test/fields/relational_fields_tests.js @@ -2324,7 +2324,7 @@ QUnit.module('relational_fields', { }); QUnit.test('many2one and many2many in one2many', function (assert) { - assert.expect(5); + assert.expect(11); this.data.turtle.records[1].product_id = 37; this.data.partner.records[0].turtles = [2, 3]; @@ -2342,7 +2342,7 @@ QUnit.module('relational_fields', { '' + '' + '' + - '' + + '' + '' + '' + '' + @@ -2351,6 +2351,20 @@ QUnit.module('relational_fields', { '' + '', res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'write') { + var commands = args.args[1].turtles; + assert.strictEqual(commands.length, 2, + "should have generated 2 commands"); + assert.deepEqual(commands[0], [1, 2, { + partner_ids: [[6, false, [2, 1]]], + product_id: 41, + }], "generated commands should be correct"); + assert.deepEqual(commands[1], [4, 3, false], + "generated commands should be correct"); + } + return this._super.apply(this, arguments); + }, }); assert.strictEqual(form.$('.o_data_row').length, 2, @@ -2366,6 +2380,32 @@ QUnit.module('relational_fields', { assert.strictEqual($('.modal .o_form_field').text(), "xphone", 'should display the form view dialog with the many2one value'); + $('.modal-footer button').click(); // close the modal + + form.$buttons.find('.o_form_button_edit').click(); + + // edit the m2m of first row + form.$('.o_list_view tbody td:first()').click(); + // remove a tag + form.$('.o_form_field_many2manytags .badge:contains(aaa) .o_delete').click(); + assert.strictEqual(form.$('.o_selected_row .o_form_field_many2manytags .o_badge_text:contains(aaa)').length, 0, + "tag should have been correctly removed"); + // add a tag + var $m2mInput = form.$('.o_selected_row .o_form_field_many2manytags input'); + $m2mInput.click(); + $m2mInput.autocomplete('widget').find('li:first()').click(); + assert.strictEqual(form.$('.o_selected_row .o_form_field_many2manytags .o_badge_text:contains(first record)').length, 1, + "tag should have been correctly added"); + + // edit the m2o of first row + var $m2oInput = form.$('.o_selected_row .o_form_field_many2one:first input'); + $m2oInput.click(); + $m2oInput.autocomplete('widget').find('li:contains(xpad)').mouseover().click(); + assert.strictEqual(form.$('.o_selected_row .o_form_field_many2one:first input').val(), 'xpad', + "m2o value should have been updated"); + + // save (should correctly generate the commands) + form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); diff --git a/addons/web/static/test/views/form_tests.js b/addons/web/static/test/views/form_tests.js index 637fba747e1..d630cfd7e01 100644 --- a/addons/web/static/test/views/form_tests.js +++ b/addons/web/static/test/views/form_tests.js @@ -2188,6 +2188,47 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('unchanged relational data is sent for onchanges', function (assert) { + assert.expect(1); + + this.data.partner.records[1].p = [4]; + this.data.partner.onchanges = { + foo: function (obj) { + obj.int_field = obj.foo.length + 1000; + }, + }; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 2, + mockRPC: function (route, args) { + if (args.method === 'onchange') { + assert.deepEqual(args.args[1].p, [[4, 4, false]], + "should send a command for field p even if it hasn't changed"); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_form_input:first').val('trigger an onchange').trigger('input'); + + form.destroy(); + }); + QUnit.test('navigation with tab key in form view', function (assert) { assert.expect(2);