diff --git a/kpi_dashboard/models/kpi_dashboard.py b/kpi_dashboard/models/kpi_dashboard.py index 5751f3ec..770a2546 100644 --- a/kpi_dashboard/models/kpi_dashboard.py +++ b/kpi_dashboard/models/kpi_dashboard.py @@ -16,6 +16,10 @@ class KpiDashboard(models.Model): "kpi.dashboard.item", inverse_name="dashboard_id", copy=True, ) number_of_columns = fields.Integer(default=5, required=True) + compute_on_fly_refresh = fields.Integer( + default=0, + help="Seconds to refresh on fly elements" + ) width = fields.Integer(compute="_compute_width") margin_y = fields.Integer(default=10, required=True) margin_x = fields.Integer(default=10, required=True) @@ -43,6 +47,15 @@ class KpiDashboard(models.Model): + rec.widget_dimension_x * rec.number_of_columns ) + def read_dashboard_on_fly(self): + self.ensure_one() + result = [] + for item in self.item_ids: + if not item.kpi_id.compute_on_fly: + continue + result.append(item._read_dashboard()) + return result + def read_dashboard(self): self.ensure_one() result = { @@ -52,6 +65,7 @@ class KpiDashboard(models.Model): "max_cols": self.number_of_columns, "margin_x": self.margin_x, "margin_y": self.margin_y, + "compute_on_fly_refresh": self.compute_on_fly_refresh, "widget_dimension_x": self.widget_dimension_x, "widget_dimension_y": self.widget_dimension_y, "background_color": self.background_color, @@ -167,10 +181,19 @@ class KpiDashboardItem(models.Model): "kpi_id": self.kpi_id.id, "suffix": self.kpi_id.suffix or "", "prefix": self.kpi_id.prefix or "", - "value": self.kpi_id.value, - "value_last_update": self.kpi_id.value_last_update, + "compute_on_fly": self.kpi_id.compute_on_fly, } ) + if self.kpi_id.compute_on_fly: + vals.update({ + "value": self.kpi_id._compute_value(), + "value_last_update": fields.Datetime.now(), + }) + else: + vals.update({ + "value": self.kpi_id.value, + "value_last_update": self.kpi_id.value_last_update, + }) if self.kpi_id.action_ids: vals["actions"] = self.kpi_id.action_ids.read_dashboard() else: diff --git a/kpi_dashboard/models/kpi_kpi.py b/kpi_dashboard/models/kpi_kpi.py index 275818d9..b67e506a 100644 --- a/kpi_dashboard/models/kpi_kpi.py +++ b/kpi_dashboard/models/kpi_kpi.py @@ -5,7 +5,11 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError import ast from odoo.tools.safe_eval import safe_eval +from odoo.addons.base.models.ir_cron import _intervalTypes +from odoo.tools.float_utils import float_compare import re +import json +import datetime class KpiKpi(models.Model): @@ -38,6 +42,26 @@ class KpiKpi(models.Model): help="Actions that can be opened from the KPI" ) code = fields.Text("Code") + store_history = fields.Boolean() + store_history_interval = fields.Selection( + selection=lambda self: + self.env['ir.cron']._fields['interval_type'].selection, + ) + store_history_interval_number = fields.Integer() + compute_on_fly = fields.Boolean() + history_ids = fields.One2many("kpi.kpi.history", inverse_name="kpi_id") + computed_value = fields.Serialized(compute='_compute_computed_value') + computed_date = fields.Datetime(compute='_compute_computed_value') + + @api.depends('value', 'value_last_update', 'compute_on_fly') + def _compute_computed_value(self): + for record in self: + if record.compute_on_fly: + record.computed_value = record._compute_value() + record.computed_date = fields.Datetime.now() + else: + record.computed_value = record.value + record.computed_date = record.value_last_update def _cron_vals(self): return { @@ -55,14 +79,32 @@ class KpiKpi(models.Model): record._compute() return True + def _generate_history_vals(self, value): + return { + "kpi_id": self.id, + "value": value, + "widget": self.widget, + } + + def _compute_value(self): + return getattr(self, "_compute_value_%s" % self.computation_method)() + def _compute(self): - self.write( - { - "value": getattr( - self, "_compute_value_%s" % self.computation_method - )() - } - ) + value = self._compute_value() + self.write({"value": value}) + if self.store_history: + last = self.env['kpi.kpi.history'].search([ + ('kpi_id', '=', self.id) + ], limit=1) + if ( + not last or + not self.store_history_interval or + last.create_date + _intervalTypes[self.store_history_interval]( + self.store_history_interval_number) < fields.Datetime.now() + ): + self.env["kpi.kpi.history"].create( + self._generate_history_vals(value) + ) notifications = [] for dashboard_item in self.dashboard_item_ids: channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id @@ -92,6 +134,8 @@ class KpiKpi(models.Model): return { "self": self, "model": self.browse(), + "datetime": datetime, + "float_compare": float_compare, } def _forbidden_code(self): @@ -115,6 +159,20 @@ class KpiKpi(models.Model): self.env.cr.execute("rollback to %s" % savepoint) return results.get("result", {}) + def show_value(self): + self.ensure_one() + action = self.env.ref('kpi_dashboard.kpi_kpi_act_window') + result = action.read()[0] + result.update({ + 'res_id': self.id, + 'target': 'new', + 'view_mode': 'form', + 'views': [(self.env.ref( + 'kpi_dashboard.kpi_kpi_widget_form_view' + ).id, 'form')], + }) + return result + class KpiKpiAction(models.Model): _name = 'kpi.kpi.action' @@ -129,13 +187,49 @@ class KpiKpiAction(models.Model): ('ir.actions.client', 'ir.actions.client')], required=True, ) + context = fields.Char() def read_dashboard(self): - result = [] + result = {} for r in self: - result.append({ + result[r.id] = { 'id': r.action.id, 'type': r.action._name, - 'name': r.action.name - }) + 'name': r.action.name, + 'context': safe_eval(r.context or '{}') + } + return result + + +class KpiKpiHistory(models.Model): + _name = 'kpi.kpi.history' + _description = 'KPI history' + _order = 'create_date DESC' + + kpi_id = fields.Many2one( + 'kpi.kpi', required=True, ondelete='cascade', readonly=True + ) + value = fields.Serialized(readonly=True) + raw_value = fields.Char(compute='_compute_raw_value') + name = fields.Char(related='kpi_id.name') + widget = fields.Selection( + selection=lambda self: + self.env['kpi.kpi']._fields['widget'].selection, + required=True) + + @api.depends('value') + def _compute_raw_value(self): + for record in self: + record.raw_value = json.dumps(record.value) + + def show_form(self): + self.ensure_one() + action = self.env.ref('kpi_dashboard.kpi_kpi_history_act_window') + result = action.read()[0] + result.update({ + 'res_id': self.id, + 'target': 'new', + 'view_mode': 'form', + 'views': [(self.env.context.get('form_id'), 'form')], + }) return result diff --git a/kpi_dashboard/security/ir.model.access.csv b/kpi_dashboard/security/ir.model.access.csv index 6f7b25b7..8dacb37e 100644 --- a/kpi_dashboard/security/ir.model.access.csv +++ b/kpi_dashboard/security/ir.model.access.csv @@ -3,7 +3,9 @@ access_kpi_dashboard,access_kpi_dashboard,model_kpi_dashboard,base.group_user,1, access_kpi_dashboard_kpi,access_kpi_dashboard_kpi,model_kpi_dashboard_item,base.group_user,1,0,0,0 access_kpi_kpi,access_kpi_kpi,model_kpi_kpi,base.group_user,1,0,0,0 access_kpi_kpi_action,access_kpi_kpi_action,model_kpi_kpi_action,base.group_user,1,0,0,0 +access_kpi_kpi_history,access_kpi_kpi_history,model_kpi_kpi_history,base.group_user,1,0,0,0 manage_kpi_dashboard,manage_kpi_dashboard,model_kpi_dashboard,group_kpi_dashboard_manager,1,1,1,1 manage_kpi_dashboard_kpi,manage_kpi_dashboard_kpi,model_kpi_dashboard_item,group_kpi_dashboard_manager,1,1,1,1 manage_kpi_kpi,manage_kpi_kpi,model_kpi_kpi,group_kpi_dashboard_manager,1,1,1,1 manage_kpi_kpi_action,manage_kpi_kpi_action,model_kpi_kpi_action,group_kpi_dashboard_manager,1,1,1,1 +manage_kpi_kpi_history,manage_kpi_kpi_history,model_kpi_kpi_history,group_kpi_dashboard_manager,1,1,1,1 diff --git a/kpi_dashboard/static/src/js/dashboard_controller.js b/kpi_dashboard/static/src/js/dashboard_controller.js index 30f28c0a..952077bd 100644 --- a/kpi_dashboard/static/src/js/dashboard_controller.js +++ b/kpi_dashboard/static/src/js/dashboard_controller.js @@ -10,7 +10,29 @@ odoo.define('kpi_dashboard.DashboardController', function (require) { var DashboardController = BasicController.extend({ custom_events: _.extend({}, BasicController.prototype.custom_events, { addDashboard: '_addDashboard', + refresh_on_fly: '_refreshOnFly', }), + _refreshOnFly: function (event) { + var self = this; + this._rpc({ + model: this.modelName, + method: 'read_dashboard_on_fly', + args: [[this.renderer.state.res_id]], + context: _.extend( + {}, + this.model.get(this.handle, {raw: true}).getContext(), + {bin_size: true} + ), + }).then(function (data) { + _.each(data, function (item) { + // We will follow the same logic used on Bus Notifications + self.renderer._onNotification([[ + "kpi_dashboard_" + self.renderer.state.res_id, + item + ]]) + }); + }); + }, renderPager: function ($node, options) { options = _.extend({}, options, { validate: this.canBeDiscarded.bind(this), diff --git a/kpi_dashboard/static/src/js/dashboard_renderer.js b/kpi_dashboard/static/src/js/dashboard_renderer.js index 6ff43ef5..b2603771 100644 --- a/kpi_dashboard/static/src/js/dashboard_renderer.js +++ b/kpi_dashboard/static/src/js/dashboard_renderer.js @@ -53,8 +53,21 @@ odoo.define('kpi_dashboard.DashboardRenderer', function (require) { 'bus_service', 'onNotification', this, this._onNotification ); + if (this.state.specialData.compute_on_fly_refresh > 0) { + // Setting the refresh interval + this.on_fly_interval = setInterval(function () { + self.trigger_up('refresh_on_fly'); + }, this.state.specialData.compute_on_fly_refresh *1000); + }; return $.when(); }, + on_detach_callback: function () { + // We want to clear the refresh interval once we exit the view + if (this.on_fly_interval) { + clearInterval(this.on_fly_interval) + } + this._super.apply(this, arguments); + }, _onNotification: function (notifications) { var self = this; _.each(notifications, function (notification) { diff --git a/kpi_dashboard/static/src/js/field_widget.js b/kpi_dashboard/static/src/js/field_widget.js new file mode 100644 index 00000000..2a25003e --- /dev/null +++ b/kpi_dashboard/static/src/js/field_widget.js @@ -0,0 +1,68 @@ +odoo.define('kpi_dashboard.KpiFieldWidget', function(require) { + "use strict"; + + var basic_fields = require('web.basic_fields'); + var field_registry = require('web.field_registry'); + var core = require('web.core'); + var qweb = core.qweb; + var registry = require('kpi_dashboard.widget_registry'); + + var KpiFieldWidget = basic_fields.FieldChar.extend({ + jsLibs: [ + '/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js', + ], + cssLibs: [ + '/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css', + ], + className: 'o_dashboard_view', + _renderReadonly: function () { + this.$el.html($(qweb.render('dashboard_kpi.dashboard'))); + var marginx = 0; + var marginy = 0; + var widgetx = 400; + var widgety = 400; + this.$el.find('.gridster').css('width', widgety); + this.$grid = this.$el.find('.gridster ul'); + var widgetVals = { + value: this.value, + col: 1, + row: 1, + sizex: 1, + sizey: 1, + name: this.recordData[this.nodeOptions.name], + value_last_update: this.recordData[this.nodeOptions.date] + } + var Widget = registry.getAny([ + this.recordData[this.nodeOptions.widget], 'abstract', + ]); + this.state = { + specialData: { + margin_x: marginx, + margin_y: marginy, + widget_dimension_x: widgetx, + widget_dimension_y: widgety, + } + } + var widget = new Widget(this, widgetVals); + var element = $(qweb.render( + 'kpi_dashboard.kpi', {widget: widgetVals})); + element.css('background-color', 'white'); + element.css('color', 'black'); + this.$grid.append(element); + widget.appendTo(element) + this.$grid.gridster({ + widget_margins: [ + marginx, + marginy, + ], + widget_base_dimensions: [ + widgetx, + widgety, + ], + cols: 1, + }).data('gridster').disable(); + }, + }); + field_registry.add('kpi', KpiFieldWidget); + return KpiFieldWidget; +}); diff --git a/kpi_dashboard/static/src/js/widget/abstract_widget.js b/kpi_dashboard/static/src/js/widget/abstract_widget.js index b99b2d9e..470054f3 100644 --- a/kpi_dashboard/static/src/js/widget/abstract_widget.js +++ b/kpi_dashboard/static/src/js/widget/abstract_widget.js @@ -54,7 +54,7 @@ odoo.define('kpi_dashboard.AbstractWidget', function (require) { return; this.fillWidget(values); var item = this.$el.find('[data-bind="value_last_update_display"]'); - if (item && values.value_last_update !== undefined) { + if (item && ! values.compute_on_fly && values.value_last_update !== undefined) { var value = field_utils.parse.datetime(values.value_last_update); item.text(value.clone().add( this.getSession().getTZOffset(value), 'minutes').format( @@ -82,7 +82,10 @@ odoo.define('kpi_dashboard.AbstractWidget', function (require) { _onClickDirectAction: function(event) { event.preventDefault(); var $data = $(event.currentTarget).closest('a'); - return this.do_action($($data).data('id')); + var action = this.actions[$($data).data('id')]; + return this.do_action(action.id, { + additional_context: action.context + }); } }); diff --git a/kpi_dashboard/static/src/xml/dashboard.xml b/kpi_dashboard/static/src/xml/dashboard.xml index c9c81541..619d1853 100644 --- a/kpi_dashboard/static/src/xml/dashboard.xml +++ b/kpi_dashboard/static/src/xml/dashboard.xml @@ -19,9 +19,10 @@ - + + diff --git a/kpi_dashboard/tests/test_formula.py b/kpi_dashboard/tests/test_formula.py index abe73d4e..84886251 100644 --- a/kpi_dashboard/tests/test_formula.py +++ b/kpi_dashboard/tests/test_formula.py @@ -57,3 +57,25 @@ result['previous'] = len(model.search([('id', '!=', %s)])) self.assertTrue(value.get("value")) self.assertEqual(value.get("value"), 1) self.assertEqual(value.get("previous"), self.kpi.search_count([]) - 1) + self.assertFalse(self.kpi.history_ids) + + def test_computation_history(self): + self.assertFalse(self.kpi.value) + self.kpi.store_history = True + self.kpi.compute() + self.assertTrue(self.kpi.history_ids) + self.assertEqual(self.kpi.value, {}) + self.kpi.code = """ +result = {} +result['value'] = len(model.search([('id', '=', %s)])) +result['previous'] = len(model.search([('id', '!=', %s)])) + """ % ( + self.kpi.id, + self.kpi.id, + ) + self.kpi.compute() + value = self.kpi.value + self.assertTrue(value.get("value")) + self.assertEqual(value.get("value"), 1) + self.assertEqual(value.get("previous"), self.kpi.search_count([]) - 1) + self.assertTrue(self.kpi.history_ids) diff --git a/kpi_dashboard/views/kpi_dashboard.xml b/kpi_dashboard/views/kpi_dashboard.xml index eba04663..e2f841ad 100644 --- a/kpi_dashboard/views/kpi_dashboard.xml +++ b/kpi_dashboard/views/kpi_dashboard.xml @@ -54,6 +54,9 @@ + + + diff --git a/kpi_dashboard/views/kpi_kpi.xml b/kpi_dashboard/views/kpi_kpi.xml index 72fc0d3c..6f96f5fb 100644 --- a/kpi_dashboard/views/kpi_kpi.xml +++ b/kpi_dashboard/views/kpi_kpi.xml @@ -4,6 +4,55 @@ + + Kpi History + kpi.kpi.history + tree + [('kpi_id', '=', active_id)] + {} + + + + kpi.kpi.history.raw.form (in kpi_dashboard) + kpi.kpi.history + +
+ + + + + + +
+ + + kpi.kpi.history.raw.form (in kpi_dashboard) + kpi.kpi.history + +
+ + +
+
+ + + kpi.kpi.history.tree (in kpi_dashboard) + kpi.kpi.history + + + +