[FIX] web: handle nested x2m default values

A `default_get` for a x2many field can return a new list of commands for one of
its x2many field, like

[[0, 0, {
    'groups_id': [[6, 0, [1, 2, 3]]],
}]]

where [1, 2, 3] is a list of existing ids.

This use case was not correctly handled by the BasicModel because
`_makeDefaultRecord` was not recursive.

Closes #22401
This commit is contained in:
Martin Geubelle
2018-01-24 10:59:45 +01:00
parent b8f8012af8
commit 2d3ecf29b3
2 changed files with 149 additions and 86 deletions
@@ -3238,92 +3238,7 @@ var BasicModel = AbstractModel.extend({
defs.push(self._fetchNameGet(dp));
record._changes[name] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var fieldInfo = record.fieldsInfo[record.viewType][name];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields;
var viewType = view ? view.type : fieldInfo.viewType;
var x2manyList = self._makeDataPoint({
context: record.context,
fieldsInfo: fieldsInfo,
fields: fields,
limit: fieldInfo.limit,
modelName: field.relation,
parentID: record.id,
rawContext: fieldInfo && fieldInfo.context,
relationField: field.relation_field,
res_ids: [],
static: true,
type: 'list',
viewType: viewType,
});
record._changes[name] = x2manyList.id;
x2manyList._changes = [];
var many2ones = {};
var r;
result[name] = result[name] || []; // handle false value
var isCommandList = result[name].length && _.isArray(result[name][0]);
if (!isCommandList) {
result[name] = [[6, false, result[name]]];
}
_.each(result[name], function (value) {
// value is a command
if (value[0] === 0) {
// CREATE
r = self._makeDataPoint({
modelName: x2manyList.model,
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
parentID: x2manyList.id,
viewType: viewType,
});
x2manyList._changes.push({operation: 'ADD', id: r.id});
x2manyList._cache[r.res_id] = r.id;
// this is necessary so the fields are initialized
_.each(r.getFieldNames(), function (fieldName) {
r.data[fieldName] = null;
});
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
if (!r._changes[name]) {
continue;
}
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
var rec = self._makeDataPoint({
context: r.context,
modelName: r.fields[name].relation,
data: {id: r._changes[name]},
parentID: r.id,
});
r._changes[name] = rec.id;
many2ones[name] = true;
}
}
}
if (value[0] === 6) {
// REPLACE_WITH
_.each(value[2], function (res_id) {
x2manyList._changes.push({operation: 'ADD', resID: res_id});
});
var def = self._readUngroupedList(x2manyList).then(function () {
return $.when(
self._fetchX2ManysBatched(x2manyList),
self._fetchReferencesBatched(x2manyList)
);
});
defs.push(def);
}
});
// fetch many2ones display_name
_.each(_.keys(many2ones), function (name) {
defs.push(self._fetchNameGets(x2manyList, name));
});
defs.push(self._processX2ManyCommands(record, name, result[name]));
} else {
record._changes[name] = self._parseServerValue(field, result[name]);
}
@@ -3497,6 +3412,122 @@ var BasicModel = AbstractModel.extend({
return record;
});
},
/**
* Process x2many commands in a default record by transforming the list of
* commands in operations (pushed in _changes) and fetch the related
* records fields.
*
* Note that this method can be called recursively.
*
* @todo in master: factorize this code with the postprocessing of x2many in
* _applyOnChange
*
* @private
* @param {Object} record
* @param {string} fieldName
* @param {Array[Array]} commands
* @returns {Deferred}
*/
_processX2ManyCommands: function (record, fieldName, commands) {
var self = this;
var defs = [];
var field = record.fields[fieldName];
var fieldInfo = record.fieldsInfo[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;
var viewType = view ? view.type : fieldInfo.viewType;
var x2manyList = self._makeDataPoint({
context: record.context,
fieldsInfo: fieldsInfo,
fields: fields,
limit: fieldInfo.limit,
modelName: field.relation,
parentID: record.id,
rawContext: fieldInfo && fieldInfo.context,
relationField: field.relation_field,
res_ids: [],
static: true,
type: 'list',
viewType: viewType,
});
record._changes[fieldName] = x2manyList.id;
x2manyList._changes = [];
var many2ones = {};
var r;
commands = commands || []; // handle false value
var isCommandList = commands.length && _.isArray(commands[0]);
if (!isCommandList) {
commands = [[6, false, commands]];
}
_.each(commands, function (value) {
// value is a command
if (value[0] === 0) {
// CREATE
r = self._makeDataPoint({
modelName: x2manyList.model,
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
parentID: x2manyList.id,
viewType: viewType,
});
x2manyList._changes.push({operation: 'ADD', id: r.id});
x2manyList._cache[r.res_id] = r.id;
// this is necessary so the fields are initialized
_.each(r.getFieldNames(), function (fieldName) {
r.data[fieldName] = null;
});
r._changes = _.defaults(value[2], r.data);
for (var fieldName in r._changes) {
if (!r._changes[fieldName]) {
continue;
}
var isFieldInView = fieldName in r.fields;
if (isFieldInView) {
var field = r.fields[fieldName];
var fieldType = field.type;
if (fieldType === 'many2one') {
var rec = self._makeDataPoint({
context: r.context,
modelName: field.relation,
data: {id: r._changes[fieldName]},
parentID: r.id,
});
r._changes[fieldName] = rec.id;
many2ones[fieldName] = true;
} else if (_.contains(['one2many', 'many2many'], fieldType)) {
var x2mCommands = commands[0][2][fieldName];
defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
}
}
}
}
if (value[0] === 6) {
// REPLACE_WITH
_.each(value[2], function (res_id) {
x2manyList._changes.push({operation: 'ADD', resID: res_id});
});
var def = self._readUngroupedList(x2manyList).then(function () {
return $.when(
self._fetchX2ManysBatched(x2manyList),
self._fetchReferencesBatched(x2manyList)
);
});
defs.push(def);
}
});
// fetch many2ones display_name
_.each(_.keys(many2ones), function (name) {
defs.push(self._fetchNameGets(x2manyList, name));
});
return $.when.apply($, defs);
},
/**
* Reads data from server for all missing fields.
*
@@ -6201,6 +6201,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('nested x2many default values', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="top">' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.model === 'partner' && args.method === 'default_get') {
return $.when({turtles: [[0, 0, {
partner_ids: [[6, 0, [4]]],
}]]});
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .badge').length, 1,
"m2mtags should contain one tag");
assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .o_badge_text').text(),
'aaa', "tag name should have been correctly loaded");
form.destroy();
});
QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) {
assert.expect(1);