[FIX] web, *: basic model: makeRecord with X2manys

`makeRecord` is now asynchronous as it will load data
for relational fields (X2manys and many2ones if `display_name`
is not fetched).

This change has implied a refactoring in all `makeRecord`
occurences to adapt with the returned deferred.
This commit is contained in:
Martin Geubelle
2017-04-11 19:44:38 +02:00
committed by Aaron Bohy
parent b6ce7b136f
commit 0cb5005946
4 changed files with 357 additions and 136 deletions
@@ -37,23 +37,27 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
* @override
*/
start: function () {
this._super();
var self = this;
var defs = [this._super.apply(this, arguments)];
this.time = Date.now();
this.$progress = this.$('.progress');
if (this._initialState.bank_statement_id) {
this.handleNameRecord = this.model.makeRecord("account.bank.statement", [{
var def = this.model.makeRecord("account.bank.statement", [{
type: 'char',
name: 'name',
attrs: {string: ""},
value: this._initialState.bank_statement_id.display_name
}]);
this.name = new basic_fields.FieldChar(this,
'name', this.model.get(this.handleNameRecord),
{mode: 'edit', required: true});
}]).then(function (recordID) {
self.handleNameRecord = recordID;
self.name = new basic_fields.FieldChar(self,
'name', self.model.get(self.handleNameRecord),
{mode: 'edit', required: true});
this.name.appendTo(this.$('.statement_name_edition'));
this.$('.statement_name').text(this._initialState.bank_statement_id.display_name);
self.name.appendTo(self.$('.statement_name_edition'));
self.$('.statement_name').text(self._initialState.bank_statement_id.display_name);
});
defs.push(def);
}
this.$('h1.statement_name').text(this._initialState.title);
@@ -66,6 +70,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
}
}.bind(this);
$('body').on('keyup', this.enterHandler);
return $.when.apply($, defs);
},
/**
* @override
@@ -213,21 +219,23 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
this.model = model;
this._initialState = state;
this.fields = {
'partner_id' : new relational_fields.FieldMany2One(this,
'partner_id',
this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name),
{mode: 'edit'}
)
};
},
/**
* @override
*/
start: function () {
this.fields.partner_id.appendTo(this.$('.accounting_view caption'));
var self = this;
var def1 = this._makePartnerRecord(this._initialState.st_line.partner_id, this._initialState.st_line.partner_name).then(function (recordID) {
self.fields = {
partner_id : new relational_fields.FieldMany2One(self,
'partner_id',
self.model.get(recordID),
{mode: 'edit'}
)
};
self.fields.partner_id.appendTo(self.$('.accounting_view caption'));
});
this.$('thead .line_info_button').attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState}));
this.$el.popover({
'selector': '.line_info_button',
@@ -238,7 +246,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
'animation': false,
'toggle': 'popover'
});
delete this._initialState;
var def2 = this._super.apply(this, arguments);
return $.when(def1, def2);
},
//--------------------------------------------------------------------------
@@ -251,15 +260,17 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @param {object} state - statement line
*/
update: function (state) {
var self = this;
// isValid
this.$('caption .o_buttons button.o_validate').toggleClass('hidden', !!state.balance.type);
this.$('caption .o_buttons button.o_reconcile').toggleClass('hidden', state.balance.type <= 0);
this.$('caption .o_buttons .o_no_valid').toggleClass('hidden', state.balance.type >= 0);
// parnter_id
var record = this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name);
this.fields.partner_id.reset(record);
this.$el.attr('data-partner', state.st_line.partner_id);
// partner_id
this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name).then(function (recordID) {
self.fields.partner_id.reset(self.model.get(recordID));
self.$el.attr('data-partner', state.st_line.partner_id);
});
// mode
this.$('.create, .match').each(function () {
@@ -332,7 +343,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
var data = this.model.get(this.handleCreateRecord).data;
this.model.notifyChanges(this.handleCreateRecord, state.createForm);
record = this.model.get(this.handleCreateRecord);
var record = this.model.get(this.handleCreateRecord);
_.each(this.fields, function (field, fieldName) {
if (fieldName === "partner_id") return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
@@ -351,15 +362,18 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @private
* @param {integer} partnerID
* @param {string} partnerName
* @returns {object} dataPoint
* @returns {string} local id of the dataPoint
*/
_makePartnerRecord: function (partnerID, partnerName) {
var recordID = this.model.makeRecord('account.bank.statement.line', [{
relation: 'res.partner',
type: 'many2one',
name: 'partner_id',
value: [partnerID, partnerName]
}], {
var field = {
relation: 'res.partner',
type: 'many2one',
name: 'partner_id',
};
if (partnerID) {
field.value = [partnerID, partnerName];
}
return this.model.makeRecord('account.bank.statement.line', [field], {
partner_id: {
domain: [["parent_id", "=", false], "|", ["customer", "=", true], ["supplier", "=", true]],
options: {
@@ -367,7 +381,6 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
}
});
return this.model.get(recordID);
},
/**
@@ -377,7 +390,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @param {object} state - statement line
*/
_renderCreate: function (state) {
this.handleCreateRecord = this.model.makeRecord('account.bank.statement.line', [{
var self = this;
this.model.makeRecord('account.bank.statement.line', [{
relation: 'account.account',
type: 'many2one',
name: 'account_id',
@@ -403,35 +417,37 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
account_id: {string: _t("Account")},
label: {string: _t("Label")},
amount: {string: _t("Account")}
}).then(function (recordID) {
self.handleCreateRecord = recordID;
var record = self.model.get(self.handleCreateRecord);
self.fields.account_id = new relational_fields.FieldMany2One(self,
'account_id', record, {mode: 'edit', required: true});
self.fields.journal_id = new relational_fields.FieldMany2One(self,
'journal_id', record, {mode: 'edit'});
self.fields.tax_id = new relational_fields.FieldMany2One(self,
'tax_id', record, {mode: 'edit'});
self.fields.analytic_account_id = new relational_fields.FieldMany2One(self,
'analytic_account_id', record, {mode: 'edit'});
self.fields.label = new basic_fields.FieldChar(self,
'label', record, {mode: 'edit', required: true});
self.fields.amount = new basic_fields.FieldFloat(self,
'amount', record, {mode: 'edit', required: true});
var $create = $(qweb.render("reconciliation.line.create", {'state': state}));
self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'));
self.fields.journal_id.appendTo($create.find('.create_journal_id .o_td_field'));
self.fields.tax_id.appendTo($create.find('.create_tax_id .o_td_field'));
self.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field'));
self.fields.label.appendTo($create.find('.create_label .o_td_field'));
self.fields.amount.appendTo($create.find('.create_amount .o_td_field'));
self.$('.create').append($create);
});
var record = this.model.get(this.handleCreateRecord);
this.fields.account_id = new relational_fields.FieldMany2One(this,
'account_id', record, {mode: 'edit', required: true});
this.fields.journal_id = new relational_fields.FieldMany2One(this,
'journal_id', record, {mode: 'edit'});
this.fields.tax_id = new relational_fields.FieldMany2One(this,
'tax_id', record, {mode: 'edit'});
this.fields.analytic_account_id = new relational_fields.FieldMany2One(this,
'analytic_account_id', record, {mode: 'edit'});
this.fields.label = new basic_fields.FieldChar(this,
'label', record, {mode: 'edit', required: true});
this.fields.amount = new basic_fields.FieldFloat(this,
'amount', record, {mode: 'edit', required: true});
var $create = $(qweb.render("reconciliation.line.create", {'state': state}));
this.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'));
this.fields.journal_id.appendTo($create.find('.create_journal_id .o_td_field'));
this.fields.tax_id.appendTo($create.find('.create_tax_id .o_td_field'));
this.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field'));
this.fields.label.appendTo($create.find('.create_label .o_td_field'));
this.fields.amount.appendTo($create.find('.create_amount .o_td_field'));
this.$('.create').append($create);
},
//--------------------------------------------------------------------------
@@ -598,33 +614,6 @@ var ManualRenderer = StatementRenderer.extend({
*/
var ManualLineRenderer = LineRenderer.extend({
template: "reconciliation.manual.line",
/**
* create partner_id field in readonly mode
*
* @override
*/
init: function (parent, model, state) {
this._super.apply(this, arguments);
if (state.partner_id) {
this.fields.partner_id = new relational_fields.FieldMany2One(this,
'partner_id',
this._makePartnerRecord(state.partner_id, state.partner_name),
{mode: 'readonly'}
);
} else {
this.fields.title_account_id = new relational_fields.FieldMany2One(this,
'account_id',
this.model.get(this.model.makeRecord('account.move.line', [{
relation: 'account.account',
type: 'many2one',
name: 'account_id',
value: [state.account_id.id, state.account_id.display_name]
}])),
{mode: 'readonly'}
);
}
},
/**
* @override
* @param {string} handle
@@ -643,13 +632,44 @@ var ManualLineRenderer = LineRenderer.extend({
* @override
*/
start: function () {
this._super();
if (!this.fields.title_account_id) {
this.fields.partner_id.$el.prependTo(this.$('.accounting_view thead td:eq(1) span:first'));
} else {
this.fields.partner_id.destroy();
this.fields.title_account_id.appendTo(this.$('.accounting_view thead td:eq(1) span:first'));
}
var self = this;
return this._super.apply(this, arguments).then(function () {
var defs = [];
var def;
if (self._initialState.partner_id) {
def = self._makePartnerRecord(self._initialState.partner_id, self._initialState.partner_name).then(function (recordID) {
self.fields.partner_id = new relational_fields.FieldMany2One(self,
'partner_id',
self.model.get(recordID),
{mode: 'readonly'}
);
});
defs.push(def);
} else {
def = self.model.makeRecord('account.move.line', [{
relation: 'account.account',
type: 'many2one',
name: 'account_id',
value: [self._initialState.account_id.id, self._initialState.account_id.display_name],
}]).then(function (recordID) {
self.fields.title_account_id = new relational_fields.FieldMany2One(self,
'account_id',
self.model.get(recordID),
{mode: 'readonly'}
);
});
defs.push(def);
}
return $.when.apply($, defs).then(function () {
if (!self.fields.title_account_id) {
self.fields.partner_id.$el.prependTo(self.$('.accounting_view thead td:eq(1) span:first'));
} else {
self.fields.partner_id.destroy();
self.fields.title_account_id.appendTo(self.$('.accounting_view thead td:eq(1) span:first'));
}
});
});
},
/**
* @override
@@ -451,39 +451,92 @@ var BasicModel = AbstractModel.extend({
*/
makeRecord: function (model, fields, fieldInfo) {
var self = this;
var defs = [];
var record_fields = {};
_.each(fields, function (field) {
record_fields[field.name] = _.pick(field, 'type', 'relation', 'domain');
});
var viewInfo = {};
viewInfo.default = fieldInfo || _.mapObject(fields, function () {
return {};
fieldInfo = fieldInfo || {};
var fieldsInfo = {};
fieldsInfo.default = {};
_.each(fields, function (field) {
fieldsInfo.default[field.name] = fieldInfo[field.name] || {};
});
var record = this._makeDataPoint({
modelName: model,
fields: record_fields,
fieldsInfo: viewInfo,
fieldsInfo: fieldsInfo,
viewType: 'default',
});
_.each(fields, function (field) {
if ('value' in field) {
if (field.type === 'many2one') {
var id = field.value[0];
var display_name = field.value[1];
var relatedRecord = self._makeDataPoint({
var dataPoint;
if (field.type === 'many2one') {
if (field.value) {
var id = _.isArray(field.value) ? field.value[0] : field.value;
var display_name = _.isArray(field.value) ? field.value[1] : undefined;
dataPoint = self._makeDataPoint({
modelName: field.relation,
data: {
id: id,
display_name: display_name,
}
},
});
record.data[field.name] = relatedRecord.id;
} else {
record.data[field.name] = field.value;
record.data[field.name] = dataPoint.id;
if (display_name === undefined) {
defs.push(self._fetchNameGet(dataPoint));
}
}
} else if (field.type === 'one2many' || field.type === 'many2many') {
var relatedFieldsInfo = {};
relatedFieldsInfo.default = {};
_.each(field.fields, function (field) {
relatedFieldsInfo.default[field.name] = {};
});
var dpParams = {
fieldsInfo: relatedFieldsInfo,
modelName: field.relation,
static: true,
type: 'list',
viewType: 'default',
};
var needLoad = false;
// As value, you could either pass:
// - a list of ids related to the record
// - a list of object
// We only need to load the datapoint in the first case.
if (field.value && field.value.length) {
if (_.isObject(field.value[0])) {
dpParams.res_ids = _.pluck(field.value, 'id');
dataPoint = self._makeDataPoint(dpParams);
_.each(field.value, function (data) {
var recordDP = self._makeDataPoint({
data: data,
modelName: field.relation,
type: 'record',
});
dataPoint.data.push(recordDP.id);
});
} else {
dpParams.res_ids = field.value;
dataPoint = self._makeDataPoint(dpParams);
needLoad = true;
}
} else {
dpParams.res_ids = [];
dataPoint = self._makeDataPoint(dpParams);
}
if (needLoad) {
defs.push(self._load(dataPoint));
}
record.data[field.name] = dataPoint.id;
} else if (field.value) {
record.data[field.name] = field.value;
}
});
return record.id;
return $.when.apply($, defs).then(function () {
return record.id;
});
},
/**
* This is an extremely important method. All changes in any field go
@@ -832,6 +885,7 @@ var BasicModel = AbstractModel.extend({
var def;
if (rel_data.display_name === undefined) {
var field = record.fields[fieldName];
// TODO: refactor this to use _fetchNameGet
def = this._rpc({
model: field.relation,
method: 'name_get',
@@ -1154,6 +1208,22 @@ var BasicModel = AbstractModel.extend({
});
});
},
/**
* Fetch name_get for a record datapoint.
*
* @param {Object} dataPoint
* @returns {Deferred}
*/
_fetchNameGet: function (dataPoint) {
return this._rpc({
model: dataPoint.model,
method: 'name_get',
args: [dataPoint.res_id],
context: dataPoint.getContext(),
}).then(function (result) {
dataPoint.data.display_name = result[0][1];
});
},
_fetchNameGets: function (list, fieldName) {
var self = this;
var model;
@@ -48,6 +48,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
FieldManagerMixin.init.call(this);
this.title = options.title;
this.fields = options.fields;
this.fieldName = options.fieldName;
this.write_model = options.write_model;
this.write_field = options.write_field;
@@ -56,20 +57,29 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
this.filters = options.filters;
this.label = options.label;
this.getColor = options.getColor;
},
willStart: function () {
var self = this;
var defs = [this._super.apply(this, arguments)];
if (this.write_model || this.write_field) {
this.many2one = new SidebarFilterM2O(this,
this.write_field,
this.model.get(this.model.makeRecord(this.write_model, [{
name: this.write_field,
relation: options.fields[this.fieldName].relation,
type: 'many2one',
}])),
{
mode: 'edit',
can_create: false,
});
var def = this.model.makeRecord(this.write_model, [{
name: this.write_field,
relation: this.fields[this.fieldName].relation,
type: 'many2one',
}]).then(function (recordID) {
self.many2one = new SidebarFilterM2O(self,
self.write_field,
self.model.get(recordID),
{
mode: 'edit',
can_create: false,
});
});
defs.push(def);
}
return $.when.apply($, defs);
},
start: function () {
this._super();
+138 -17
View File
@@ -747,19 +747,18 @@ QUnit.module('Views', {
var record = model.get(resultID, {raw: true});
assert.deepEqual(record.data.product_ids, [37], "should have correct initial value");
var relatedRecordID = model.makeRecord('product', [{
model.makeRecord('product', [{
name: 'name',
string: "Product Name",
type: "char",
value: "xpod"
}
]);
model.notifyChanges(record.id, {
product_ids: {operation: "ADD", id: relatedRecordID}
]).then(function (relatedRecordID) {
model.notifyChanges(record.id, {
product_ids: {operation: "ADD", id: relatedRecordID}
});
model.save(record.id);
});
model.save(record.id);
});
model.destroy();
});
@@ -794,22 +793,23 @@ QUnit.module('Views', {
assert.strictEqual(record.data.product_ids.data.length, 0,
"one2many should start with a list of length 0");
var relatedRecordID = model.makeRecord('product', [{
model.makeRecord('product', [{
name: 'name',
string: "Product Name",
type: "char",
value: "xpod"
}
]);
model.notifyChanges(resultID, {
product_ids: {operation: "ADD", id: relatedRecordID}
});
]).then(function (relatedRecordID) {
model.notifyChanges(resultID, {
product_ids: {operation: "ADD", id: relatedRecordID}
});
record = model.get(resultID);
assert.strictEqual(record.data.product_ids.data.length, 1,
"one2many should be a list of length 1");
assert.strictEqual(record.data.product_ids.data[0].data.name, "xpod",
"one2many should have correct data");
record = model.get(resultID);
assert.strictEqual(record.data.product_ids.data.length, 1,
"one2many should be a list of length 1");
assert.strictEqual(record.data.product_ids.data[0].data.name, "xpod",
"one2many should have correct data");
});
});
model.destroy();
});
@@ -1096,4 +1096,125 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('call makeRecord with a pre-fetched many2one field', function (assert) {
assert.expect(3);
var rpcCount = 0;
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
rpcCount++;
return this._super(route, args);
},
});
model.makeRecord('coucou', [{
name: 'partner_id',
relation: 'partner',
type: 'many2one',
value: [1, 'first partner'],
}], {
partner_id: {
options: {
no_open: true,
},
},
}).then(function (recordID) {
var record = model.get(recordID);
assert.deepEqual(record.fieldsInfo.default.partner_id, {options: {no_open: true}},
"makeRecord should have generated the fieldsInfo");
assert.deepEqual(record.data.partner_id.data, {id: 1, display_name: 'first partner'},
"many2one should contain the partner with id 1");
assert.strictEqual(rpcCount, 0, "makeRecord should not have done any rpc");
});
model.destroy();
});
QUnit.test('call makeRecord with a many2many field', function (assert) {
assert.expect(5);
var rpcCount = 0;
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
rpcCount++;
return this._super(route, args);
},
});
model.makeRecord('coucou', [{
name: 'partner_ids',
fields: [{
name: 'id',
type: 'integer',
}, {
name: 'display_name',
type: 'char',
}],
relation: 'partner',
type: 'many2many',
value: [1, 2],
}]).then(function (recordID) {
var record = model.get(recordID);
assert.deepEqual(record.fieldsInfo.default.partner_ids, {},
"makeRecord should have generated the fieldsInfo");
assert.strictEqual(record.data.partner_ids.count, 2,
"there should be 2 elements in the many2many");
assert.strictEqual(record.data.partner_ids.data.length, 2,
"many2many should be a list of length 2");
assert.deepEqual(record.data.partner_ids.data[0].data, {id: 1, display_name: 'first partner'},
"many2many should contain the partner with id 1");
assert.strictEqual(rpcCount, 1, "makeRecord should have done 1 rpc");
});
model.destroy();
});
QUnit.test('call makeRecord with a pre-fetched many2many field', function (assert) {
assert.expect(5);
var rpcCount = 0;
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
rpcCount++;
return this._super(route, args);
},
});
model.makeRecord('coucou', [{
name: 'partner_ids',
fields: [{
name: 'id',
type: 'integer',
}, {
name: 'display_name',
type: 'char',
}],
relation: 'partner',
type: 'many2many',
value: [{
id: 1,
display_name: "first partner",
}, {
id: 2,
display_name: "second partner",
}],
}]).then(function (recordID) {
var record = model.get(recordID);
assert.deepEqual(record.fieldsInfo.default.partner_ids, {},
"makeRecord should have generated the fieldsInfo");
assert.strictEqual(record.data.partner_ids.count, 2,
"there should be 2 elements in the many2many");
assert.strictEqual(record.data.partner_ids.data.length, 2,
"many2many should be a list of length 2");
assert.deepEqual(record.data.partner_ids.data[0].data, {id: 1, display_name: 'first partner'},
"many2many should contain the partner with id 1");
assert.strictEqual(rpcCount, 0, "makeRecord should not have done any rpc");
});
model.destroy();
});
});});