summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js6
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js47
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column_progressbar.js247
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js10
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js110
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js25
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_view.js36
-rw-r--r--addons/web/static/src/less/kanban_column_progressbar.less115
-rw-r--r--addons/web/static/src/xml/kanban.xml15
9 files changed, 593 insertions, 18 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 779073b51ed..bcccefbb984 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1576,7 +1576,7 @@ var BasicModel = AbstractModel.extend({
if (change.id) {
relRecord = self.localData[change.id];
}
- switch(change.operation) {
+ switch (change.operation) {
case 'ADD':
list.count++;
var resID = relRecord ? relRecord.res_id : change.resID;
@@ -1666,7 +1666,7 @@ var BasicModel = AbstractModel.extend({
var result = {};
var self = this;
var evalContext;
- function evalModifier (mod) {
+ function evalModifier(mod) {
if (mod === undefined || mod === false || mod === true) {
return !!mod;
}
@@ -2775,7 +2775,7 @@ var BasicModel = AbstractModel.extend({
var context = _.extend({}, record.data, record._changes);
// calls _generateX2ManyCommands for a given field, and returns the array of commands
- function _generateX2ManyCommands (fieldName) {
+ function _generateX2ManyCommands(fieldName) {
var commands = self._generateX2ManyCommands(record, {fieldNames: [fieldName]});
return commands[fieldName];
}
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index ab071d0e930..dac28c536dd 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -8,6 +8,7 @@ var kanban_quick_create = require('web.kanban_quick_create');
var KanbanRecord = require('web.KanbanRecord');
var view_dialogs = require('web.view_dialogs');
var Widget = require('web.Widget');
+var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
var _t = core._t;
var QWeb = core.qweb;
@@ -19,6 +20,8 @@ var KanbanColumn = Widget.extend({
cancel_quick_create: '_onCancelQuickCreate',
kanban_record_delete: '_onDeleteRecord',
quick_create_add_record: '_onQuickCreateAddRecord',
+ tweak_column: '_onTweakColumn',
+ tweak_column_records: '_onTweakColumnRecords',
},
events: {
'click .o_column_edit': '_onEditColumn',
@@ -58,6 +61,13 @@ var KanbanColumn = Widget.extend({
this.offset = 0;
this.remaining = this.size - this.data_records.length;
+ if (options.hasProgressBar) {
+ this.barOptions = {
+ columnID: this.db_id,
+ progressBarStates: options.progressBarStates,
+ };
+ }
+
this.record_options = _.clone(recordOptions);
if (options.grouped_by_m2o) {
@@ -81,6 +91,7 @@ var KanbanColumn = Widget.extend({
*/
start: function () {
var self = this;
+ var defs = [this._super.apply(this, arguments)];
this.$header = this.$('.o_kanban_header');
for (var i = 0; i < this.data_records.length; i++) {
@@ -128,9 +139,12 @@ var KanbanColumn = Widget.extend({
self._onToggleFold(event);
}
});
- this._update();
+ if (this.barOptions) {
+ this.progressBar = new KanbanColumnProgressBar(this, this.barOptions, this.data);
+ defs.push(this.progressBar.appendTo(this.$header));
+ }
- return this._super.apply(this, arguments);
+ return $.when.apply($, defs).then(this._update.bind(this));
},
//--------------------------------------------------------------------------
@@ -180,6 +194,18 @@ var KanbanColumn = Widget.extend({
isEmpty: function () {
return !this.records.length;
},
+ /**
+ * Updates the column progressBar and sets the new data. New data are
+ * supposed to already match column rendering (except for the progressBar).
+ *
+ * @param {Object} data
+ */
+ updateProgressBar: function (data) {
+ this.data = data;
+ if (!this.folded && this.progressBar) {
+ this.progressBar.update(this.data);
+ }
+ },
//--------------------------------------------------------------------------
// Private
@@ -221,6 +247,7 @@ var KanbanColumn = Widget.extend({
} else {
this.$('.o_kanban_load_more').html(QWeb.render('KanbanView.LoadMore', {widget: this}));
}
+ this.updateProgressBar(this.data);
},
//--------------------------------------------------------------------------
@@ -321,6 +348,22 @@ var KanbanColumn = Widget.extend({
},
/**
* @private
+ * @param {OdooEvent} ev
+ */
+ _onTweakColumn: function (ev) {
+ ev.data.callback(this.$el);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onTweakColumnRecords: function (ev) {
+ _.each(this.records, function (record) {
+ ev.data.callback(record.$el, record.state.data);
+ });
+ },
+ /**
+ * @private
* @param {MouseEvent} event
*/
_onUnarchiveRecords: function (event) {
diff --git a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
new file mode 100644
index 00000000000..c4a62989b97
--- /dev/null
+++ b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
@@ -0,0 +1,247 @@
+odoo.define('web.KanbanColumnProgressBar', function (require) {
+'use strict';
+
+var Widget = require('web.Widget');
+var session = require('web.session');
+var utils = require('web.utils');
+
+var KanbanColumnProgressBar = Widget.extend({
+ template: 'KanbanView.ColumnProgressBar',
+ events: {
+ 'click .o_kanban_counter_progress': '_onProgressBarParentClick',
+ 'click .progress-bar': '_onProgressBarClick',
+ },
+ /**
+ * Allows to disable animations for tests.
+ * @type {boolean}
+ */
+ ANIMATE: true,
+
+ /**
+ * @constructor
+ */
+ init: function (parent, options, columnState) {
+ this._super.apply(this, arguments);
+
+ this.columnID = options.columnID;
+
+ // <progressbar/> attributes
+ this.fieldName = columnState.progressBarValues.field;
+ this.colors = columnState.progressBarValues.colors;
+ this.sumField = columnState.progressBarValues.sum;
+
+ // Previous progressBar state
+ var state = options.progressBarStates[this.columnID];
+ this.groupCount = state ? state.groupCount : 0;
+ this.subgroupCounts = state ? state.subgroupCounts : {};
+ this.totalCounterValue = state ? state.totalCounterValue : 0;
+ this.activeFilter = state ? state.activeFilter : false;
+
+ // Prepare currency (TODO this should be automatic... use a field ?)
+ this.counterPrefix = '';
+ this.counterSuffix = '';
+ var sumFieldInfo = this.sumField && columnState.fieldsInfo.kanban[this.sumField];
+ var currencyField = sumFieldInfo && sumFieldInfo.options && sumFieldInfo.options.currency_field;
+ if (currencyField && columnState.data.length) {
+ var currency = session.currencies[columnState.data[0].data[currencyField].res_id];
+ if (currency.position === 'before') {
+ this.counterPrefix = currency.symbol + ' ';
+ } else {
+ this.counterSuffix = ' ' + currency.symbol;
+ }
+ }
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+
+ this.$bars = {};
+ _.each(this.colors, function (val, key) {
+ self.$bars[val] = self.$('.bg-' + val + '-full');
+ });
+ this.$counter = this.$('.o_kanban_counter_side');
+
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates internal data and rendering according to new received column
+ * state.
+ *
+ * @param {Object} columnState
+ */
+ update: function (columnState) {
+ var self = this;
+
+ var subgroupCounts = {};
+ _.each(self.colors, function (val, key) {
+ var subgroupCount = columnState.progressBarValues.counts[key] || 0;
+ if (self.activeFilter === val && subgroupCount === 0) {
+ self.activeFilter = false;
+ }
+ subgroupCounts[key] = subgroupCount;
+ });
+
+ this.groupCount = columnState.count;
+ this.subgroupCounts = subgroupCounts;
+ this.prevTotalCounterValue = this.totalCounterValue;
+ this.totalCounterValue = this.sumField ? (columnState.aggregateValues[this.sumField] || 0) : columnState.count;
+ this._notifyState();
+ this._render();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates the rendering according to internal data. This is done without
+ * qweb rendering because there are animations.
+ *
+ * @private
+ */
+ _render: function () {
+ var self = this;
+
+ // Update column display according to active filter
+ this.trigger_up('tweak_column', {
+ callback: function ($el) {
+ $el.removeClass('o_kanban_group_show');
+ _.each(self.colors, function (val, key) {
+ $el.removeClass('o_kanban_group_show_' + val);
+ });
+ if (self.activeFilter) {
+ $el.addClass('o_kanban_group_show o_kanban_group_show_' + self.colors[self.activeFilter]);
+ }
+ },
+ });
+ this.trigger_up('tweak_column_records', {
+ callback: function ($el, recordData) {
+ var categoryValue = recordData[self.fieldName];
+ _.each(self.colors, function (val, key) {
+ $el.removeClass('oe_kanban_card_' + val);
+ });
+ if (self.colors[categoryValue]) {
+ $el.addClass('oe_kanban_card_' + self.colors[categoryValue]);
+ }
+ },
+ });
+
+ // Display and animate the progress bars
+ _.each(self.colors, function (val, key) {
+ var $bar = self.$bars[val];
+ var count = self.subgroupCounts[key];
+
+ if (!$bar) {
+ return;
+ }
+
+ // Adapt tooltip
+ $bar.attr('data-original-title', count + ' ' + key);
+ $bar.tooltip({
+ delay: '0',
+ trigger:'hover',
+ placement: 'top'
+ });
+
+ // Adapt active state
+ $bar.toggleClass('active progress-bar-striped', key === self.activeFilter);
+
+ // Adapt width
+ $bar.removeClass('o_bar_has_records transition-off');
+ window.getComputedStyle($bar[0]).getPropertyValue('width'); // Force reflow so that animations work
+ if (count > 0) {
+ $bar.addClass('o_bar_has_records');
+ $bar.css('width', (count * 100 / self.groupCount) + '%');
+ } else {
+ $bar.css('width', '');
+ }
+ });
+
+ // Display and animate the counter number
+ var humanNumber;
+ var suffix = this.counterSuffix;
+ var end = this.totalCounterValue;
+ if (end >= 10000) {
+ humanNumber = utils.human_number(end, 1);
+ end = parseFloat(humanNumber.substr(0, humanNumber.length - 1));
+ suffix = humanNumber[humanNumber.length - 1] + suffix;
+ }
+ var start = this.prevTotalCounterValue || 0;
+ if (start >= 10000) {
+ humanNumber = utils.human_number(start, 1);
+ start = parseFloat(humanNumber.substr(0, humanNumber.length - 1));
+ }
+ if (end > start && this.ANIMATE) {
+ $({currentValue: start}).animate({currentValue: end}, {
+ duration: 1000,
+ start: function () {
+ self.$counter.addClass('o_kanban_grow');
+ },
+ step: function () {
+ self.$counter.html(self.counterPrefix + Math.round(this.currentValue) + suffix);
+ },
+ complete: function () {
+ self.$counter.html(self.counterPrefix + this.currentValue + suffix);
+ self.$counter.removeClass('o_kanban_grow');
+ },
+ });
+ } else {
+ this.$counter.html(this.counterPrefix + end + suffix);
+ }
+ },
+ /**
+ * Notifies the new progressBar state so that if a full rerender occurs, the
+ * new progressBar that would replace this one will be initialized with
+ * current state, so that animations are correct.
+ *
+ * @private
+ */
+ _notifyState: function () {
+ this.trigger_up('set_progress_bar_state', {
+ columnID: this.columnID,
+ values: {
+ groupCount: this.groupCount,
+ subgroupCounts: this.subgroupCounts,
+ totalCounterValue: this.totalCounterValue,
+ activeFilter: this.activeFilter,
+ },
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onProgressBarClick: function (ev) {
+ this.$clickedBar = $(ev.currentTarget);
+ var filter = this.$clickedBar.data('filter');
+ this.activeFilter = (this.activeFilter === filter ? false : filter);
+ this._notifyState();
+ this._render();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onProgressBarParentClick: function (ev) {
+ if (ev.target !== ev.currentTarget) {
+ return;
+ }
+ this.activeFilter = false;
+ this._notifyState();
+ this._render();
+ },
+});
+return KanbanColumnProgressBar;
+});
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 7e018327060..8ba0e3fb7c3 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -78,6 +78,12 @@ var KanbanController = BasicController.extend({
* @returns {Deferred}
*/
_confirmSave: function (id) {
+ var data = this.model.get(this.handle, {raw: true});
+ var grouped = data.groupedBy.length;
+ if (grouped) {
+ var columnState = this.model.getColumn(id);
+ return this.renderer.updateColumn(columnState.id, columnState);
+ }
return this.renderer.updateRecord(this.model.get(id));
},
/**
@@ -164,7 +170,7 @@ var KanbanController = BasicController.extend({
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
});
- });
+ });
}).fail(this.reload.bind(this));
},
/**
@@ -305,7 +311,7 @@ var KanbanController = BasicController.extend({
context: _.extend({default_name: name}, context),
title: _t("Create"),
disable_multiple_selection: true,
- on_saved: function(record) {
+ on_saved: function (record) {
add_record([record.res_id]);
},
}).open();
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 f714fc519df..400d73c0b7d 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -98,9 +98,25 @@ var KanbanModel = BasicModel.extend({
if (dp && dp.tooltipData) {
result.tooltipData = $.extend(true, {}, dp.tooltipData);
}
+ if (dp && dp.progressBarValues) {
+ result.progressBarValues = $.extend(true, {}, dp.progressBarValues);
+ }
return result;
},
/**
+ * Same as @see get but getting the parent element whose ID is given.
+ *
+ * @param {string} id
+ * @returns {Object}
+ */
+ getColumn: function (id) {
+ var element = this.localData[id];
+ if (element) {
+ return this.get(element.parentID);
+ }
+ return null;
+ },
+ /**
* @override
*/
load: function (params) {
@@ -148,7 +164,8 @@ var KanbanModel = BasicModel.extend({
// Manually updates groups data. Note: this is done before the actual
// save as it might need to perform a read group in some cases so those
// updated data might be overriden again.
- var resID = self.localData[recordID].res_id;
+ var record = self.localData[recordID];
+ var resID = record.res_id;
// Remove record from its current group
var old_group;
for (var i = 0; i < parent.data.length; i++) {
@@ -170,6 +187,7 @@ var KanbanModel = BasicModel.extend({
return this.notifyChanges(recordID, changes).then(function () {
return self.save(recordID);
}).then(function () {
+ record.parentID = new_group.id;
return [old_group.id, new_group.id];
});
},
@@ -182,7 +200,15 @@ var KanbanModel = BasicModel.extend({
if (options && options.groupBy && !options.groupBy.length) {
options.groupBy = this.defaultGroupedBy;
}
- return this._super(id, options);
+ var def = this._super(id, options);
+ return this._reloadProgressBarGroupFromRecord(id, def);
+ },
+ /**
+ * @override
+ */
+ save: function (recordID) {
+ var def = this._super.apply(this, arguments);
+ return this._reloadProgressBarGroupFromRecord(recordID, def);
},
//--------------------------------------------------------------------------
@@ -190,6 +216,25 @@ var KanbanModel = BasicModel.extend({
//--------------------------------------------------------------------------
/**
+ * @override
+ */
+ _makeDataPoint: function (params) {
+ var dataPoint = this._super.apply(this, arguments);
+ if (params.progressBar) {
+ dataPoint.progressBar = params.progressBar;
+ }
+ return dataPoint;
+ },
+ /**
+ * @override
+ */
+ _load: function (dataPoint, options) {
+ if (dataPoint.progressBar) {
+ return this._readProgressBarGroup(dataPoint, options);
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
* Ensures that there is no nested groups in Kanban (only the first grouping
* level is taken into account).
*
@@ -207,6 +252,34 @@ var KanbanModel = BasicModel.extend({
});
},
/**
+ * @private
+ * @param {Object} dataPoint
+ * @returns {Deferred<Object>}
+ */
+ _readProgressBarGroup: function (list, options) {
+ var self = this;
+ var groupsDef = this._readGroup(list, options);
+ var progressBarDef = this._rpc({
+ model: list.model,
+ method: 'read_progress_bar',
+ kwargs: {
+ domain: list.domain,
+ group_by: list.groupedBy[0],
+ progress_bar: list.progressBar,
+ context: list.context,
+ },
+ });
+ return $.when(groupsDef, progressBarDef).then(function (a, data) {
+ _.each(list.data, function (groupID) {
+ var group = self.localData[groupID];
+ group.progressBarValues = _.extend({
+ counts: data[group.value] || {},
+ }, list.progressBar);
+ });
+ return list;
+ });
+ },
+ /**
* Fetches tooltip specific fields on the group by relation and stores it in
* the column datapoint in a special key `tooltipData`.
* Data for the tooltips (group_by_tooltip) are fetched in batch for all
@@ -253,8 +326,37 @@ var KanbanModel = BasicModel.extend({
}
return $.when();
},
-});
+ /**
+ * Reloads all progressbar data if the given id is a record's one. This is
+ * done after given deferred and insures that the given deferred's result is
+ * not lost.
+ *
+ * @private
+ * @param {string} recordID
+ * @param {Deferred} def
+ * @returns {Deferred}
+ */
+ _reloadProgressBarGroupFromRecord: function (recordID, def) {
+ var element = this.localData[recordID];
+ if (element.type !== 'record') {
+ return def;
+ }
+ // If we updated a record, then we must potentially update columns'
+ // progressbars, so we need to load groups info again
+ var self = this;
+ while (element) {
+ if (element.progressBar) {
+ return def.then(function (data) {
+ return self._load(element, {onlyGroups: true}).then(function () {
+ return data;
+ });
+ });
+ }
+ element = this.localData[element.parentID];
+ }
+ return def;
+ },
+});
return KanbanModel;
-
});
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 5d7be3d3636..49752ca2678 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -83,6 +83,10 @@ function transformQwebTemplate(node, fields) {
var KanbanRenderer = BasicRenderer.extend({
className: 'o_kanban_view',
+ custom_events: _.extend({}, BasicRenderer.prototype.custom_events || {}, {
+ 'set_progress_bar_state': '_onSetProgressBarState',
+ }),
+
/**
* @override
*/
@@ -100,6 +104,9 @@ var KanbanRenderer = BasicRenderer.extend({
viewType: 'kanban',
});
this.columnOptions = _.extend({}, params.column_options, { qweb: this.qweb });
+ if (this.columnOptions.hasProgressBar) {
+ this.columnOptions.progressBarStates = {};
+ }
this._setState(state);
},
@@ -336,6 +343,24 @@ var KanbanRenderer = BasicRenderer.extend({
});
this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates progressbar internal states (necessary for animations) with
+ * received data.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onSetProgressBarState: function (ev) {
+ if (!this.columnOptions.progressBarStates[ev.data.columnID]) {
+ this.columnOptions.progressBarStates[ev.data.columnID] = {};
+ }
+ _.extend(this.columnOptions.progressBarStates[ev.data.columnID], ev.data.values);
+ },
});
return KanbanRenderer;
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 55a90e83242..3e808de72e3 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -6,6 +6,7 @@ var core = require('web.core');
var KanbanModel = require('web.KanbanModel');
var KanbanRenderer = require('web.KanbanRenderer');
var KanbanController = require('web.KanbanController');
+var utils = require('web.utils');
var _lt = core._lt;
@@ -21,6 +22,10 @@ var KanbanView = BasicView.extend({
Renderer: KanbanRenderer,
},
viewType: 'kanban',
+
+ /**
+ * @constructor
+ */
init: function (viewInfo, params) {
this._super.apply(this, arguments);
@@ -29,8 +34,20 @@ var KanbanView = BasicView.extend({
this.loadParams.limit = this.loadParams.limit || 40;
this.loadParams.openGroupByDefault = true;
this.loadParams.type = 'list';
-
this.loadParams.groupBy = arch.attrs.default_group_by ? [arch.attrs.default_group_by] : (params.groupBy || []);
+ var progressBar;
+ utils.traverse(arch, function (n) {
+ var isProgressBar = (n.tag === 'progressbar');
+ if (isProgressBar) {
+ progressBar = _.clone(n.attrs);
+ progressBar.colors = JSON.parse(progressBar.colors);
+ progressBar.sum = progressBar.sum || false;
+ }
+ return !isProgressBar;
+ });
+ if (progressBar) {
+ this.loadParams.progressBar = progressBar;
+ }
var activeActions = this.controllerParams.activeActions;
activeActions = _.extend(activeActions, {
@@ -44,6 +61,7 @@ var KanbanView = BasicView.extend({
deletable: activeActions.group_delete,
group_creatable: activeActions.group_create,
quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(viewInfo),
+ hasProgressBar: !!progressBar,
};
this.rendererParams.record_options = {
editable: activeActions.edit,
@@ -56,10 +74,19 @@ var KanbanView = BasicView.extend({
this.controllerParams.readOnlyMode = false;
this.controllerParams.hasButtons = true;
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Object} viewInfo
+ */
_isQuickCreateEnabled: function (viewInfo) {
var groupBy = this.loadParams.groupBy[0];
groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined;
- if(groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], viewInfo.fields[groupBy].type)) {
+ if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], viewInfo.fields[groupBy].type)) {
return false;
}
if (!this.controllerParams.activeActions.create) {
@@ -70,11 +97,6 @@ var KanbanView = BasicView.extend({
}
return true;
}
-
-
});
-
return KanbanView;
-
});
-
diff --git a/addons/web/static/src/less/kanban_column_progressbar.less b/addons/web/static/src/less/kanban_column_progressbar.less
new file mode 100644
index 00000000000..c1cf86adc81
--- /dev/null
+++ b/addons/web/static/src/less/kanban_column_progressbar.less
@@ -0,0 +1,115 @@
+
+.o-kanban-css-filter(@class, @accent-color) {
+ // Provide CSS reordering and highlight
+ &.o_kanban_group_show_@{class} {
+ @mix-soft: mix(@accent-color, @odoo-webclient-background-color, 5%);
+ @mix-full: mix(@accent-color, @odoo-webclient-background-color);
+
+ &, .o_kanban_header {
+ background-color: @mix-soft;
+ border-color: @mix-full;
+ }
+
+ .progress-bar.bg-@{class}-full {
+ border: 1px solid white;
+ }
+
+ .oe_kanban_card_@{class} {
+ order: 1;
+ margin-bottom: @o-kanban-inside-vgutter*0.5;
+
+ ~ .oe_kanban_card_@{class} {
+ margin-top: -@o-kanban-inside-vgutter*0.5 - 1px;
+ }
+ }
+
+ .o_kanban_load_more {
+ order: 2;
+ padding: @o-kanban-inside-vgutter*0.5 0 @o-kanban-inside-vgutter;
+ }
+
+ .o_kanban_record:not(.oe_kanban_card_@{class}) {
+ order: 3;
+ opacity: 0.5;
+ box-shadow: none;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+}
+
+.o_kanban_counter {
+ position: relative;
+ .o-flex-display();
+ .o-align-items(center);
+ height: 30px;
+ margin-bottom: @o-kanban-record-margin*2;
+
+ > .o_kanban_counter_progress {
+ width: 76%;
+ height: @font-size-base;
+ margin-bottom: 0;
+ background: fade(black, 7%);
+ box-shadow: none;
+
+ .progress-bar {
+ margin-bottom: 0;
+ box-shadow: none;
+ cursor: pointer;
+
+ // Be sure that a bar with data has a minimun width
+ &.o_bar_has_records {
+ min-width: 10px;
+ }
+ }
+
+ .o_kanban_counter_label {
+ font-size: 10px;
+ .user-select(none);
+ }
+ }
+
+ > .o_kanban_counter_side {
+ width: 21%;
+ margin-left: 3%;
+ color: @headings-color;
+ font-weight: bold;
+ text-align: right;
+
+ &.o_kanban_grow {
+ .o-animation(grow, 400ms, ease-in, 200ms);
+ }
+ }
+}
+.o_column_folded .o_kanban_counter {
+ display: none;
+}
+
+.o_kanban_view .o_kanban_group {
+ .o-kanban-css-filter(success, @brand-success);
+ .o-kanban-css-filter(warning, @brand-warning);
+ .o-kanban-css-filter(danger, @brand-danger);
+
+ &.o_kanban_group_show {
+ .o-flex-display();
+ .o-flex-flow(column, nowrap);
+
+ > * {
+ .o-flex(0, 0, auto);
+ }
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale3d(1, 1, 1);
+ }
+ 50% {
+ transform: scale3d(1.5, 1.5, 1.5);
+ }
+ 100% {
+ transform: scale3d(1, 1, 1);
+ }
+}
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index c7e7c83e836..acde7fcc739 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -91,4 +91,19 @@
</tbody>
</table>
</t>
+
+<t t-name="KanbanView.ColumnProgressBar">
+ <div class="o_kanban_counter">
+ <div class="o_kanban_counter_progress progress">
+ <t t-foreach="widget.colors" t-as="color">
+ <t t-set="count" t-value="widget.subgroupCounts[color] || 0"/>
+ <div t-attf-class="progress-bar transition-off bg-#{color_value}-full#{count ? ' o_bar_has_records' : ''}#{widget.activeFilter ? ' active progress-bar-striped' : ''}"
+ t-att-data-filter="color"
+ t-attf-data-original-title="#{count} #{color}"
+ t-attf-style="width: #{count ? (count * 100 / widget.groupCount) : 0}%;"/>
+ </t>
+ </div>
+ <div class="o_kanban_counter_side"><t t-esc="widget.totalCounterValue"/></div>
+ </div>
+</t>
</template>