diff --git a/kpi_dashboard/models/kpi_dashboard.py b/kpi_dashboard/models/kpi_dashboard.py index 5751f3ec..aa216754 100644 --- a/kpi_dashboard/models/kpi_dashboard.py +++ b/kpi_dashboard/models/kpi_dashboard.py @@ -167,10 +167,18 @@ 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, } ) + 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..423fc4e5 100644 --- a/kpi_dashboard/models/kpi_kpi.py +++ b/kpi_dashboard/models/kpi_kpi.py @@ -5,7 +5,9 @@ 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 import re +import json class KpiKpi(models.Model): @@ -38,6 +40,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 +77,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 @@ -115,6 +155,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' @@ -139,3 +193,37 @@ class KpiKpiAction(models.Model): 'name': r.action.name }) 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/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/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_kpi.xml b/kpi_dashboard/views/kpi_kpi.xml index 72fc0d3c..ec36a48e 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 + + + +