diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 37fdbb1e454..901811df26c 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -3268,13 +3268,17 @@ var FieldReference = FieldMany2One.extend({
// needs to be copied as it is an unmutable object
this.field = _.extend({}, this.field);
- this._setState();
+ this.resetOnAnyFieldChange = this.resetOnAnyFieldChange || this.nodeOptions.model_field;
+ this._setState(false);
},
/**
* @override
*/
start: function () {
- this.$('select').val(this.field.relation);
+ this.modelName = this.field.relation;
+ if (this.el.querySelector('select')) {
+ this.el.querySelector('select').value = this.modelName;
+ }
return this._super.apply(this, arguments);
},
@@ -3311,7 +3315,21 @@ var FieldReference = FieldMany2One.extend({
}
return value && value.data && value.data.display_name || '';
},
-
+ /**
+ * Apply the model contained in the option model_field
+ * and re-initialize the record if the model change.
+ * @param {boolean} initRecord :true, re-initialize the record if the model changes.
+ * Necessary for wizards.
+ */
+ _applyModelField: function (initRecord) {
+ let resourceRef = this.record.specialData[this.name];
+ if (resourceRef) {
+ if (initRecord && resourceRef.hasChanged) {
+ this.reinitialize(false);
+ }
+ this.modelName = resourceRef.modelName;
+ }
+ },
/**
* Add a select in edit mode (for the model).
*
@@ -3320,15 +3338,16 @@ var FieldReference = FieldMany2One.extend({
_renderEdit: function () {
this._super.apply(this, arguments);
- if (this.$('select').val()) {
+ if (this.modelName) {
this.$('.o_input_dropdown').show();
- this.$el.addClass('o_row'); // this class is used to display the two
- // components (select & input) on the same line
+ if (!this.nodeOptions.model_field) {
+ // this class is used to display the two components (select & input) on the same line
+ this.$el.addClass('o_row');
+ }
} else {
// hide the many2one if the selection is empty
this.$('.o_input_dropdown').hide();
}
-
},
/**
* @override
@@ -3336,9 +3355,10 @@ var FieldReference = FieldMany2One.extend({
*/
_reset: function () {
this._super.apply(this, arguments);
- var value = this.$('select').val();
- this._setState();
- this.$('select').val(this.value && this.value.model || value);
+ this._setState(true);
+ if (this.el.querySelector('select')) {
+ this.el.querySelector('select').value = this.modelName;
+ }
},
/**
* Set `relation` key in field properties.
@@ -3353,14 +3373,18 @@ var FieldReference = FieldMany2One.extend({
/**
* @private
*/
- _setState: function () {
+ _setState: function (initRecord) {
if (this.field.type === 'char') {
// in this case, the value is stored in specialData instead
this.value = this.record.specialData[this.name];
}
-
- if (this.value) {
- this._setRelation(this.value.model);
+ if (this.nodeOptions.model_field) {
+ this._applyModelField(initRecord);
+ } else if (this.value && this.value.model) {
+ this.modelName = this.value.model;
+ }
+ if (this.modelName) {
+ this._setRelation(this.modelName);
}
},
/**
@@ -3371,7 +3395,7 @@ var FieldReference = FieldMany2One.extend({
value = value || {};
// we need to specify the model for the change in basic_model
// the value is then now a dict with id, display_name and model
- value.model = this.$('select').val();
+ value.model = this.modelName;
return this._super(value, options);
},
@@ -3385,9 +3409,9 @@ var FieldReference = FieldMany2One.extend({
* @private
*/
_onSelectionChange: function () {
- var value = this.$('select').val();
+ this.modelName = this.el.querySelector('select').value || '';
this.reinitialize(false);
- this._setRelation(value);
+ this._setRelation(this.modelName);
},
});
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 333c0a7554f..3e45d6421f8 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -2365,6 +2365,58 @@ var BasicModel = AbstractModel.extend({
}
return evaluated;
},
+ /**
+ * Fetch the type of model (e.g. 'res.partner') contained in the model_field
+ * @private
+ * @param {Object} record - an element from the localData
+ * @param {Object} fieldName - the name of the field
+ * @param {Object} fieldInfo - the info of the field
+ * @returns {Promise}
+ */
+ _fetchModelFieldReference: async function (record, fieldName, fieldInfo) {
+ const modelField = fieldInfo.options.model_field;
+ const value = record._changes && record._changes[modelField] || record.data[modelField];
+ if (value) {
+ if (record.fields[modelField].type !== "many2one"
+ || record.fields[modelField].relation !== 'ir.model') {
+ throw new Error(`The model_field of the reference field
+ ${fieldName} must be a many2one('ir.model').`);
+ }
+ const modelId = this.localData[value].res_id;
+ const resourceRef = record.specialData[fieldName];
+ if (resourceRef && modelId === resourceRef.modelId) {
+ resourceRef.hasChanged = false;
+ return Promise.resolve(resourceRef);
+ } else {
+ const result = await this._rpc({
+ model: 'ir.model',
+ method: 'read',
+ args: [modelId, ['id', 'model']],
+ });
+ // Checks the case where the data on the backend side are not synchronized
+ // (modelFieldName != referenceFieldName) when opening the edit view (!_changes).
+ // We want to avoid resynchronization in order not to modify the data
+ // without being requested.
+ if (!record._changes && record.data[fieldName]
+ && result[0].model !== this.localData[record.data[fieldName]].model) {
+ const modelFieldName = record.fields[modelField].string;
+ const referenceFieldName = record.fields[fieldName].string;
+
+ this.do_warn(_t(`'${referenceFieldName}' is unsynchronized
+ with '${modelFieldName}'.`),
+ _t(`If you change ${modelFieldName} or
+ ${referenceFieldName}, the synchronization
+ will be reapplied and the data will be modified.`), true);
+ return false;
+ }
+ return {
+ modelName: result[0].model,
+ modelId: result[0].id,
+ hasChanged: true,
+ };
+ }
+ }
+ },
/**
* Fetch name_get for a record datapoint.
*
@@ -2846,17 +2898,19 @@ var BasicModel = AbstractModel.extend({
* @private
* @param {Object} record - an element from the localData
* @param {Object} fieldName - the name of the field
+ * @param {Object} fieldInfo - the info of the field
* @returns {Promise}
*/
- _fetchSpecialReference: function (record, fieldName) {
- var def;
+ _fetchSpecialReference: function (record, fieldName, fieldInfo) {
var field = record.fields[fieldName];
if (field.type === 'char') {
// if the widget reference is set on a char field, the name_get
// needs to be fetched a posteriori
- def = this._fetchReference(record, fieldName);
+ return Promise.resolve(this._fetchReference(record, fieldName));
+ } else if (fieldInfo.options.model_field) {
+ return this._fetchModelFieldReference(record, fieldName, fieldInfo);
}
- return Promise.resolve(def);
+ return Promise.resolve();
},
/**
* Fetches all the m2o records associated to the given fieldName. If the
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 4aae9ffa8a3..764d15dc5c8 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1273,10 +1273,12 @@
-
+
+
+
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index dd80553379c..cf09b08dd45 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -39,6 +39,7 @@ QUnit.module('relational_fields', {
user_id: {string: "User", type: 'many2one', relation: 'user'},
reference: {string: "Reference Field", type: 'reference', selection: [
["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
+ model_id: {string: "Model", type:'many2one', relation:'ir.model'}
},
records: [{
id: 1,
@@ -148,6 +149,25 @@ QUnit.module('relational_fields', {
name: "Christine",
}]
},
+ 'ir.model': {
+ fields: {
+ model: {string: "Model", type: "char"},
+ },
+ records: [{
+ id: 17,
+ name: "Partner",
+ model: 'partner',
+ }, {
+ id: 20,
+ name: "Product",
+ model: 'product',
+ }, {
+ id: 21,
+ name: "Partner Type",
+ model: 'partner_type',
+ }],
+ onchanges: {},
+ },
};
},
}, function () {
@@ -2991,6 +3011,81 @@ QUnit.module('relational_fields', {
list.destroy();
});
+ QUnit.test('widget reference with model_field option', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].reference = false;
+ this.data.partner.records[0].model_id = 20;
+ this.data.partner.records[1].display_name = "John Smith";
+ this.data.product.records[0].display_name = "Product 1";
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: ``,
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.containsNone(form.$('select'), 'the selection list of the reference field should not exist.');
+ assert.strictEqual(form.$('.o_field_many2one[name="reference"] input').val(), '',
+ 'no record should be selected in the reference field');
+
+ await testUtils.fields.editInput(form.$('.o_field_many2one[name="reference"] input'), 'Product 1');
+ await testUtils.dom.click($('.ui-autocomplete .ui-menu-item:first-child'));
+ assert.strictEqual(form.$('.o_field_many2one[name="reference"] input').val(), 'Product 1',
+ 'the Product 1 record should be selected in the reference field');
+
+ await testUtils.fields.editInput(form.$('.o_field_many2one[name="model_id"] input'), 'Partner');
+ await testUtils.dom.click($('.ui-autocomplete .ui-menu-item:first-child'));
+ assert.strictEqual(form.$('.o_field_many2one[name="reference"] input').val(), '',
+ 'no record should be selected in the reference field');
+
+ await testUtils.fields.editInput(form.$('.o_field_many2one[name="reference"] input'), 'John');
+ await testUtils.dom.click($('.ui-autocomplete .ui-menu-item:first-child'));
+ assert.strictEqual(form.$('.o_field_many2one[name="reference"] input').val(), 'John Smith',
+ 'the John Smith record should be selected in the reference field');
+
+ form.destroy();
+ });
+
+ QUnit.test('widget reference with model_field option (model_field not synchronized with reference)', async function (assert) {
+ // Checks that the data is not modified even though it is not synchronized.
+ // Not synchronized = model_id contains a different model than the one used in reference.
+ assert.expect(5);
+ this.data.partner.records[0].reference = 'partner,1';
+ this.data.partner.records[0].model_id = 20;
+ this.data.partner.records[0].display_name = "John Smith";
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: ``,
+ res_id: 1,
+ });
+
+ assert.containsNone(form.$('select'), 'the selection list of the reference field should not exist.');
+ assert.strictEqual(form.$('.o_field_widget[name="model_id"] span').text(), 'Product',
+ 'the value of model_id field should be Product');
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] span').text(), 'John Smith',
+ 'the value of model_id field should be John Smith');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_many2one[name="model_id"] input').val(), 'Product',
+ 'the Product model should be selected in the model_id field');
+ assert.strictEqual(form.$('.o_field_many2one[name="reference"] input').val(), 'John Smith',
+ 'the John Smith record should be selected in the reference field');
+
+ form.destroy();
+ });
+
QUnit.test('one2many with extra field from server not in form', async function (assert) {
assert.expect(6);
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index eb27db11501..1111e6b626f 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -2275,6 +2275,11 @@ reference (FieldReference)
- Supported field types: *char, reference*
+ Options:
+
+ - model_field: name of a FieldMany2One('ir.model') containing the model of the records that can be selected.
+ When this option is set, the select part of the FieldReference isn't displayed.
+
Client actions
==============