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);