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:
Gervais Naoussi
2016-12-02 08:43:56 -05:00
committed by EdgarRetes
parent f193f8d481
commit 96a3ec6e70
94 changed files with 31286 additions and 0 deletions

9
kpi/models/__init__.py Normal file
View 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
View 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

View 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
View 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)

View 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

View 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