From 5f7030fc446c30f7996b32c9ffed006882a31d42 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 22 Aug 2017 14:37:50 +0200 Subject: [PATCH 001/119] [FIX] stock: move name length - Create a stockable product with a long description (> 3000 characters) - Validate the SO An error is raised because the stock move name is too long. The stock move name is taken from the procurement name, which is itself taken from the SO line description. However, the stock move name is an indexed field and therefore its length is limited (to 2712 characters by default). We truncate the move name to 2000 characters. Actually, there is no real point to so many characters, nor its indexing. opw-767040 --- addons/stock/models/procurement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index c1ea59f239a..4d3e78aa75c 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -151,7 +151,7 @@ class ProcurementOrder(models.Model): qty_done = sum(self.move_ids.filtered(lambda move: move.state == 'done').mapped('product_uom_qty')) qty_left = max(self.product_qty - qty_done, 0) return { - 'name': self.name, + 'name': self.name[:2000], 'company_id': self.rule_id.company_id.id or self.rule_id.location_src_id.company_id.id or self.rule_id.location_id.company_id.id or self.company_id.id, 'product_id': self.product_id.id, 'product_uom': self.product_uom.id, From 388e258ce8c2a4fbd5e2ec5ec746bb91ca48d97f Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 21 Dec 2016 16:35:09 +0100 Subject: [PATCH 002/119] [FIX] web: rotate graph label When there are too many labels to display on the x-axis, they overlap and it simply becomes unreadable. By slightly rotating them, this can be avoided in most cases. opw-697625 opw-697626 --- addons/web/static/src/js/views/graph_widget.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views/graph_widget.js b/addons/web/static/src/js/views/graph_widget.js index db6418fd15a..2aa98965329 100644 --- a/addons/web/static/src/js/views/graph_widget.js +++ b/addons/web/static/src/js/views/graph_widget.js @@ -166,7 +166,7 @@ return Widget.extend({ var chart = nv.models.multiBarChart(); var maxVal = _.max(values, function(v) {return v.y}) chart.options({ - margin: {left: 12 * String(maxVal && maxVal.y || 10000000).length}, + margin: {left: 12 * String(maxVal && maxVal.y || 10000000).length, bottom: 60}, delay: 250, transition: 10, showLegend: _.size(data) <= MAX_LEGEND_LENGTH, @@ -175,7 +175,7 @@ return Widget.extend({ rightAlignYAxis: false, stacked: this.stacked, reduceXTicks: false, - // rotateLabels: 40, + rotateLabels: -20, showControls: (this.groupbys.length > 1) }); chart.yAxis.tickFormat(function(d) { return formats.format_value(d, { type : 'float' });}); From ffd43386393f493cd098339c9cb3f812dea1dd5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 23 Aug 2017 10:56:57 +0200 Subject: [PATCH 003/119] [FIX] web_kanban: allow special chaaracters in kanban tooltip --- addons/web_kanban/static/src/js/kanban_column.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/web_kanban/static/src/js/kanban_column.js b/addons/web_kanban/static/src/js/kanban_column.js index f848c266944..b4fa2e8d010 100644 --- a/addons/web_kanban/static/src/js/kanban_column.js +++ b/addons/web_kanban/static/src/js/kanban_column.js @@ -65,9 +65,10 @@ var KanbanColumn = Widget.extend({ }); var self = this; - if (group_data.options && group_data.options.group_by_tooltip) { - this.tooltip_info = _.map(group_data.options.group_by_tooltip, function (key, value, list) { - return (self.values && self.values[value] && "
" +key + "
" + self.values[value] + "
") || ''; + if (group_data.options && group_data.options.group_by_tooltip && this.values) { + this.tooltip_info = _.map(group_data.options.group_by_tooltip, function (key, value) { + if (!self.values[value]) { return ''; } + return $('
').text(key + '
' + self.values[value]).text(); }).join(''); } else { this.tooltip_info = ""; From 6797d6a7c639c22f9987207481accee9b9b5b486 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Tue, 22 Aug 2017 13:41:12 +0200 Subject: [PATCH 004/119] [FIX] web: don't save empty required x2manys Before this commit, the same function was called at the rendering and at the writing of a field. It caused a field that should be displayed all the time (field is never false) to ignore its required attribute, in particular with one2many's. After this commit, we differentiate between the rendering function and the validate to write function, making a type of field both requirable and always displayed even if empty OPW 766002 Closes odoo/enterprise#1306 Cherry-pick odoo/enterprise@be8e5709918b42e6c60fe0327329f855a29f5a99 --- addons/web/static/src/js/views/form_common.js | 13 ++++++++++++- .../static/src/js/views/form_relational_widgets.js | 12 ++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js index 6752885f372..3ed2e77e695 100644 --- a/addons/web/static/src/js/views/form_common.js +++ b/addons/web/static/src/js/views/form_common.js @@ -716,7 +716,7 @@ var AbstractField = FormWidget.extend(FieldInterface, { } }, _toggle_label: function() { - var empty = this.get('effective_readonly') && this.is_false(); + var empty = this.get('effective_readonly') && !this.is_set(); this.$label.toggleClass('o_form_label_empty', empty).toggleClass('o_form_label_false', this.get('effective_readonly') && this.get('value') === false); this.$el.toggleClass('o_form_field_empty', empty); }, @@ -755,10 +755,21 @@ var AbstractField = FormWidget.extend(FieldInterface, { /** * Method useful to implement to ease validity testing. Must return true if the current * value is similar to false in OpenERP. + * Used at write time, in particular when the field is required */ is_false: function() { return this.get('value') === false; }, + + /** + * Method called at rendering time to determine if the field should be + * displayed (non-empty) or not (empty) + * We test the same thing as is_false but with different semantics + */ + is_set: function() { + return !this.is_false(); + }, + _check_css_flags: function() { var show_translate = (!this.get('effective_readonly') && this.field_manager.get('actual_mode') !== "create"); this.$translate.toggleClass('o_translate_active', !!show_translate); diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index 9a24eef6ae5..722ef2f4932 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -345,8 +345,8 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com if (!this.get("effective_readonly")) { this.$input.val(noValue ? "" : (str.split("\n")[0].trim() || $(data.noDisplayContent).text())); this.current_display = this.$input.val(); - this.$follow_button.toggle(!this.is_false()); - this.$el.toggleClass('o_with_button', !!this.$follow_button && this.$follow_button.length > 0 && !this.is_false()); + this.$follow_button.toggle(this.is_set()); + this.$el.toggleClass('o_with_button', !!this.$follow_button && this.$follow_button.length > 0 && this.is_set()); } else { this.$el.html(noValue ? "" : (_.escape(str.trim()).split("\n").join("
") || data.noDisplayContent)); // Define callback to perform when clicking on the field @@ -871,6 +871,10 @@ var FieldX2Many = AbstractManyField.extend({ is_false: function () { return _(this.dataset.ids).isEmpty(); }, + is_set: function() { + // always consider that field is "set" hence displayed + return true; + }, }); var X2ManyDataSet = data.BufferedDataSet.extend({ @@ -1697,6 +1701,10 @@ var FieldMany2ManyCheckBoxes = AbstractManyField.extend(common.ReinitializeField is_false: function() { return false; }, + is_set: function() { + // always consider that field is "set" hence displayed + return true; + }, }); core.form_widget_registry From 9214d7815267d6bdad954f7e94a4bf187cea35bc Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 23 Aug 2017 17:59:04 +0200 Subject: [PATCH 005/119] [FIX] board: height of graph When a graph is added to the dashboard, it doesn't have a minimum height set, so only the label and the axis are displayed, not the content. opw-766454 --- addons/board/static/src/less/dashboard.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/board/static/src/less/dashboard.less b/addons/board/static/src/less/dashboard.less index 36f0727c0ac..855da1d2acc 100644 --- a/addons/board/static/src/less/dashboard.less +++ b/addons/board/static/src/less/dashboard.less @@ -114,6 +114,11 @@ .o_kanban_view .o_kanban_record .oe_kanban_list_many2many .o_form_field_many2manytags { border: none; } + + // Override height of graph. min-height doesn't do the trick + .o_graph { + height: 500px; + } } } } From acb9d60d944f21606ee4a9decb752a7a24c65d9d Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 22 Aug 2017 08:45:52 +0200 Subject: [PATCH 006/119] [FIX] web: Pivot: handle __count__ measure The former implementation of the Pivot view used __count__ as magic field to display the number of records in groups. However, in filters defined in xml, __count is used instead of __count__, so it crashed when such filters were activated. With the new views, we made those filters work by using __count. However, if a user saved filters using __count__ at that time as favorite, activating them in the new views produces a crash as well. This rev. adds a compatibility layer to ensure that the pivot view handles both __count__ and __count measures uniformly. --- .../static/src/js/views/pivot/pivot_model.js | 13 ++++++++- addons/web/static/tests/views/pivot_tests.js | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index d74af7fee22..13275926c02 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -222,6 +222,17 @@ var PivotModel = AbstractModel.extend({ * @returns {Deferred} */ load: function (params) { + // In the preview implementation of the pivot view (a.k.a. version 2), + // the virtual field used to display the number of records was named + // __count__, whereas __count is actually the one used in xml. So + // basically, activating a filter specifying __count as measures crashed. + // Unfortunately, as __count__ was used in the JS, all filters saved as + // favorite at that time were saved with __count__, and not __count. + // So in order the make them still work with the new implementation, we + // simply rename __count__ into __count at this unique entry point. + var measures = _.map(params.measures, function (measure) { + return measure === '__count__' ? '__count' : measure; + }); this.initialDomain = params.domain; this.initialRowGroupBys = params.rowGroupBys; this.fields = params.fields; @@ -231,7 +242,7 @@ var PivotModel = AbstractModel.extend({ context: _.extend({}, session.user_context, params.context), groupedBy: params.groupedBy, colGroupBys: params.colGroupBys, - measures: params.measures, + measures: measures, sorted_column: {}, }; this.defaultGroupedBy = params.groupedBy; diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 267437845b0..eb2b18f7d71 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -859,4 +859,33 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('pivot still handles __count__ measure', function (assert) { + // for retro-compatibility reasons, the pivot view still handles + // '__count__' measure. + assert.expect(2); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '', + mockRPC: function (route, args) { + if (args.method === 'read_group') { + assert.deepEqual(args.kwargs.fields, ['__count'], + "should make a read_group with field __count"); + } + return this._super(route, args); + }, + viewOptions: { + context: { + pivot_measures: ['__count__'], + }, + }, + }); + + var $countMeasure = pivot.$buttons.find('li[data-field=__count]'); + assert.ok($countMeasure.hasClass('selected'), "The count measure should be activated"); + + pivot.destroy(); + }); });}); From 42f22a21614ee7967b900736001b8e68b8ad756e Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 22 Aug 2017 14:33:25 +0200 Subject: [PATCH 007/119] [FIX] web: read context when reloading Graph or Pivot views Before this rev., keys like pivot_measures, graph_mode..., were correctly took into account at first load of Pivot or Graph view, but not at reload. Those keys are particularily useful when a user saves a research to its favorites. When such a favorite is selected, the view is reloaded and the interesting information is in the context. --- .../src/js/views/graph/graph_controller.js | 10 ++++ .../static/src/js/views/graph/graph_model.js | 15 ++++-- .../static/src/js/views/graph/graph_view.js | 8 ++-- .../static/src/js/views/pivot/pivot_model.js | 48 +++++++++++++------ .../static/src/js/views/pivot/pivot_view.js | 6 +-- addons/web/static/tests/views/graph_tests.js | 46 ++++++++++++++++++ addons/web/static/tests/views/pivot_tests.js | 47 ++++++++++++++++++ 7 files changed, 154 insertions(+), 26 deletions(-) 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 d68808bac1d..77e3f1e7a40 100644 --- a/addons/web/static/src/js/views/graph/graph_controller.js +++ b/addons/web/static/src/js/views/graph/graph_controller.js @@ -101,11 +101,21 @@ var GraphController = AbstractController.extend({ self._updateButtons(); }); }, + /** + * @private + */ + _update: function () { + this._updateButtons(); + return this._super.apply(this, arguments); + }, /** * makes sure that the buttons in the control panel matches the current * state (so, correct active buttons and stuff like that) */ _updateButtons: function () { + if (!this.$buttons) { + return; + } var state = this.model.get(); this.$buttons.find('.o_graph_button').removeClass('active'); this.$buttons 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 50e9f224697..a6d2b057d0e 100644 --- a/addons/web/static/src/js/views/graph/graph_model.js +++ b/addons/web/static/src/js/views/graph/graph_model.js @@ -55,14 +55,15 @@ return AbstractModel.extend({ * to keep track of various entities. */ load: function (params) { - this.initialGroupBys = params.groupBys; + var groupBys = params.context.graph_groupbys || params.groupBys; + this.initialGroupBys = groupBys; this.fields = params.fields; this.modelName = params.modelName; this.chart = { data: [], - groupedBy: params.groupedBy.length ? params.groupedBy : params.groupBys, - measure: params.measure, - mode: params.mode, + groupedBy: params.groupedBy.length ? params.groupedBy : groupBys, + measure: params.context.graph_measure || params.measure, + mode: params.context.graph_mode || params.mode, domain: params.domain, context: params.context, }; @@ -85,6 +86,12 @@ return AbstractModel.extend({ * @returns {Deferred} */ reload: function (handle, params) { + if ('context' in params) { + this.chart.context = params.context; + this.chart.groupedBy = params.context.graph_groupbys || this.chart.groupedBy; + this.chart.measure = params.context.graph_measure || this.chart.measure; + this.chart.mode = params.context.graph_mode || this.chart.mode; + } if ('domain' in params) { this.chart.domain = params.domain; } diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js index 08b5ce1968f..423d52ca64c 100644 --- a/addons/web/static/src/js/views/graph/graph_view.js +++ b/addons/web/static/src/js/views/graph/graph_view.js @@ -27,7 +27,7 @@ var GraphView = AbstractView.extend({ /** * @override */ - init: function (viewInfo, params) { + init: function (viewInfo) { this._super.apply(this, arguments); var measure; @@ -57,9 +57,9 @@ var GraphView = AbstractView.extend({ this.controllerParams.measures = measures; this.rendererParams.stacked = viewInfo.arch.attrs.stacked !== "False"; - this.loadParams.mode = params.context.graph_mode || viewInfo.arch.attrs.type || 'bar'; - this.loadParams.measure = params.context.graph_measure || measure || '__count__'; - this.loadParams.groupBys = params.context.graph_groupbys || groupBys || []; + this.loadParams.mode = viewInfo.arch.attrs.type || 'bar'; + this.loadParams.measure = measure || '__count__'; + this.loadParams.groupBys = groupBys || []; this.loadParams.fields = viewInfo.fields; }, }); diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 13275926c02..7eb2ba4bc6b 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -222,27 +222,16 @@ var PivotModel = AbstractModel.extend({ * @returns {Deferred} */ load: function (params) { - // In the preview implementation of the pivot view (a.k.a. version 2), - // the virtual field used to display the number of records was named - // __count__, whereas __count is actually the one used in xml. So - // basically, activating a filter specifying __count as measures crashed. - // Unfortunately, as __count__ was used in the JS, all filters saved as - // favorite at that time were saved with __count__, and not __count. - // So in order the make them still work with the new implementation, we - // simply rename __count__ into __count at this unique entry point. - var measures = _.map(params.measures, function (measure) { - return measure === '__count__' ? '__count' : measure; - }); this.initialDomain = params.domain; - this.initialRowGroupBys = params.rowGroupBys; + this.initialRowGroupBys = params.context.pivot_row_groupby || params.rowGroupBys; this.fields = params.fields; this.modelName = params.modelName; this.data = { domain: params.domain, context: _.extend({}, session.user_context, params.context), groupedBy: params.groupedBy, - colGroupBys: params.colGroupBys, - measures: measures, + colGroupBys: params.context.pivot_column_groupby || params.colGroupBys, + measures: this._processMeasures(params.context.pivot_measures) || params.measures, sorted_column: {}, }; this.defaultGroupedBy = params.groupedBy; @@ -256,6 +245,12 @@ var PivotModel = AbstractModel.extend({ */ reload: function (handle, params) { var self = this; + if ('context' in params) { + this.data.context = params.context; + this.data.colGroupBys = params.context.pivot_column_groupby || this.data.colGroupBys; + this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy; + this.data.measures = this._processMeasures(params.context.pivot_measures) || this.data.measures; + } if ('domain' in params) { this.data.domain = params.domain; } else { @@ -272,7 +267,7 @@ var PivotModel = AbstractModel.extend({ var old_col_root = this.data.main_col.root; return this._loadData().then(function () { var new_groupby_length; - if (!('groupBy' in params)) { + if (!('groupBy' in params) && !('pivot_row_groupby' in (params.context || {}))) { // we only update the row groupbys according to the old groupbys // if we don't have the key 'groupBy' in params. In that case, // we want to have the full open state for the groupbys. @@ -659,6 +654,29 @@ var PivotModel = AbstractModel.extend({ this.data.main_row.root = main_row_header; this.data.main_col.root = main_col_header; }, + /** + * In the preview implementation of the pivot view (a.k.a. version 2), + * the virtual field used to display the number of records was named + * __count__, whereas __count is actually the one used in xml. So + * basically, activating a filter specifying __count as measures crashed. + * Unfortunately, as __count__ was used in the JS, all filters saved as + * favorite at that time were saved with __count__, and not __count. + * So in order the make them still work with the new implementation, we + * handle both __count__ and __count. + * + * This function replaces in the given array of measures occurences of + * '__count__' by '__count'. + * + * @param {Array[string] || undefined} measures + * @return {Array[string] || undefined} + */ + _processMeasures: function (measures) { + if (measures) { + return _.map(measures, function (measure) { + return measure === '__count__' ? '__count' : measure; + }); + } + }, /** * Format a value to a usable string, for the renderer to display. * diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index 356672081d9..7e4dddc4a52 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -87,9 +87,9 @@ var PivotView = AbstractView.extend({ activeMeasures.push('__count'); } - this.loadParams.measures = params.context.pivot_measures || activeMeasures; - this.loadParams.colGroupBys = params.context.pivot_column_groupby || colGroupBys; - this.loadParams.rowGroupBys = params.context.pivot_row_groupby || rowGroupBys; + this.loadParams.measures = activeMeasures; + this.loadParams.colGroupBys = colGroupBys; + this.loadParams.rowGroupBys = rowGroupBys; this.loadParams.fields = fields; this.controllerParams.title = params.title || arch.attrs.string || _t("Untitled"); diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js index c7d69b194ff..07f7d060347 100644 --- a/addons/web/static/tests/views/graph_tests.js +++ b/addons/web/static/tests/views/graph_tests.js @@ -372,6 +372,52 @@ QUnit.module('Views', { }); }); + QUnit.test('correctly uses graph_ keys from the context (at reload)', function (assert) { + var done = assert.async(); + assert.expect(8); + + var graph = createView({ + View: GraphView, + model: "foo", + data: this.data, + arch: '', + }); + + assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode"); + assert.ok(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'), + 'bar chart button should be active'); + + var reloadParams = { + context: { + graph_measure: 'foo', + graph_mode: 'line', + graph_groupbys: ['color_id'], + }, + }; + graph.reload(reloadParams); + return concurrency.delay(0).then(function () { + // check measure + assert.strictEqual(graph.$('text.nv-legend-text:contains(Foo)').length, 1, + "should now use the 'foo' measure"); + + // check mode + assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode"); + assert.notOk(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'), + 'bar chart button should not be active'); + assert.ok(graph.$buttons.find('button[data-mode="line"]').hasClass('active'), + 'line chart button should be active'); + + // check groupbys + assert.strictEqual(graph.$('text:contains(xphone)').length, 0, + "should not contain a text element with product in legend"); + assert.strictEqual(graph.$('text:contains(red)').length, 1, + "should contain a text element with color in legend"); + + graph.destroy(); + done(); + }); + }); + QUnit.test('reload graph with correct fields', function (assert) { assert.expect(2); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index eb2b18f7d71..fe8c4c5b3f9 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -825,6 +825,53 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('correctly uses pivot_ keys from the context (at reload)', function (assert) { + assert.expect(8); + + this.data.partner.fields.amount = {string: "Amount", type: "float"}; + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '' + + '' + + '' + + '', + }); + + assert.strictEqual(pivot.$('tbody tr:first td.o_pivot_cell_value:last').text(), '0.00', + "the active measure should be amount"); + + var reloadParams = { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }; + pivot.reload(reloadParams); + + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + "column: should have one opened header"); + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed:contains(First)').length, 1, + "column: should display one closed header with 'First'"); + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed:contains(Second)').length, 1, + "column: should display one closed header with 'Second'"); + + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + "row: should have one opened header"); + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed:contains(xphone)').length, 1, + "row: should display one closed header with 'xphone'"); + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed:contains(xpad)').length, 1, + "row: should display one closed header with 'xpad'"); + + assert.strictEqual(pivot.$('tbody tr:first td:nth(3)').text(), '32', + "selected measure should be foo, with total 32"); + + pivot.destroy(); + }); + QUnit.test('correctly use group_by key from the context', function (assert) { assert.expect(7); From 6e4b59cc9cf2ede2ff54579d0cb672210f2b80ab Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 23 Aug 2017 16:10:44 +0200 Subject: [PATCH 008/119] [FIX] web: Form: display button box in create mode Before this rev., the button box wasn't displayed on new (not saved yet) records. This wasn't the case before the new views. This is a problem in lunch, as the lunch.order form view is opened in create mode, and the balance is displayed in the button box. --- addons/web/static/src/js/views/form/form_renderer.js | 4 ---- addons/web/static/tests/views/form_tests.js | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index 2ef0f077e09..999006e54c2 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -246,10 +246,6 @@ var FormRenderer = BasicRenderer.extend({ _renderButtonBox: function (node) { var self = this; var $result = $('<' + node.tag + '>', { 'class': 'o_not_full' }); - // Avoid to show buttons if we are in create mode (edit mode without res_id) - if (this.mode === 'edit' && !this.state.res_id) { - return $result; - } var buttons = _.map(node.children, function (child) { if (child.tag === 'button') { return self._renderStatButton(child); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index ada05e5ed28..3ceb2046756 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -2642,7 +2642,7 @@ QUnit.module('Views', { form.destroy(); }); - QUnit.test('button box is not rendered in create mode', function (assert) { + QUnit.test('button box is rendered in create mode', function (assert) { assert.expect(3); var form = createView({ @@ -2672,8 +2672,8 @@ QUnit.module('Views', { // create mode (leave edition first!) form.$buttons.find('.o_form_button_cancel').click(); form.$buttons.find('.o_form_button_create').click(); - assert.strictEqual(form.$('.oe_stat_button').length, 0, - "button box should not be displayed when creating a new record"); + assert.strictEqual(form.$('.oe_stat_button').length, 1, + "button box should be displayed when creating a new record as well"); form.destroy(); }); From d8c897de6452114533ffe7447ff394fcaf080032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Wed, 23 Aug 2017 16:56:13 +0200 Subject: [PATCH 009/119] [FIX] sale_timesheet: uncomment forgotten code when testing previous fix --- addons/sale_timesheet/models/account.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index eec7637be31..7e6e70c50b8 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -27,9 +27,9 @@ class AccountAnalyticLine(models.Model): @api.multi def write(self, values): # prevent to update invoiced timesheets - #if self.filtered(lambda timesheet: timesheet.timesheet_invoice_id): - # if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]): - # raise UserError(_('You can not modify already invoiced timesheets.')) + if self.filtered(lambda timesheet: timesheet.timesheet_invoice_id): + if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]): + raise UserError(_('You can not modify already invoiced timesheets.')) so_lines = self.mapped('so_line') if values.get('task_id'): From 32aa7dc9a0e4a987aff13f93627faa7036798df3 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 23 Aug 2017 14:54:26 +0200 Subject: [PATCH 010/119] [FIX] models: implement `change_default` behavior for fields The behavior is no longer implemented by the web client. Implement it server-side as part of the onchange mechanism, simply by defining onchange methods for the fields that have the flag. --- odoo/addons/test_new_api/models.py | 2 +- .../test_new_api/tests/test_onchange.py | 22 +++++++++++++++++++ odoo/models.py | 12 ++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index c84827d47ce..02609521ae0 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -286,7 +286,7 @@ class Foo(models.Model): _name = 'test_new_api.foo' name = fields.Char() - value1 = fields.Integer() + value1 = fields.Integer(change_default=True) value2 = fields.Integer() diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py index a6e2bff1b35..43c36b5b1d1 100644 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ b/odoo/addons/test_new_api/tests/test_onchange.py @@ -231,6 +231,28 @@ class TestOnChange(common.TransactionCase): for user in discussion.participants + demo], ) + def test_onchange_default(self): + """ test the effect of a conditional user-default on a field """ + Foo = self.env['test_new_api.foo'] + field_onchange = Foo._onchange_spec() + self.assertTrue(Foo._fields['value1'].change_default) + self.assertEqual(field_onchange.get('value1'), '1') + + # create a user-defined default based on 'value1' + self.env['ir.values'].set_default('test_new_api.foo', 'value2', 666, condition='value1=42') + + # setting 'value1' to 42 should trigger the change of 'value2' + self.env.invalidate_all() + values = {'name': 'X', 'value1': 42, 'value2': False} + result = Foo.onchange(values, 'value1', field_onchange) + self.assertEqual(result['value'], {'value2': 666}) + + # setting 'value1' to 24 should not trigger the change of 'value2' + self.env.invalidate_all() + values = {'name': 'X', 'value1': 24, 'value2': False} + result = Foo.onchange(values, 'value1', field_onchange) + self.assertEqual(result['value'], {}) + def test_onchange_one2many_value(self): """ test the value of the one2many field inside the onchange """ discussion = self.env.ref('test_new_api.discussion_0') diff --git a/odoo/models.py b/odoo/models.py index d5540341b4b..f6139aa3a36 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -573,6 +573,7 @@ class BaseModel(object): def is_onchange(func): return callable(func) and hasattr(func, '_onchange') + # collect onchange methods on the model's class cls = type(self) methods = defaultdict(list) for attr, func in getmembers(cls, is_onchange): @@ -581,6 +582,17 @@ class BaseModel(object): _logger.warning("@onchange%r parameters must be field names", func._onchange) methods[name].append(func) + # add onchange methods to implement "change_default" on fields + def onchange_default(field, self): + value = field.convert_to_write(self[field.name], self) + condition = "%s=%s" % (field.name, value) + defaults = self.env['ir.values'].get_defaults_dict(self._name, condition) + self.update(defaults) + + for name, field in cls._fields.items(): + if field.change_default: + methods[name].append(functools.partial(onchange_default, field)) + # optimization: memoize result on cls, it will not be recomputed cls._onchange_methods = methods return methods From 04470b811231eabab9ed4a7ed147882b16585e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 24 Aug 2017 10:57:15 +0200 Subject: [PATCH 011/119] [FIX] web: increase timeout for JS unit tests Some runbot servers are quite slow in the midday, and they fail the unit tests because the 3min timeout is reached. --- addons/web/tests/test_js.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py index 2253b1b73fc..c2f29ba0d0d 100644 --- a/addons/web/tests/test_js.py +++ b/addons/web/tests/test_js.py @@ -10,4 +10,4 @@ class WebSuite(odoo.tests.HttpCase): at_install = False def test_01_js(self): - self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=180) + self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=240) From c0247e6a8d6057710908be941dec6a846c31cd28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 24 Aug 2017 09:57:02 +0200 Subject: [PATCH 012/119] [FIX] web: correct evaluation for x2many fields in onchanges Before this commit, it could happen that a x2many field was considered invalid, which prevent triggering onchanges, even though each of its required fields are properly set. Here is such a scenario: We have a one2many field something_ids in a form view, with an inline tree view, which contains a field X. The field X is required on the model, but marked required="0" in the inline view. In that case, we incorrectly evaluated the required modifiers: since the attrs did not contain the required attribute, we fell back on the field_get value, which is true in this case. But this is not correct, the fieldviewget already computed the actual modifiers and took into account the fact that it was overridden. We need to only look at the modifiers. --- .../static/src/js/views/basic/basic_model.js | 3 +- .../tests/fields/relational_fields_tests.js | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) 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 0d95c292070..d54f609f002 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2738,8 +2738,7 @@ var BasicModel = AbstractModel.extend({ var fieldInfo = element.fieldsInfo[element.viewType][fieldName]; var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); var modifiers = self._evalModifiers(record, rawModifiers); - var required = 'required' in modifiers ? modifiers.required : field.required; - if (required && !self._isFieldSet(recordData[fieldName], field.type)) { + if (modifiers.required && !self._isFieldSet(recordData[fieldName], field.type)) { isValid = false; } }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 89f4601efc4..a2504af38bb 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -5352,6 +5352,43 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchange and required fields with override in arch', function (assert) { + assert.expect(4); + + this.data.partner.onchanges = { + turtles: function (obj) {} + }; + this.data.turtle.fields.turtle_foo.required = true; + this.data.partner.records[0].turtles = []; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + form.$buttons.find('.o_form_button_edit').click(); + + // triggers an onchange on partner, because the new record is valid + form.$('.o_field_x2many_list_row_add a').click(); + + assert.verifySteps(['read', 'default_get', 'onchange']); + form.destroy(); + }); + + QUnit.module('FieldMany2Many'); From 2eac9788f6de93a9017541e877322f90f082c3cd Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 23 Aug 2017 13:43:45 +0200 Subject: [PATCH 013/119] [FIX] l10n_multilang: Getting translations of templates from records Bug: Before the the fix, when trying to get all the translations of templates from a specific model: -All the translations for the templates of this model were catched first -> TT -All the records of this model for a specific company were catched afterwards -> RC Then, the linked between the translations(TT) and the records(RC) were made in function process_translations. But in some cases, a few records were removed and then it raised an error because the number of translations was different than the number of records. Fix: Thanks to the function create_record_with_xmlid in addons/account/models/chart_template.py, all the records created from a template have an xml_id in ir.model.data with str(company.id)+'_'+template_xmlid.name So with this way, it is possible to catch only the records created from a template. opw:767095 --- addons/l10n_multilang/models/l10n_multilang.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py index de0ef064bcd..bee06394b7d 100644 --- a/addons/l10n_multilang/models/l10n_multilang.py +++ b/addons/l10n_multilang/models/l10n_multilang.py @@ -73,22 +73,26 @@ class AccountChartTemplate(models.Model): @api.multi def _process_accounts_translations(self, company_id, langs, field): - in_ids = self.env['account.account.template'].search([('chart_template_id', '=', self.id)], order='id') - out_ids = self.env['account.account'].search([('company_id', '=', company_id)], order='id') + in_ids, out_ids = self._get_template_from_model(company_id, 'account.account') return self.process_translations(langs, field, in_ids, out_ids) @api.multi def _process_taxes_translations(self, company_id, langs, field): - in_ids = self.env['account.tax.template'].search([('chart_template_id', '=', self.id)], order='id') - out_ids = self.env['account.tax'].search([('company_id', '=', company_id)], order='id') + in_ids, out_ids = self._get_template_from_model(company_id, 'account.tax') return self.process_translations(langs, field, in_ids, out_ids) @api.multi def _process_fiscal_pos_translations(self, company_id, langs, field): - in_ids = self.env['account.fiscal.position.template'].search([('chart_template_id', '=', self.id)], order='id') - out_ids = self.env['account.fiscal.position'].search([('company_id', '=', company_id)], order='id') + in_ids, out_ids = self._get_template_from_model(company_id, 'account.fiscal.position') return self.process_translations(langs, field, in_ids, out_ids) + def _get_template_from_model(self, company_id, model): + out_data = self.env['ir.model.data'].search([('model', '=', model), ('name', '=like', str(company_id)+'\_%')]) + out_ids = self.env[model].search([('id', 'in', out_data.mapped('res_id'))], order='id') + in_xml_id_names = [xml_id.partition(str(company_id) + '_')[-1] for xml_id in out_data.mapped('name')] + in_xml_ids = self.env['ir.model.data'].search([('model', '=', model+'.template'), ('name', 'in', in_xml_id_names)]) + in_ids = self.env[model+'.template'].search([('id', 'in', in_xml_ids.mapped('res_id'))], order='id') + return (in_ids, out_ids) class BaseLanguageInstall(models.TransientModel): """ Install Language""" From 7ec4f2d6f5e5974f9e16b60fde305477668a39d3 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 24 Aug 2017 11:15:13 +0200 Subject: [PATCH 014/119] [FIX] project: # Days to Close Take into account the End Date for task analysis. opw-767245 --- addons/project/report/project_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/project/report/project_report.py b/addons/project/report/project_report.py index 00d739ec9c4..cddd6722ba5 100644 --- a/addons/project/report/project_report.py +++ b/addons/project/report/project_report.py @@ -58,7 +58,7 @@ class ReportProjectTaskUser(models.Model): t.partner_id, t.stage_id as stage_id, t.kanban_state as state, - (extract('epoch' from (t.write_date-t.create_date)))/(3600*24) as closing_days, + (extract('epoch' from (NULLIF(t.date_end, t.write_date)-t.create_date)))/(3600*24) as closing_days, (extract('epoch' from (t.date_start-t.create_date)))/(3600*24) as opening_days, (extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days """ From 11c523f69357b850cabf6c238d9d4f40d43b3d77 Mon Sep 17 00:00:00 2001 From: Do Vuong Tien Date: Thu, 24 Aug 2017 12:28:03 +0700 Subject: [PATCH 015/119] [CLA] Signature for vuongtien2411 Done at #19025 --- doc/cla/individual/vuongtien2411.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/cla/individual/vuongtien2411.md diff --git a/doc/cla/individual/vuongtien2411.md b/doc/cla/individual/vuongtien2411.md new file mode 100644 index 00000000000..821a9881a43 --- /dev/null +++ b/doc/cla/individual/vuongtien2411.md @@ -0,0 +1,11 @@ +Vietnam, 2017-08-24 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Do Vuong Tien tiendv@trobz.com https://github.com/vuongtien2411 From dedff1842b7ea3ae3f2b0935ab0684201cc4aac0 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 2 Dec 2016 14:19:30 +0100 Subject: [PATCH 016/119] [FIX] account: account domain in reconciliation In the reconciliation widget, create a counterpart line. No domain is applied on the "Account" field regarding the company, meaning that we can select an account from any company. opw-694149 --- addons/account/models/account_journal_dashboard.py | 4 ++-- .../account/static/src/js/account_reconciliation_widgets.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 505442c36dd..edb2e5a01ff 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -264,11 +264,11 @@ class account_journal(models.Model): return { 'type': 'ir.actions.client', 'tag': 'bank_statement_reconciliation_view', - 'context': {'statement_ids': bank_stmt.ids}, + 'context': {'statement_ids': bank_stmt.ids, 'company_ids': self.mapped('company_id').ids}, } else: # Open reconciliation view for customers/suppliers - action_context = {'show_mode_selector': False} + action_context = {'show_mode_selector': False, 'company_ids': self.mapped('company_id').ids} if self.type == 'sale': action_context.update({'mode': 'customers'}) elif self.type == 'purchase': diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index dbbb55bddb1..6aacbb24c68 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -91,6 +91,10 @@ var abstractReconciliation = Widget.extend(ControlPanelMixin, { // NB : for presets to work correctly, a field id must be the same string as a preset field this.presets = {}; // Description of the fields to initialize in the "create new line" form + var domain_account_id = [['deprecated', '=', false]]; + if (context && context.context && context.context.company_ids) { + domain_account_id.push(['company_id', 'in', context.context.company_ids]); + } this.create_form_fields = { account_id: { id: "account_id", @@ -103,7 +107,7 @@ var abstractReconciliation = Widget.extend(ControlPanelMixin, { relation: "account.account", string: _t("Account"), type: "many2one", - domain: [['deprecated', '=', false]], + domain: domain_account_id, }, }, label: { From f4270a244ee9cd3d283b38f52f08b909edb73da0 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 24 Aug 2017 16:00:01 +0200 Subject: [PATCH 017/119] [FIX] hr_payroll: missing field in view opw-767431 --- addons/hr_payroll/views/hr_payroll_views.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/hr_payroll/views/hr_payroll_views.xml b/addons/hr_payroll/views/hr_payroll_views.xml index ac8006a26ab..114a57a8b38 100644 --- a/addons/hr_payroll/views/hr_payroll_views.xml +++ b/addons/hr_payroll/views/hr_payroll_views.xml @@ -507,6 +507,7 @@
+ From ea60ec029d48daf3453bac5ed5752cf5ca3b37ee Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 24 Aug 2017 16:20:29 +0200 Subject: [PATCH 018/119] [FIX] account: traceback in reconciliation widget If a user reconcile an active line, when he active an other line, the widget can't close the removed line because it no longer exists. bug introduce by https://github.com/odoo/odoo/commit/1de418aa7afb986eeb2b98f12dee0c5661902737 --- .../static/src/js/reconciliation/reconciliation_action.js | 5 ++++- addons/account/static/tests/reconciliation_tests.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 8763d5acaa1..fabfee1ad00 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -201,7 +201,10 @@ var StatementAction = Widget.extend(ControlPanelMixin, { _.each(self.model.lines, function (line, _handle) { if (line.mode !== 'inactive' && _handle !== handle) { self.model.changeMode(_handle, 'inactive'); - self._getWidget(_handle).update(line); + var widget = self._getWidget(_handle); + if (widget) { + widget.update(line); + } } }); } diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 511656906d9..d90ceb08d4c 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -1005,7 +1005,6 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation pager', function (assert) { assert.expect(6); @@ -1219,9 +1218,11 @@ QUnit.module('account', { assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should display 10 lines"); assert.strictEqual(clientAction.$('.js_load_more:visible').length, 1, "should display the load more button"); + clientAction.$('.accounting_view:has(.o_reconcile:visible) thead .cell_action').click(); clientAction.$('.o_reconcile:visible').click(); assert.strictEqual(clientAction.$('.o_reconcile:visible').length, 0, "should remove the reconciled line"); assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should load one line to complete the 10"); + clientAction.$('thead .cell_action:eq(1)').click(); clientAction.$('.js_load_more').click(); assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 11, "should load the last record"); assert.strictEqual(clientAction.$('.js_load_more:visible').length, 0, "should hide the load more button"); From 6e22e8b027ae8237b9c0ad9165eae695b7d875d6 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 24 Aug 2017 18:13:54 +0200 Subject: [PATCH 019/119] [FIX] web: use 'o_input' class on kanban progressbar Since the new views, the 'o_input' class has to be set explicitely on ,