From cc67988241b4f1c0e296774e2f095aada52ae852 Mon Sep 17 00:00:00 2001 From: "Francois (fge)" Date: Thu, 24 Sep 2020 09:15:36 +0000 Subject: [PATCH] [IMP] web: added option model_field to the reference field. model_field: name of the FieldMany2One('ir.model') containing the model of the records that can be selected. If its value is different from False, the select will not be displayed. The purpose of adding this option is to allow the creation of a reference field having its model defined in another field (Many2One). Task-2195019 --- .../static/src/js/fields/relational_fields.js | 58 +++++++---- .../static/src/js/views/basic/basic_model.js | 62 +++++++++++- addons/web/static/src/xml/base.xml | 10 +- .../tests/fields/relational_fields_tests.js | 95 +++++++++++++++++++ doc/reference/javascript_reference.rst | 5 + 5 files changed, 205 insertions(+), 25 deletions(-) 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 ==============