[FIX] web: fix various bugs when fetching x2manys in form views
- When an one2many is displayed in a list view, the field data are not fully loaded since only the number of record is displayed. Then when the related form view is opened the one2many fields displayed need to be loaded. - Optimized the fetch of x2manys without doing an useless rpc if we don't need all the fields but only the relation records ids and adapt the tests for this optimisation. - Fix wrong inheritance for kanbanmany2many_tags
This commit is contained in:
@@ -1179,7 +1179,7 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
|
||||
},
|
||||
});
|
||||
|
||||
var KanbanFieldMany2ManyTags = FieldMany2Many.extend({
|
||||
var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({
|
||||
_render: function () {
|
||||
var self = this;
|
||||
this.$el.addClass('o_form_field_many2manytags o_kanban_tags');
|
||||
|
||||
@@ -1619,40 +1619,50 @@ var BasicModel = AbstractModel.extend({
|
||||
if (!ids.length || fieldInfo.__no_fetch) {
|
||||
return $.when();
|
||||
}
|
||||
|
||||
var def;
|
||||
var fieldNames = _.keys(fieldInfo.relatedFields);
|
||||
// step 2: fetch data from server
|
||||
return this._rpc({
|
||||
// if we want specific fields
|
||||
// if not we return an array of objects with the id
|
||||
// to avoid fetching all the relation fields and an useless rpc
|
||||
if (fieldNames.length) {
|
||||
def = this._rpc({
|
||||
model: field.relation,
|
||||
method: 'read',
|
||||
args: [ids, _.keys(fieldInfo.relatedFields)],
|
||||
args: [ids, fieldNames],
|
||||
context: {}, // FIXME
|
||||
})
|
||||
.then(function (results) {
|
||||
// step 3: assign values to correct datapoints
|
||||
var dataPoints = _.map(results, function (result) {
|
||||
return self._makeDataPoint({
|
||||
modelName: field.relation,
|
||||
data: result,
|
||||
fields: fields,
|
||||
fieldsInfo: fieldsInfo,
|
||||
viewType: viewType,
|
||||
});
|
||||
});
|
||||
|
||||
_.each(list.data, function (dataPoint) {
|
||||
var record = self.localData[dataPoint];
|
||||
var m2mList = self.localData[record.data[fieldName]];
|
||||
|
||||
m2mList.data = [];
|
||||
_.each(m2mList.res_ids, function (res_id) {
|
||||
var dataPoint = _.find(dataPoints, function (d) {
|
||||
return d.res_id === res_id;
|
||||
});
|
||||
m2mList.data.push(dataPoint.id);
|
||||
m2mList.count++;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
def = $.when(_.map(ids, function (id) {
|
||||
return {id:id};
|
||||
}));
|
||||
}
|
||||
return def.then(function (results) {
|
||||
// step 3: assign values to correct datapoints
|
||||
var dataPoints = _.map(results, function (result) {
|
||||
return self._makeDataPoint({
|
||||
modelName: field.relation,
|
||||
data: result,
|
||||
fields: fields,
|
||||
fieldsInfo: fieldsInfo,
|
||||
viewType: viewType,
|
||||
});
|
||||
});
|
||||
|
||||
_.each(list.data, function (dataPoint) {
|
||||
var record = self.localData[dataPoint];
|
||||
var m2mList = self.localData[record.data[fieldName]];
|
||||
|
||||
m2mList.data = [];
|
||||
_.each(m2mList.res_ids, function (res_id) {
|
||||
var dataPoint = _.find(dataPoints, function (d) {
|
||||
return d.res_id === res_id;
|
||||
});
|
||||
m2mList.data.push(dataPoint.id);
|
||||
m2mList.count++;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* batch request for x2ms for datapoint of type list
|
||||
@@ -2444,12 +2454,18 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
}
|
||||
if (missingIds.length) {
|
||||
def = this._rpc({
|
||||
model: list.model,
|
||||
method: 'read',
|
||||
args: [missingIds, fieldNames],
|
||||
context: {} // FIXME
|
||||
});
|
||||
if (fieldNames.length) {
|
||||
def = this._rpc({
|
||||
model: list.model,
|
||||
method: 'read',
|
||||
args: [missingIds, fieldNames],
|
||||
context: {} // FIXME
|
||||
});
|
||||
} else {
|
||||
def = $.when(_.map(missingIds, function (id) {
|
||||
return {id:id};
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
def = $.when();
|
||||
}
|
||||
|
||||
@@ -66,6 +66,29 @@ var BasicView = AbstractView.extend({
|
||||
var viewType = this.viewType;
|
||||
var viewFields = Object.keys(record.fieldsInfo[viewType]);
|
||||
var fieldNames = _.difference(viewFields, Object.keys(record.data));
|
||||
// Suppose that in a form view, there is an x2many list view with
|
||||
// an x2many field F of the related record, and that F is also
|
||||
// displayed in the x2many form view (e.g. as a list or a kanban).
|
||||
// In this case, F is represented in record.data
|
||||
// (as it is known by the x2many list view), but its related fields
|
||||
// (those displayed in the list or kanban) still need to be fetched.
|
||||
// So when this happens, F is added to the list of fieldNames to fetch.
|
||||
_.each(viewFields, function (name) {
|
||||
if (!_.contains(fieldNames, name)) {
|
||||
var fieldType = record.fields[name].type;
|
||||
if ((fieldType === 'one2many' || fieldType === 'many2many')) {
|
||||
if (!('fieldsInfo' in record.data[name])) {
|
||||
fieldNames.push(name);
|
||||
} else {
|
||||
var recordViewTypes = Object.keys(record.data[name].fieldsInfo);
|
||||
var fieldViewTypes = Object.keys(record.fieldsInfo[viewType][name].views);
|
||||
if (_.difference(fieldViewTypes, recordViewTypes).length) {
|
||||
fieldNames.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fieldNames.length && !this.model.isNew(record.id)) {
|
||||
return this.model.reload(this.recordID, {
|
||||
fieldNames: fieldNames,
|
||||
|
||||
@@ -431,7 +431,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('fetch x2manys in list, with not too many rpcs', function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.records[0].category = [12, 15];
|
||||
this.data.partner.records[1].category = [12, 14];
|
||||
@@ -441,12 +441,11 @@ QUnit.module('Views', {
|
||||
this.params.groupedBy = [];
|
||||
this.params.res_id = undefined;
|
||||
|
||||
var rpcCount = 0;
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
rpcCount++;
|
||||
assert.step(route);
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
@@ -456,7 +455,8 @@ QUnit.module('Views', {
|
||||
|
||||
assert.strictEqual(record.data[0].data.category.data.length, 2,
|
||||
"first record should have 2 categories loaded");
|
||||
assert.strictEqual(rpcCount, 2, "should have done 2 rpc (searchread and read category)");
|
||||
assert.verifySteps(["/web/dataset/search_read"],
|
||||
"should have done 2 rpc (searchread and read category)");
|
||||
});
|
||||
model.destroy();
|
||||
});
|
||||
@@ -1280,4 +1280,24 @@ QUnit.module('Views', {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('load model with many2many field properly fetched', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.params.fieldNames = ['category'];
|
||||
this.params.res_id = 1;
|
||||
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
mockRPC: function(route, args) {
|
||||
assert.step(args.method);
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
model.load(this.params);
|
||||
assert.verifySteps(['read'],
|
||||
"there should be only one read");
|
||||
model.destroy();
|
||||
});
|
||||
});});
|
||||
|
||||
@@ -3101,6 +3101,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('render stat button with string inline', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -3144,5 +3145,65 @@ QUnit.module('Views', {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('open one2many form containing one2many', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
this.data.partner.records[0].product_ids = [37];
|
||||
this.data.product.fields.partner_type_ids = {
|
||||
string: "one2many partner", type: "one2many", relation: "partner_type",
|
||||
};
|
||||
this.data.product.records[0].partner_type_ids = [12];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="product_ids">' +
|
||||
'<tree create="0">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="partner_type_ids"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'product,false,form':
|
||||
'<form string="Products">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="partner_type_ids">' +
|
||||
'<tree create="0">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="color"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
var row = form.$('.o_form_field_one2many .o_list_view .o_data_row');
|
||||
assert.strictEqual(row.children()[1].textContent, '1 record',
|
||||
"the cell should contains the number of record: 1");
|
||||
row.click();
|
||||
var modal_row = $('.modal-body .o_form_sheet .o_form_field_one2many .o_list_view .o_data_row');
|
||||
assert.strictEqual(modal_row.children().length, 2,
|
||||
"the row should contains the 2 fields defined in the form view");
|
||||
assert.strictEqual($(modal_row).text(), "gold2",
|
||||
"the value of the fields should be fetched and displayed");
|
||||
assert.verifySteps(['read', 'read', 'read', 'read'],
|
||||
"there should be 4 read rpcs");
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -420,13 +420,12 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2manytags are correctly fetched and displayed', function (assert) {
|
||||
assert.expect(3);
|
||||
QUnit.test('many2many_tags are correctly fetched and displayed', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.records[0].category_ids = [6, 7];
|
||||
this.data.partner.records[1].category_ids = [7];
|
||||
|
||||
var count = 0;
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
@@ -439,8 +438,8 @@ QUnit.module('Views', {
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function () {
|
||||
count++;
|
||||
mockRPC: function (route) {
|
||||
assert.step(route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
@@ -450,7 +449,7 @@ QUnit.module('Views', {
|
||||
'first record should contain 2 tags');
|
||||
assert.ok($first_record.find('.o_tag:first()').hasClass('o_tag_color_2'),
|
||||
'first tag should have color 2');
|
||||
assert.strictEqual(count, 2,
|
||||
assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/category/read'],
|
||||
'two RPC should have been done(one search read and one read for the m2m');
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
@@ -644,9 +644,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('can display a list with a many2many field', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var rpcCount = 0;
|
||||
assert.expect(3);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
@@ -656,11 +654,11 @@ QUnit.module('Views', {
|
||||
'<field name="m2m"/>' +
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
rpcCount++;
|
||||
assert.step(route);
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(rpcCount, 2, "should have done 2 rpcs: 1 searchread and 1 read for m2m");
|
||||
assert.verifySteps(['/web/dataset/search_read'], "should have done 1 search_read");
|
||||
assert.ok(list.$('td:contains(3 records)').length,
|
||||
"should have a td with correct formatted value");
|
||||
list.destroy();
|
||||
|
||||
Reference in New Issue
Block a user