[FIX] web: BasicModel: correctly generate x2many commands
This commit fixes two problems. First, the commands weren't correctly generated for relational fields inside relational fields. For example, if a form view contained a one2many containing itself a many2many, the commands weren't generated for the many2many inside the one2many. Thus, it's value couldn't be saved. Second, when doing an onchange RPC, all record data should be sent to the server, not only what have changed. Before this rev., for x2many fields, commands were sent only if the field had changed.
This commit is contained in:
@@ -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;
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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', {
|
||||
'<field name="product_id"/>' +
|
||||
'</group>' +
|
||||
'</form>' +
|
||||
'<tree>' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<field name="partner_ids" widget="many2many_tags"/>' +
|
||||
@@ -2351,6 +2351,20 @@ QUnit.module('relational_fields', {
|
||||
'</group>' +
|
||||
'</form>',
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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: '<form>' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="int_field"/>' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="bar"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</group>' +
|
||||
'</form>',
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user