diff options
Diffstat (limited to 'addons/web')
17 files changed, 234 insertions, 99 deletions
diff --git a/addons/web/models/models.py b/addons/web/models/models.py index 572f07333d2..2e3bd54788d 100644 --- a/addons/web/models/models.py +++ b/addons/web/models/models.py @@ -40,11 +40,14 @@ class Base(models.AbstractModel): records_values = self.search_read(domain or [], [progress_bar['field'], group_by]) data = {} + field_type = self._fields[group_by].type + if field_type == 'selection': + selection_labels = dict(self.fields_get()[group_by]['selection']) + for record_values in records_values: group_by_value = record_values[group_by] # Again, imitating what _read_group_format_result and _read_group_prepare_data do - field_type = self._fields[group_by].type if group_by_value and field_type in ['date', 'datetime']: locale = self._context.get('lang') or 'en_US' group_by_value = fields.Datetime.to_datetime(group_by_value) @@ -61,7 +64,7 @@ class Base(models.AbstractModel): locale=locale) if field_type == 'selection': - group_by_value = dict(self.fields_get()[group_by]['selection'])[group_by_value] + group_by_value = selection_labels[group_by_value] if type(group_by_value) == tuple: group_by_value = group_by_value[1] # FIXME should use technical value (0) diff --git a/addons/web/static/src/js/chrome/abstract_action.js b/addons/web/static/src/js/chrome/abstract_action.js index 5b0f7e3aeca..c4755338bf0 100644 --- a/addons/web/static/src/js/chrome/abstract_action.js +++ b/addons/web/static/src/js/chrome/abstract_action.js @@ -32,6 +32,13 @@ var AbstractAction = Widget.extend({ * Called each time the action is detached from the DOM. */ on_detach_callback: function () {}, + /** + * Called by the action manager when action is restored (typically, when the + * user clicks on the action in the breadcrumb) + * + * @returns {Deferred|undefined} + */ + willRestore: function () {}, //-------------------------------------------------------------------------- // Public diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 7b87cb45dd3..e66ae801bc1 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -646,8 +646,10 @@ ActionManager.include({ }); } else { viewOptions = _.extend(viewOptions || {}, action.env); - return controller.widget.reload(viewOptions).then(function () { - return controller; + return $.when(controller.widget.willRestore()).then(function () { + return controller.widget.reload(viewOptions).then(function () { + return controller; + }); }); } }); diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 725658a351b..2a961596862 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -397,9 +397,10 @@ return { if (options.maxWidth) { maxWidth = options.maxWidth(); } else { - var rect = $el[0].getBoundingClientRect(); + var mLeft = $el.is('.ml-auto, .mx-auto, .m-auto'); + var mRight = $el.is('.mr-auto, .mx-auto, .m-auto'); + maxWidth = computeFloatOuterWidthWithMargins($el[0], mLeft, mRight); var style = window.getComputedStyle($el[0]); - maxWidth = (rect.right - rect.left); maxWidth -= (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth)); maxWidth -= _.reduce($unfoldableItems, function (sum, el) { return sum + computeFloatOuterWidthWithMargins(el); @@ -434,10 +435,17 @@ return { $extraItemsToggle.find('.nav-link').toggleClass('active', $extraItems.children().hasClass('active')); } - function computeFloatOuterWidthWithMargins(el) { + function computeFloatOuterWidthWithMargins(el, mLeft, mRight) { var rect = el.getBoundingClientRect(); var style = window.getComputedStyle(el); - return rect.right - rect.left + parseFloat(style.marginLeft) + parseFloat(style.marginRight); + var outerWidth = rect.right - rect.left; + if (mLeft !== false) { + outerWidth += parseFloat(style.marginLeft); + } + if (mRight !== false) { + outerWidth += parseFloat(style.marginRight); + } + return outerWidth; } }, /** 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 0aff337dcbf..183885b9fb8 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -264,7 +264,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { resIDs: record.res_ids, }, on_success: def.resolve.bind(def), - on_fail: def.reject.bind(def), + on_fail: function () { + self.update({}, {reload: false}).always(def.reject.bind(def)); + }, on_closed: reload, }); return this.alive(def); diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 7587e52fcd0..9c354be6765 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -49,6 +49,15 @@ var FormController = BasicController.extend({ this._super.apply(this, arguments); this.autofocus(); }, + /** + * Force mode back to readonly. Whenever we leave a form view, it is saved, + * and should no longer be in edit mode. + * + * @override + */ + willRestore: function () { + this.mode = 'readonly'; + }, //-------------------------------------------------------------------------- // Public @@ -241,7 +250,7 @@ var FormController = BasicController.extend({ * @override */ update: function (params, options) { - params = _.extend({viewType: 'form'}, params); + params = _.extend({viewType: 'form', mode: this.mode}, params); return this._super(params, options); }, diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js index 09992695506..27bd59e476d 100644 --- a/addons/web/static/src/js/views/graph/graph_controller.js +++ b/addons/web/static/src/js/views/graph/graph_controller.js @@ -179,7 +179,6 @@ var GraphController = AbstractController.extend(GroupByMenuInterfaceMixin,{ */ _onButtonClick: function (ev) { var $target = $(ev.target); - var parent; var field; if ($target.hasClass('o_graph_button')) { this._setMode($target.data('mode')); diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js index adefe1e8953..26c1aec6545 100644 --- a/addons/web/static/src/js/views/graph/graph_model.js +++ b/addons/web/static/src/js/views/graph/graph_model.js @@ -6,11 +6,8 @@ odoo.define('web.GraphModel', function (require) { * server. It basically just do a read_group and format/normalize data. */ -var core = require('web.core'); var AbstractModel = require('web.AbstractModel'); -var _t = core._t; - return AbstractModel.extend({ /** * @override diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index fd21ed4df86..bfb6f5e0cc9 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -305,7 +305,7 @@ var KanbanModel = BasicModel.extend({ * @override */ _load: function (dataPoint, options) { - if (dataPoint.progressBar) { + if (dataPoint.groupedBy.length && dataPoint.progressBar) { return this._readProgressBarGroup(dataPoint, options); } return this._super.apply(this, arguments); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index d6ea10d1243..ee11afbc3bd 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -186,9 +186,12 @@ var FormViewDialog = ViewDialog.extend({ } fields_view_def.then(function (viewInfo) { + var refinedContext = _.pick(self.context, function (value, key) { + return key.indexOf('_view_ref') === -1; + }); var formview = new FormView(viewInfo, { modelName: self.res_model, - context: self.context, + context: refinedContext, ids: self.res_id ? [self.res_id] : [], currentId: self.res_id || undefined, index: 0, diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 9233229a984..7424d31f365 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -2878,6 +2878,31 @@ QUnit.module('ActionManager', { actionManager.destroy(); }); + QUnit.test('form views are restored in readonly when coming back in breadcrumbs', function (assert) { + assert.expect(2); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(3); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + // switch to edit mode + $('.o_control_panel .o_form_button_edit').click(); + + assert.ok(actionManager.$('.o_form_view').hasClass('o_form_editable')); + // do some other action + actionManager.doAction(4); + // go back to form view + $('.o_control_panel .breadcrumb a').last().click(); + assert.ok(actionManager.$('.o_form_view').hasClass('o_form_readonly')); + + actionManager.destroy(); + }); + QUnit.test('honor group_by specified in actions context', function (assert) { assert.expect(5); diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index cb864afdf7e..f1640b06c82 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1247,10 +1247,7 @@ QUnit.module('basic_fields', { form.destroy(); }); - QUnit.skip('input field: change password value', function (assert) { - // password policy needs an RPC call to initialize & - // presents somewhat differently (custom widget), need way - // to augment/override tests + QUnit.test('input field: change password value', function (assert) { assert.expect(4); var form = createView({ @@ -1278,7 +1275,7 @@ QUnit.module('basic_fields', { form.destroy(); }); - QUnit.skip('input field: empty password', function (assert) { + QUnit.test('input field: empty password', function (assert) { assert.expect(3); this.data.partner.records[0].foo = false; diff --git a/addons/web/static/tests/qweb_tests.js b/addons/web/static/tests/qweb_tests.js index 6e40dd1b1cb..b31c604b4bf 100644 --- a/addons/web/static/tests/qweb_tests.js +++ b/addons/web/static/tests/qweb_tests.js @@ -49,9 +49,10 @@ function loadTest(assert, template, context) { var args = params ? JSON.parse(params.textContent) : (context ? _.clone(context) : {}); var results = doc.querySelector('result#' + template); + assert.equal( trim(qweb.render(template, args)), - trim(results.textContent), + trim(results.textContent.replace(new RegExp(String.fromCharCode(13), 'g'), '')), template); } catch (error) { assert.notOk(error.stack || error, 'Rendering error'); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index fce865ac29f..ada27d09afc 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5993,10 +5993,7 @@ QUnit.module('Views', { form.destroy(); }); - QUnit.skip('support password attribute', function (assert) { - // password policy needs an RPC call to initialize & - // presents somewhat differently (custom widget), need way - // to augment/override tests + QUnit.test('support password attribute', function (assert) { assert.expect(3); var form = createView({ @@ -6989,6 +6986,69 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('keep editing after call_button fail', function (assert) { + assert.expect(4); + + var values; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form>' + + '<button name="post" class="p" string="Raise Error" type="object"/>' + + '<field name="p">' + + '<tree editable="top">' + + '<field name="display_name"/>' + + '<field name="product_id"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + intercepts: { + execute_action: function (ev) { + assert.ok(true, 'the action is correctly executed'); + ev.data.on_fail(); + }, + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.deepEqual(args.args[1].p[0][2], values); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + mode: 'edit', + }, + }); + + // add a row and partially fill it + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input:nth(0)').val('abc').trigger('input'); + + // click button which will trigger_up 'execute_action' (this will save) + values = { + display_name: 'abc', + product_id: false, + }; + form.$('button.p').click(); + + // edit the new row again and set a many2one value + form.$('.o_form_view .o_field_one2many .o_data_row .o_data_cell').click(); + var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); + form.$('.o_field_many2one input').click(); + $dropdown.find('li:first()').click(); + + assert.strictEqual(form.$('.o_field_many2one input').val(), 'xphone', + "value of the m2o should have been correctly updated"); + + values = { + product_id: 37, + }; + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('asynchronous rendering of a widget tag', function (assert) { assert.expect(1); @@ -7286,7 +7346,6 @@ QUnit.module('Views', { '</sheet>' + '</form>', res_id: 2, - //debug:1, }); assert.strictEqual(form.$buttons.find('.o_form_button_edit')[0],document.activeElement, "in read mode, when there are no primary buttons on the form, the default button with the focus should be edit"); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 305dda05701..435c13cd3f1 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -4364,6 +4364,37 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('column progressbars should not crash in non grouped views', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: + '<kanban>' + + '<field name="bar"/>' + + '<field name="int_field"/>' + + '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="name"/>' + + '</div>' + + '</t></templates>' + + '</kanban>', + mockRPC: function (route, args) { + assert.step(route) + return this._super(route, args); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_record').text(), 'namenamenamename', + "should have renderer 4 records"); + + assert.verifySteps(['/web/dataset/search_read'], "no read on progress bar data is done"); + kanban.destroy(); + }); + QUnit.test('column progressbars: creating a new column should create a new progressbar', function (assert) { assert.expect(1); @@ -4459,8 +4490,7 @@ QUnit.module('Views', { kanban.destroy(); }); - // XXX test deactivated in saas~11.2 as kanban archiving has been removed (and readded in saas~11.4) - QUnit.skip('column progressbars on archiving records update counter', function (assert) { + QUnit.test('column progressbars on archiving records update counter', function (assert) { assert.expect(4); // add active field on partner model and make all records active @@ -4491,7 +4521,9 @@ QUnit.module('Views', { "the counter progressbars should be correctly displayed"); // archive all records of the second columns - kanban.$('.o_kanban_group:eq(1) .o_column_archive').click(); + kanban.$('.o_kanban_group:eq(1) .o_kanban_config a.o-no-caret').click(); + kanban.$('.o_column_archive_records:visible').click(); + $('.modal-footer button:first').click(); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "0", "counter should contain the correct value"); @@ -4501,7 +4533,7 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.skip('kanban with progressbars: correctly update env when archiving records', function (assert) { + QUnit.test('kanban with progressbars: correctly update env when archiving records', function (assert) { assert.expect(3); // add active field on partner model and make all records active @@ -4526,13 +4558,15 @@ QUnit.module('Views', { groupBy: ['bar'], intercepts: { env_updated: function (ev) { - assert.step(ev.data.ids); + assert.step(ev.data.env.ids); }, }, }); // archive all records of the first column - kanban.$('.o_kanban_group:first .o_column_archive').click(); + kanban.$('.o_kanban_group:first .o_kanban_config a.o-no-caret').click(); + kanban.$('.o_column_archive_records:visible').click(); + $('.modal-footer button:first').click(); assert.verifySteps([ [1, 2, 3, 4], diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 4540b34c725..030a676f4eb 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -2849,72 +2849,6 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.skip('navigation: moving left/right with keydown', function (assert) { - assert.expect(8); - - this.data.foo.fields.foo.type = 'text'; - var list = createView({ - View: ListView, - model: 'foo', - data: this.data, - arch: - '<tree editable="bottom">' + - '<field name="m2m" widget="many2many_tags"/>' + - '<field name="foo"/>' + - '<field name="bar"/>' + - '<field name="m2o"/>' + - '<field name="qux"/>' + - '</tree>', - }); - - list.$('td:contains(13)').click(); - var $m2m = list.$('[name="m2m"] input'); - var $foo = list.$('textarea[name="foo"]'); - var $bar = list.$('[name="bar"] input'); - var $m2o = list.$('[name="m2o"] input'); - var $qux = list.$('input[name="qux"]'); - - assert.strictEqual(document.activeElement, $qux[0], - "'qux' input should be focused"); - - $qux[0].selectionEnd = 0; // Simulate browser keyboard left behavior (unselect) - $qux.trigger({type: 'keydown', which: $.ui.keyCode.LEFT}); - assert.strictEqual(document.activeElement, $m2o[0], - "'m2o' input should be focused"); - - // forget unselecting and try leaving - $m2o.trigger({type: 'keydown', which: $.ui.keyCode.LEFT}); - assert.strictEqual(document.activeElement, $m2o[0], - "'m2o' input should still be focused"); - - $m2o[0].selectionEnd = 0; // Simulate browser keyboard left behavior (unselect) - $m2o.trigger({type: 'keydown', which: $.ui.keyCode.LEFT}); - assert.strictEqual(document.activeElement, $bar[0], - "'bar' input should be focused"); - - // no unselect here as it is a checkbox - $bar.trigger({type: 'keydown', which: $.ui.keyCode.LEFT}); - assert.strictEqual(document.activeElement, $foo[0], - "'foo' input should be focused"); - - // forget unselecting and try leaving - $foo.trigger({type: 'keydown', which: $.ui.keyCode.LEFT}); - assert.strictEqual(document.activeElement, $foo[0], - "'foo' input should still be focused"); - - $foo[0].selectionEnd = 0; // Simulate browser keyboard left behavior (unselect) - $foo.trigger({type: 'keydown', which: $.ui.keyCode.LEFT}); - assert.strictEqual(document.activeElement, $m2m[0], - "'m2m' input should be focused"); - - $m2m[0].selectionStart = $m2m[0].value.length; // Simulate browser keyboard right behavior (unselect) - $m2m.trigger({type: 'keydown', which: $.ui.keyCode.RIGHT}); - assert.strictEqual(document.activeElement, $foo[0], - "'foo' input should be focused"); - - list.destroy(); - }); - QUnit.test('discarding changes in a row properly updates the rendering', function (assert) { assert.expect(3); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index b45c27e4d60..372edbc0f24 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -364,6 +364,61 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('Form dialog and subview with _view_ref contexts', function (assert) { + assert.expect(2); + + this.data.instrument.records = [{id: 1, name: 'Tromblon', badassery: [1]}]; + this.data.partner.records[0].instrument = 1; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="name"/>' + + '<field name="instrument" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' + + '</form>', + res_id: 1, + archs: { + 'instrument,false,form': '<form>'+ + '<field name="name"/>'+ + '<field name="badassery" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' + + '</form>', + + 'badassery,false,list': '<tree>'+ + '<field name="level"/>'+ + '</tree>', + }, + viewOptions: { + mode: 'edit', + }, + + mockRPC: function(route, args) { + if (args.method === 'get_formview_id') { + return $.when(false); + } + return this._super(route, args); + }, + + interceptsPropagate: { + load_views: function (ev) { + var evaluatedContext = ev.data.context; + if (ev.data.modelName === 'instrument') { + assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_tree_view'}, + 'The correct _view_ref should have been sent to the server, first time'); + } + if (ev.data.modelName === 'badassery') { + assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_other_tree_view'}, + 'The correct _view_ref should have been sent to the server for the subview'); + } + }, + }, + }); + + form.$('.o_field_widget[name="instrument"] button.o_external_button').click(); + form.destroy(); + }); + QUnit.test('SelectCreateDialog: save current search', function (assert) { assert.expect(4); |
