mirror of
https://github.com/OCA/reporting-engine.git
synced 2025-02-16 16:30:38 +02:00
mgmtsystem_kpi rename to kpi (#543)
* kpi migration to odoo 9 * UI error connected * Correction base on last maxime review * correction on @elicoidal review * Warning exception improve in kpi_threshold * Latest Ellicoidal comment implemented * last Ellicoial recommendation done. * Copyright corrected
This commit is contained in:
committed by
EdgarRetes
parent
f193f8d481
commit
96a3ec6e70
9
kpi/models/__init__.py
Normal file
9
kpi/models/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import kpi_category
|
||||
from . import kpi_threshold_range
|
||||
from . import kpi_threshold
|
||||
from . import kpi_history
|
||||
from . import kpi
|
||||
189
kpi/models/kpi.py
Normal file
189
kpi/models/kpi.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from openerp import fields, models, api
|
||||
from openerp.tools.safe_eval import safe_eval
|
||||
from openerp.tools import (
|
||||
DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT,
|
||||
)
|
||||
import re
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_one_value(result):
|
||||
# check if sql query returns only one value
|
||||
if type(result) is dict and 'value' in result.dictfetchone():
|
||||
return True
|
||||
elif type(result) is list and 'value' in result[0]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
RE_SELECT_QUERY = re.compile('.*(' + '|'.join((
|
||||
'INSERT',
|
||||
'UPDATE',
|
||||
'DELETE',
|
||||
'CREATE',
|
||||
'ALTER',
|
||||
'DROP',
|
||||
'GRANT',
|
||||
'REVOKE',
|
||||
'INDEX',
|
||||
)) + ')')
|
||||
|
||||
|
||||
def is_sql_or_ddl_statement(query):
|
||||
"""Check if sql query is a SELECT statement"""
|
||||
return not RE_SELECT_QUERY.match(query.upper())
|
||||
|
||||
|
||||
class KPI(models.Model):
|
||||
"""Key Performance Indicators."""
|
||||
|
||||
_name = "kpi"
|
||||
_description = "Key Performance Indicator"
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
description = fields.Text('Description')
|
||||
category_id = fields.Many2one(
|
||||
'kpi.category',
|
||||
'Category',
|
||||
required=True,
|
||||
)
|
||||
threshold_id = fields.Many2one(
|
||||
'kpi.threshold',
|
||||
'Threshold',
|
||||
required=True,
|
||||
)
|
||||
periodicity = fields.Integer('Periodicity', default=1)
|
||||
|
||||
periodicity_uom = fields.Selection((
|
||||
('hour', 'Hour'),
|
||||
('day', 'Day'),
|
||||
('week', 'Week'),
|
||||
('month', 'Month')
|
||||
), 'Periodicity UoM', required=True, default='day')
|
||||
|
||||
next_execution_date = fields.Datetime(
|
||||
'Next execution date',
|
||||
readonly=True,
|
||||
)
|
||||
value = fields.Float(string='Value',
|
||||
compute="_compute_display_last_kpi_value",
|
||||
)
|
||||
kpi_type = fields.Selection((
|
||||
('python', 'Python'),
|
||||
('local', 'SQL - Local DB'),
|
||||
('external', 'SQL - External DB')
|
||||
), 'KPI Computation Type')
|
||||
|
||||
dbsource_id = fields.Many2one(
|
||||
'base.external.dbsource',
|
||||
'External DB Source',
|
||||
)
|
||||
kpi_code = fields.Text(
|
||||
'KPI Code',
|
||||
help=("SQL code must return the result as 'value' "
|
||||
"(i.e. 'SELECT 5 AS value')."),
|
||||
)
|
||||
history_ids = fields.One2many(
|
||||
'kpi.history',
|
||||
'kpi_id',
|
||||
'History',
|
||||
)
|
||||
active = fields.Boolean(
|
||||
'Active',
|
||||
help=("Only active KPIs will be updated by the scheduler based on"
|
||||
" the periodicity configuration."), default=True
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
|
||||
@api.multi
|
||||
def _compute_display_last_kpi_value(self):
|
||||
history_obj = self.env['kpi.history']
|
||||
for obj in self:
|
||||
history_ids = history_obj.search([("kpi_id", "=", obj.id)])
|
||||
if history_ids:
|
||||
obj.value = obj.history_ids[0].value
|
||||
else:
|
||||
obj.value = 0
|
||||
|
||||
@api.multi
|
||||
def compute_kpi_value(self):
|
||||
for obj in self:
|
||||
kpi_value = 0
|
||||
if obj.kpi_code:
|
||||
if obj.kpi_type == 'local' and is_sql_or_ddl_statement(
|
||||
obj.kpi_code):
|
||||
self.env.cr.execute(obj.kpi_code)
|
||||
dic = self.env.cr.dictfetchall()
|
||||
if is_one_value(dic):
|
||||
kpi_value = dic[0]['value']
|
||||
elif (obj.kpi_type == 'external' and obj.dbsource_id.id and
|
||||
is_sql_or_ddl_statement(obj.kpi_code)):
|
||||
dbsrc_obj = obj.dbsource_id
|
||||
res = dbsrc_obj.execute(obj.kpi_code)
|
||||
if is_one_value(res):
|
||||
kpi_value = res[0]['value']
|
||||
elif obj.kpi_type == 'python':
|
||||
kpi_value = safe_eval(obj.kpi_code)
|
||||
|
||||
threshold_obj = obj.threshold_id
|
||||
values = {
|
||||
'kpi_id': obj.id,
|
||||
'value': kpi_value,
|
||||
'color': threshold_obj.get_color(kpi_value),
|
||||
}
|
||||
history_obj = self.env['kpi.history']
|
||||
history_obj.create(values)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def update_next_execution_date(self):
|
||||
for obj in self:
|
||||
if obj.periodicity_uom == 'hour':
|
||||
delta = timedelta(hours=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'day':
|
||||
delta = timedelta(days=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'week':
|
||||
delta = timedelta(weeks=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'month':
|
||||
delta = timedelta(months=obj.periodicity)
|
||||
else:
|
||||
delta = timedelta()
|
||||
new_date = datetime.now() + delta
|
||||
|
||||
obj.next_execution_date = new_date.strftime(DATETIME_FORMAT)
|
||||
|
||||
return True
|
||||
|
||||
# Method called by the scheduler
|
||||
@api.model
|
||||
def update_kpi_value(self):
|
||||
filters = [
|
||||
'&',
|
||||
'|',
|
||||
('active', '=', True),
|
||||
('next_execution_date', '<=', datetime.now().strftime(
|
||||
DATETIME_FORMAT)),
|
||||
('next_execution_date', '=', False),
|
||||
]
|
||||
if 'filters' in self.env.context:
|
||||
filters.extend(self.env.context['filters'])
|
||||
obj_ids = self.search(filters)
|
||||
res = None
|
||||
|
||||
try:
|
||||
for obj in obj_ids:
|
||||
obj.compute_kpi_value()
|
||||
obj.update_next_execution_date()
|
||||
except Exception:
|
||||
_logger.exception("Failed updating KPI values")
|
||||
|
||||
return res
|
||||
14
kpi/models/kpi_category.py
Normal file
14
kpi/models/kpi_category.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from openerp import fields, models
|
||||
|
||||
|
||||
class KPICategory(models.Model):
|
||||
"""KPI Category."""
|
||||
|
||||
_name = "kpi.category"
|
||||
_description = "KPI Category"
|
||||
name = fields.Char('Name', size=50, required=True)
|
||||
description = fields.Text('Description')
|
||||
29
kpi/models/kpi_history.py
Normal file
29
kpi/models/kpi_history.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from openerp import fields, models
|
||||
|
||||
|
||||
class KPIHistory(models.Model):
|
||||
"""History of the KPI."""
|
||||
|
||||
_name = "kpi.history"
|
||||
_description = "History of the KPI"
|
||||
_order = "date desc"
|
||||
|
||||
name = fields.Char('Name', size=150, required=True,
|
||||
default=fields.Datetime.now(),)
|
||||
kpi_id = fields.Many2one('kpi', 'KPI', required=True)
|
||||
date = fields.Datetime(
|
||||
'Execution Date',
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=fields.Datetime.now()
|
||||
)
|
||||
value = fields.Float('Value', required=True, readonly=True)
|
||||
color = fields.Text('Color', required=True,
|
||||
readonly=True, default='#FFFFFF')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
85
kpi/models/kpi_threshold.py
Normal file
85
kpi/models/kpi_threshold.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from openerp import fields, models, api, exceptions, _
|
||||
|
||||
|
||||
class KPIThreshold(models.Model):
|
||||
"""KPI Threshold."""
|
||||
|
||||
_name = "kpi.threshold"
|
||||
_description = "KPI Threshold"
|
||||
|
||||
@api.multi
|
||||
def _compute_is_valid_threshold(self):
|
||||
result = {}
|
||||
for obj in self:
|
||||
# check if ranges overlap
|
||||
# TODO: This code can be done better
|
||||
for range1 in obj.range_ids:
|
||||
for range2 in obj.range_ids:
|
||||
if (range1.valid and range2.valid and
|
||||
range1.min_value < range2.min_value):
|
||||
result[obj.id] = range1.max_value <= range2.min_value
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def _compute_generate_invalid_message(self):
|
||||
result = {}
|
||||
for obj in self:
|
||||
if obj.valid:
|
||||
result[obj.id] = ""
|
||||
else:
|
||||
result[obj.id] = ("Two of your ranges are overlapping. Please "
|
||||
"make sure your ranges do not overlap.")
|
||||
return result
|
||||
|
||||
name = fields.Char('Name', size=50, required=True)
|
||||
range_ids = fields.Many2many(
|
||||
'kpi.threshold.range',
|
||||
'kpi_threshold_range_rel',
|
||||
'threshold_id',
|
||||
'range_id',
|
||||
'Ranges'
|
||||
)
|
||||
valid = fields.Boolean(string='Valid', required=True,
|
||||
compute="_compute_is_valid_threshold", default=True)
|
||||
invalid_message = fields.Char(string='Message', size=100,
|
||||
compute="_compute_generate_invalid_message")
|
||||
kpi_ids = fields.One2many('kpi', 'threshold_id', 'KPIs')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
|
||||
@api.model
|
||||
def create(self, data):
|
||||
# check if ranges overlap
|
||||
# TODO: This code can be done better
|
||||
range_obj1 = self.env['kpi.threshold.range']
|
||||
range_obj2 = self.env['kpi.threshold.range']
|
||||
if data.get('range_ids'):
|
||||
for range1 in data['range_ids'][0][2]:
|
||||
range_obj1 = range_obj1.browse(range1)
|
||||
for range2 in data['range_ids'][0][2]:
|
||||
range_obj2 = range_obj2.browse(range2)
|
||||
if (range_obj1.valid and range_obj2.valid and
|
||||
range_obj1.min_value < range_obj2.min_value):
|
||||
if range_obj1.max_value > range_obj2.min_value:
|
||||
raise exceptions.Warning(
|
||||
_("Two of your ranges are overlapping."),
|
||||
_("Make sure your ranges do not overlap!")
|
||||
)
|
||||
range_obj2 = self.env['kpi.threshold.range']
|
||||
range_obj1 = self.env['kpi.threshold.range']
|
||||
return super(KPIThreshold, self).create(data)
|
||||
|
||||
@api.multi
|
||||
def get_color(self, kpi_value):
|
||||
color = '#FFFFFF'
|
||||
for obj in self:
|
||||
for range_obj in obj.range_ids:
|
||||
if (range_obj.min_value <= kpi_value <= range_obj.max_value and
|
||||
range_obj.valid):
|
||||
color = range_obj.color
|
||||
return color
|
||||
161
kpi/models/kpi_threshold_range.py
Normal file
161
kpi/models/kpi_threshold_range.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from openerp import fields, models, api
|
||||
from openerp.tools.safe_eval import safe_eval
|
||||
import re
|
||||
|
||||
|
||||
def is_one_value(result):
|
||||
# check if sql query returns only one value
|
||||
if type(result) is dict and 'value' in result.dictfetchone():
|
||||
return True
|
||||
elif type(result) is list and 'value' in result[0]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
RE_SELECT_QUERY = re.compile('.*(' + '|'.join((
|
||||
'INSERT',
|
||||
'UPDATE',
|
||||
'DELETE',
|
||||
'CREATE',
|
||||
'ALTER',
|
||||
'DROP',
|
||||
'GRANT',
|
||||
'REVOKE',
|
||||
'INDEX',
|
||||
)) + ')')
|
||||
|
||||
|
||||
def is_sql_or_ddl_statement(query):
|
||||
"""Check if sql query is a SELECT statement"""
|
||||
return not RE_SELECT_QUERY.match(query.upper())
|
||||
|
||||
|
||||
class KPIThresholdRange(models.Model):
|
||||
"""
|
||||
KPI Threshold Range
|
||||
"""
|
||||
_name = "kpi.threshold.range"
|
||||
_description = "KPI Threshold Range"
|
||||
|
||||
name = fields.Char('Name', size=50, required=True)
|
||||
valid = fields.Boolean(string='Valid', required=True,
|
||||
compute="_compute_is_valid_range", default=True)
|
||||
invalid_message = fields.Char(string='Message', size=100,
|
||||
compute="_compute_generate_invalid_message")
|
||||
min_type = fields.Selection((
|
||||
('static', 'Fixed value'),
|
||||
('python', 'Python Code'),
|
||||
('local', 'SQL - Local DB'),
|
||||
('external', 'SQL - Externa DB'),
|
||||
), 'Min Type', required=True)
|
||||
min_value = fields.Float(string='Minimum', compute="_compute_min_value")
|
||||
min_fixed_value = fields.Float('Minimum')
|
||||
min_code = fields.Text('Minimum Computation Code')
|
||||
min_dbsource_id = fields.Many2one(
|
||||
'base.external.dbsource',
|
||||
'External DB Source',
|
||||
)
|
||||
max_type = fields.Selection((
|
||||
('static', 'Fixed value'),
|
||||
('python', 'Python Code'),
|
||||
('local', 'SQL - Local DB'),
|
||||
('external', 'SQL - External DB'),
|
||||
), 'Max Type', required=True)
|
||||
max_value = fields.Float(string='Maximum', compute="_compute_max_value")
|
||||
max_fixed_value = fields.Float('Maximum')
|
||||
max_code = fields.Text('Maximum Computation Code')
|
||||
max_dbsource_id = fields.Many2one(
|
||||
'base.external.dbsource',
|
||||
'External DB Source',
|
||||
)
|
||||
|
||||
color = fields.Char(
|
||||
string="Color",
|
||||
help="Choose your color"
|
||||
)
|
||||
|
||||
threshold_ids = fields.Many2many(
|
||||
'kpi.threshold',
|
||||
'kpi_threshold_range_rel',
|
||||
'range_id',
|
||||
'threshold_id',
|
||||
'Thresholds',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
|
||||
@api.multi
|
||||
def _compute_min_value(self):
|
||||
result = {}
|
||||
for obj in self:
|
||||
value = None
|
||||
if obj.min_type == 'local' and is_sql_or_ddl_statement(
|
||||
obj.min_code):
|
||||
self.env.cr.execute(obj.min_code)
|
||||
dic = self.env.cr.dictfetchall()
|
||||
if is_one_value(dic):
|
||||
value = dic[0]['value']
|
||||
elif (obj.min_type == 'external' and obj.min_dbsource_id.id and
|
||||
is_sql_or_ddl_statement(obj.min_code)):
|
||||
dbsrc_obj = obj.min_dbsource_id
|
||||
res = dbsrc_obj.execute(obj.min_code)
|
||||
if is_one_value(res):
|
||||
value = res[0]['value']
|
||||
elif obj.min_type == 'python':
|
||||
value = safe_eval(obj.min_code)
|
||||
else:
|
||||
value = obj.min_fixed_value
|
||||
obj.min_value = value
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def _compute_max_value(self):
|
||||
result = {}
|
||||
for obj in self:
|
||||
value = None
|
||||
if obj.max_type == 'local' and is_sql_or_ddl_statement(
|
||||
obj.max_code):
|
||||
self.env.cr.execute(obj.max_code)
|
||||
dic = self.env.cr.dictfetchall()
|
||||
if is_one_value(dic):
|
||||
value = dic[0]['value']
|
||||
elif obj.max_type == 'python':
|
||||
value = safe_eval(obj.max_code)
|
||||
elif (obj.max_type == 'external' and obj.max_dbsource_id.id and
|
||||
is_sql_or_ddl_statement(obj.max_code)):
|
||||
dbsrc_obj = obj.max_dbsource_id
|
||||
res = dbsrc_obj.execute(obj.max_code)
|
||||
if is_one_value(res):
|
||||
value = res[0]['value']
|
||||
else:
|
||||
value = obj.max_fixed_value
|
||||
obj.max_value = value
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def _compute_is_valid_range(self):
|
||||
result = {}
|
||||
for obj in self:
|
||||
if obj.max_value < obj.min_value:
|
||||
obj.valid = False
|
||||
else:
|
||||
obj.valid = True
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def _compute_generate_invalid_message(self):
|
||||
result = {}
|
||||
for obj in self:
|
||||
if obj.valid:
|
||||
obj.invalid_message = ""
|
||||
else:
|
||||
obj.invalid_message = (
|
||||
"Minimum value is greater than the maximum "
|
||||
"value! Please adjust them.")
|
||||
return result
|
||||
Reference in New Issue
Block a user