diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index b48c0dfbe69..104d8f3bed8 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1266,11 +1266,18 @@ var FieldOne2Many = FieldX2Many.extend({ /** * @private * @param {Object} params + * @param {Object} [params.context] We allow additional context, this is + * used for example to define default values when adding new lines to + * a one2many with control/create tags. */ _openFormDialog: function (params) { + var context = this.record.getContext(_.extend({}, + this.recordParams, + { additionalContext: params.context } + )); this.trigger_up('open_one2many_record', _.extend(params, { domain: this.record.getDomain(this.recordParams), - context: this.record.getContext(this.recordParams), + context: context, field: this.field, fields_view: this.attrs.views && this.attrs.views.form, parentID: this.value.id, @@ -1297,6 +1304,7 @@ var FieldOne2Many = FieldX2Many.extend({ // we don't want interference with the components upstream. ev.stopPropagation(); + if (this.editable) { if (!this.activeActions.create) { if (data.onFail) { @@ -1308,12 +1316,14 @@ var FieldOne2Many = FieldX2Many.extend({ this._setValue({ operation: 'CREATE', position: this.editable, + context: data.context, }).always(function () { self.creatingRecord = false; }); } } else { this._openFormDialog({ + context: data.context, on_saved: function (record) { self._setValue({ operation: 'ADD', id: record.id }); }, 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 86308a32736..e0c516f02fb 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1315,13 +1315,17 @@ var BasicModel = AbstractModel.extend({ * @param {Object} [options] * @param {string} [options.position=top] if the new record should be added * on top or on bottom of the list + * @param {Object} [options.context] additional context to be merged before + * calling the default_get (eg. to set default values) * @returns {Deferred} resolves to the new record id */ _addX2ManyDefaultRecord: function (list, options) { var self = this; var position = options && options.position || 'top'; + var additionalContext = options && options.context; + var context = this._getContext(list, {additionalContext: additionalContext}); var params = { - context: this._getContext(list), + context: context, fields: list.fields, fieldsInfo: list.fieldsInfo, parentID: list.id, @@ -1799,7 +1803,10 @@ var BasicModel = AbstractModel.extend({ } break; case 'CREATE': - var options = {position: command.position}; + var options = { + context: command.context, + position: command.position, + }; def = this._addX2ManyDefaultRecord(list, options).then(function (id) { if (command.position === 'bottom' && list.orderedResIDs && list.orderedResIDs.length >= list.limit) { list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1; diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js index 0bc1b5acfce..cf67b9dd18d 100644 --- a/addons/web/static/src/js/views/field_manager_mixin.js +++ b/addons/web/static/src/js/views/field_manager_mixin.js @@ -46,7 +46,7 @@ var FieldManagerMixin = { */ _applyChanges: function (dataPointID, changes, event) { var self = this; - var options = _.pick(event.data, 'viewType', 'doNotSetDirty', 'notifyChange'); + var options = _.pick(event.data, 'context', 'doNotSetDirty', 'notifyChange', 'viewType'); return this.model.notifyChanges(dataPointID, changes, options) .then(function (result) { if (event.data.force_save) { diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index 382df0b9444..5c33a32b6d5 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -15,6 +15,7 @@ var core = require('web.core'); var dom = require('web.dom'); var ListRenderer = require('web.ListRenderer'); var utils = require('web.utils'); +var Context = require('web.Context'); var _t = core._t; @@ -37,12 +38,49 @@ ListRenderer.include({ * @param {boolean} params.addTrashIcon */ init: function (parent, state, params) { + var self = this this._super.apply(this, arguments); // if addCreateLine is true, the renderer will add a 'Add a line' link // at the bottom of the list view this.addCreateLine = params.addCreateLine; + // Controls allow overriding "add a line" by custom controls. + + // Each (only one is actually needed) is a container for (multiple) . + // Each will be a "add a line" button with custom text and context. + + // The following code will browse the arch to find + // all the that are inside + + if (this.addCreateLine) { + this.creates = []; + + _.each(this.arch.children, function (child) { + if (child.tag !== 'control') { + return; + } + + _.each(child.children, function (child) { + if (child.tag !== 'create') { + return; + } + + self.creates.push({ + 'context': child.attrs.context, + 'string': child.attrs.string, + }); + }); + }); + + // Add the default button if we didn't find any custom button. + if (this.creates.length === 0) { + this.creates.push({ + string: _t("Add a line"), + }); + } + } + // if addTrashIcon is true, there will be a small trash icon at the end // of each line, so the user can delete a record. this.addTrashIcon = params.addTrashIcon; @@ -525,15 +563,23 @@ ListRenderer.include({ * @returns {jQueryElement} */ _renderRows: function () { + var self = this; var $rows = this._super(); + if (this.addCreateLine) { - var $a = $('').text(_t("Add a line")); var $td = $('') - .attr('colspan', this._getNumberOfCols()) - .addClass('o_field_x2many_list_row_add') - .append($a); + .attr('colspan', self._getNumberOfCols()) + .addClass('o_field_x2many_list_row_add'); var $tr = $('').append($td); $rows.push($tr); + + _.each(self.creates, function (create) { + var $a = $('') + .attr('data-context', create.context) + .addClass('ml16') + .text(create.string); + $td.append($a); + }); } return $rows; }, @@ -701,20 +747,20 @@ ListRenderer.include({ * This method is called when we click on the 'Add a line' button in a sub * list such as a one2many in a form view. * - * @param {MouseEvent} event + * @param {MouseEvent} ev */ - _onAddRecord: function (event) { + _onAddRecord: function (ev) { // we don't want the browser to navigate to a the # url - event.preventDefault(); + ev.preventDefault(); // we don't want the click to cause other effects, such as unselecting // the row that we are creating, because it counts as a click on a tr - event.stopPropagation(); + ev.stopPropagation(); // but we do want to unselect current row var self = this; this.unselectRow().then(function () { - self.trigger_up('add_record'); // TODO write a test, the deferred was not considered + self.trigger_up('add_record', {context: ev.currentTarget.dataset.context}); // TODO write a test, the deferred was not considered }); }, /** diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 79304309eb6..82a08b3b27f 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -184,6 +184,9 @@ var ListRenderer = BasicRenderer.extend({ var self = this; self.handleField = null; this.columns = _.reject(this.arch.children, function (c) { + if (c.tag === 'control') { + return true; + } var reject = c.attrs.modifiers.column_invisible; // If there is an evaluated domain for the field we override the node // attribute to have the evaluated modifier value. diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 54620fcf958..2001bed1d65 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -9797,6 +9797,112 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('add a line custom control editable', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + '', + }); + + // new controls correctly added + assert.strictEqual($('.o_field_x2many_list_row_add').length, 1); + assert.strictEqual($('.o_field_x2many_list_row_add').text(), "Add foodAdd pizzaAdd pasta"); + + // click add food + // check it's empty + form.$('.o_field_x2many_list_row_add a:eq(0)').click(); + assert.strictEqual($('.o_data_cell').text(), ""); + + // click add pizza + // save the modal + // check it's pizza + form.$('.o_field_x2many_list_row_add a:eq(1)').click(); + // click add pasta + form.$('.o_field_x2many_list_row_add a:eq(2)').click(); + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual($('.o_data_cell').text(), "pizzapasta"); + + form.destroy(); + }); + + QUnit.test('add a line custom control non-editable', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + '', + }); + + // new controls correctly added + assert.strictEqual($('.o_field_x2many_list_row_add').length, 1); + assert.strictEqual($('.o_field_x2many_list_row_add').text(), "Add foodAdd pizzaAdd pasta"); + + // click add food + // check it's empty + form.$('.o_field_x2many_list_row_add a:eq(0)').click(); + $('.modal .modal-footer .btn-primary:first').click(); + assert.strictEqual($('.o_data_cell').text(), ""); + + // click add pizza + // save the modal + // check it's pizza + form.$('.o_field_x2many_list_row_add a:eq(1)').click(); + $('.modal .modal-footer .btn-primary:first').click(); + assert.strictEqual($('.o_data_cell').text(), "pizza"); + + // click add pasta + // save the whole record + // check it's pizzapasta + form.$('.o_field_x2many_list_row_add a:eq(2)').click(); + $('.modal .modal-footer .btn-primary:first').click(); + assert.strictEqual($('.o_data_cell').text(), "pizzapasta"); + + form.destroy(); + }); + QUnit.test('one2many form view with action button', function (assert) { // once the action button is clicked, the record is reloaded (via the // on_close handler, executed because the python method does not return diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 568bd7eade7..542adf7a3d5 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -308,6 +308,56 @@ Possible children elements of the list view are: :ref:`form view ` is also valid and will be used when setting up the inline form view +``control`` + defines custom controls for the current view. + + This makes sense if the parent ``tree`` view is inside a One2many field. + + Does not support any attribute, but can have children: + + ``create`` + adds a button to create a new element on the current list. + + .. note:: If any ``create`` is defined, it will overwrite the default + "add a line" button. + + The following attributes are supported: + + ``string`` (required) + The text displayed on the button. + + ``context`` + This context will be merged into the existing context + when retrieving the default value of the new record. + + For example it can be used to override default values. + + + The following example will override the default "add a line" button + by replacing it with 3 new buttons: + "Add a product", "Add a section" and "Add a note". + + "Add a product" will set the field 'display_type' to its default value. + + The two other buttons will set the field 'display_type' + to be respectively 'line_section' and 'line_note'. + + .. code-block:: xml + + + + + + + .. _reference/views/form: Forms diff --git a/odoo/addons/base/rng/common.rng b/odoo/addons/base/rng/common.rng index 3e919201786..b12e535339a 100644 --- a/odoo/addons/base/rng/common.rng +++ b/odoo/addons/base/rng/common.rng @@ -389,4 +389,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/odoo/addons/base/rng/tree_view.rng b/odoo/addons/base/rng/tree_view.rng index dc41ee5005c..068f88d909a 100644 --- a/odoo/addons/base/rng/tree_view.rng +++ b/odoo/addons/base/rng/tree_view.rng @@ -54,6 +54,7 @@ + diff --git a/odoo/tools/view_validation.py b/odoo/tools/view_validation.py index 4932b026310..014b7f107be 100644 --- a/odoo/tools/view_validation.py +++ b/odoo/tools/view_validation.py @@ -77,9 +77,9 @@ def valid_field_in_graph(arch): @validate('tree') def valid_field_in_tree(arch): - """ Children of ``tree`` view must be ``field`` or ``button``.""" + """ Children of ``tree`` view must be ``field`` or ``button`` or ``control``.""" return all( - child.tag in ('field', 'button') + child.tag in ('field', 'button', 'control') for child in arch.xpath('/tree/*') )