diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index e25c02f781e..cef45c2a976 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -407,6 +407,7 @@ var AbstractField = Widget.extend({ this._isValid = true; } catch (e) { this._isValid = false; + this.trigger_up('set_dirty', {dataPointID: this.dataPointID}); return $.Deferred().reject(); } if (!(options && options.forceChange) && this._isSameValue(value)) { diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 481fc0cf4dd..c899530de07 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -19,6 +19,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, { discard_changes: '_onDiscardChanges', reload: '_onReload', + set_dirty: '_onSetDirty', sidebar_data_asked: '_onSidebarDataAsked', translate: '_onTranslate', }), @@ -553,6 +554,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { }); } }, + /** + * @private + * @param {OdooEvent} ev + */ + _onSetDirty: function (ev) { + ev.stopPropagation(); // prevent other controllers from handling this request + this.model.setDirty(ev.data.dataPointID); + }, /** * Handler used to get all the data necessary when a custom action is * performed through the sidebar. 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 9e7fb8b78ef..57c4a1f3743 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -947,6 +947,17 @@ var BasicModel = AbstractModel.extend({ record.fields = _.defaults(record.fields, viewInfo.fields); record.fieldsInfo = _.defaults(record.fieldsInfo, viewInfo.fieldsInfo); }, + /** + * Manually sets a resource as dirty. This is used to notify that a field + * has been modified, but with an invalid value. In that case, the value is + * not sent to the basic model, but the record should still be flagged as + * dirty so that it isn't discarded without any warning. + * + * @param {string} id a resource id + */ + setDirty: function (id) { + this.localData[id]._isDirty = true; + }, /** * For list resources, this changes the orderedBy key. * diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 486fa22d56c..a96e21cc443 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -6168,6 +6168,42 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many with default value: edit line to make it invalid', function (assert) { + assert.expect(3); + + this.data.partner.fields.p.default = [ + [0, false, {foo: "coucou", int_field: 5}], + ]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
', + }); + + // edit the line and enter an invalid value for int_field + form.$('.o_data_row .o_data_cell:nth(1)').click(); + form.$('.o_field_widget[name=int_field]').val('e').trigger('input'); + form.$el.click(); // try to validate the line + + assert.strictEqual(form.$('.o_data_row.o_selected_row').length, 1, + "line should not have been removed and should still be in edition"); + assert.strictEqual($('.modal').length, 1, + "a confirmation dialog should be opened"); + assert.ok(form.$('.o_field_widget[name=int_field]').hasClass('o_field_invalid'), + "should indicate that int_field is invalid"); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) {