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 ==============