Browse Source

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
pull/637/head
Gervais Naoussi 8 years ago
committed by Daniel Reis
parent
commit
40694c42d4
  1. 77
      kpi/README.rst
  2. 5
      kpi/__init__.py
  3. 33
      kpi/__openerp__.py
  4. 18
      kpi/data/kpi.xml
  5. 0
      kpi/i18n/fr.po
  6. 0
      kpi/i18n/kpi.pot
  7. 0
      kpi/i18n/nb.po
  8. 0
      kpi/i18n/pt_BR.po
  9. 0
      kpi/i18n/vi.po
  10. 0
      kpi/images/kpi_computation.png
  11. 0
      kpi/images/kpi_definition.png
  12. 0
      kpi/images/kpi_range.png
  13. 0
      kpi/images/kpi_threshold.png
  14. 9
      kpi/models/__init__.py
  15. 189
      kpi/models/kpi.py
  16. 14
      kpi/models/kpi_category.py
  17. 29
      kpi/models/kpi_history.py
  18. 85
      kpi/models/kpi_threshold.py
  19. 161
      kpi/models/kpi_threshold_range.py
  20. 11
      kpi/security/ir.model.access.csv
  21. 39
      kpi/security/kpi_security.xml
  22. 0
      kpi/static/src/img/icon.png
  23. 97
      kpi/views/kpi.xml
  24. 45
      kpi/views/kpi_category.xml
  25. 38
      kpi/views/kpi_history.xml
  26. 54
      kpi/views/kpi_threshold.xml
  27. 69
      kpi/views/kpi_threshold_range.xml
  28. 48
      kpi/views/menu.xml
  29. 3
      mgmtsystem_kpi/__init__.py
  30. 70
      mgmtsystem_kpi/__openerp__.py
  31. 532
      mgmtsystem_kpi/mgmtsystem_kpi.py
  32. 300
      mgmtsystem_kpi/mgmtsystem_kpi_view.xml
  33. 11
      mgmtsystem_kpi/security/ir.model.access.csv
  34. 36
      mgmtsystem_kpi/security/mgmtsystem_kpi_security.xml

77
kpi/README.rst

@ -0,0 +1,77 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
===
KPI
===
This module provides the basis for creating key performance indicators,
including static and dynamic thresholds (SQL query or Python code),
on local and remote data sources.
The module also provides the mecanism to update KPIs automatically.
A scheduler is executed every hour and updates the KPI values, based
on the periodicity of each KPI. KPI computation can also be done
manually.
A threshold is a list of ranges and a range is:
* a name (like Good, Warning, Bad)
* a minimum value (fixed, sql query or python code)
* a maximum value (fixed, sql query or python code)
* color (RGB code like #00FF00 for green, #FFA500 for orange, #FF0000 for red)
Configuration
=============
Users must be added to the appropriate groups within Odoo as follows:
* Creators: Settings > Users > Groups > Management System / User
* Responsible Persons: Settings > Users > Groups > Management System / Approving User
Usage
=====
https://www.youtube.com/watch?v=OC4-y2klzIk
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/128/9.0
Known issues / Roadmap
======================
* Use web_widget_color to display color associated to threshold range
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
Credits
=======
Contributors
------------
* Daniel Reis <dreis.pt@hotmail.com>
* Glen Dromgoole <gdromgoole@tier1engineering.com>
* Loic Lacroix <loic.lacroix@savoirfairelinux.com>
* Sandy Carter <sandy.carter@savoirfairelinux.com>
* Gervais Naoussi <gervaisnaoussi@gmail.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

5
kpi/__init__.py

@ -0,0 +1,5 @@
# -*- 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 models

33
kpi/__openerp__.py

@ -0,0 +1,33 @@
# -*- 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).
{
"name": "Key Performance Indicator",
"version": "9.0.1.0.0",
"author": "Savoir-faire Linux,Odoo Community Association (OCA)",
"website": "http://www.savoirfairelinux.com",
"license": "AGPL-3",
"category": "Report",
"depends": [
'base_external_dbsource',
],
"data": [
'security/ir.model.access.csv',
'security/kpi_security.xml',
'views/kpi_category.xml',
'views/kpi_history.xml',
'views/kpi_threshold_range.xml',
'views/kpi_threshold.xml',
'views/kpi.xml',
'views/menu.xml',
'data/kpi.xml',
],
"images": [
"images/kpi_definition.png",
"images/kpi_computation.png",
"images/kpi_threshold.png",
"images/kpi_range.png",
],
'installable': True,
}

18
kpi/data/kpi.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_kpi_action" model="ir.cron">
<field name="name">Update KPI values</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field eval="'kpi'" name="model"/>
<field eval="'update_kpi_value'" name="function"/>
<field eval="'()'" name="args"/>
</record>
</data>
</odoo>

0
mgmtsystem_kpi/i18n/fr.po → kpi/i18n/fr.po

0
mgmtsystem_kpi/i18n/mgmtsystem_kpi.pot → kpi/i18n/kpi.pot

0
mgmtsystem_kpi/i18n/nb.po → kpi/i18n/nb.po

0
mgmtsystem_kpi/i18n/pt_BR.po → kpi/i18n/pt_BR.po

0
mgmtsystem_kpi/i18n/vi.po → kpi/i18n/vi.po

0
mgmtsystem_kpi/images/kpi_computation.png → kpi/images/kpi_computation.png

Before

Width: 658  |  Height: 352  |  Size: 41 KiB

After

Width: 658  |  Height: 352  |  Size: 41 KiB

0
mgmtsystem_kpi/images/kpi_definition.png → kpi/images/kpi_definition.png

Before

Width: 655  |  Height: 353  |  Size: 46 KiB

After

Width: 655  |  Height: 353  |  Size: 46 KiB

0
mgmtsystem_kpi/images/kpi_range.png → kpi/images/kpi_range.png

Before

Width: 818  |  Height: 449  |  Size: 27 KiB

After

Width: 818  |  Height: 449  |  Size: 27 KiB

0
mgmtsystem_kpi/images/kpi_threshold.png → kpi/images/kpi_threshold.png

Before

Width: 655  |  Height: 266  |  Size: 25 KiB

After

Width: 655  |  Height: 266  |  Size: 25 KiB

9
kpi/models/__init__.py

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

11
kpi/security/ir.model.access.csv

@ -0,0 +1,11 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_kpi_user","kpi.user","model_kpi","base.group_user",1,0,0,0
"access_kpi_history_user","kpi.history.user","model_kpi_history","base.group_user",1,0,0,0
"access_kpi_category_user","kpi.category.user","model_kpi_category","base.group_user",1,0,0,0
"access_kpi_threshold_user","kpi.threshold.user","model_kpi_threshold","base.group_user",1,0,0,0
"access_kpi_threshold_range_user","kpi.threshold.range.user","model_kpi_threshold_range","base.group_user",1,0,0,0
"access_kpi_manager","kpi.manager","model_kpi","base.group_user",1,1,1,1
"access_kpi_category_manager","kpi.category.manager","model_kpi_category","base.group_user",1,1,1,1
"access_kpi_threshold_manager","kpi.threshold.manager","model_kpi_threshold","base.group_user",1,1,1,1
"access_kpi_threshold_range_manager","kpi.threshold.range.manager","model_kpi_threshold_range","base.group_user",1,1,1,1
"access_base_external_dbsource_manager","base.external.dbsource.manager","base_external_dbsource.model_base_external_dbsource","base.group_user",1,1,1,1

39
kpi/security/kpi_security.xml

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 -Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data noupdate="1">
<!-- Rule -->
<record model="ir.rule" id="kpi_rule">
<field name="name">kpi multi-company</field>
<field name="model_id" ref="model_kpi"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="kpi_threshold_range_rule">
<field name="name">kpi_threshold_range multi-company</field>
<field name="model_id" ref="model_kpi_threshold_range"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="kpi_threshold_rule">
<field name="name">kpi_threshold multi-company</field>
<field name="model_id" ref="model_kpi_threshold"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="kpi_history_rule">
<field name="name">kpi_history multi-company</field>
<field name="model_id" ref="model_kpi_history"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
</data>
</odoo>

0
mgmtsystem_kpi/static/src/img/icon.png → kpi/static/src/img/icon.png

Before

Width: 100  |  Height: 100  |  Size: 2.3 KiB

After

Width: 100  |  Height: 100  |  Size: 2.3 KiB

97
kpi/views/kpi.xml

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data>
<!-- KPI -->
<record id="view_kpi_tree" model="ir.ui.view">
<field name="name">kpi.tree</field>
<field name="model">kpi</field>
<field name="arch" type="xml">
<tree string="Key Performance Indicators">
<field name="name"/>
<field name="value" widget="progressbar"/>
<field name="category_id"/>
<field name="kpi_type"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_kpi_filter" model="ir.ui.view">
<field name="name">kpi.filter</field>
<field name="model">kpi</field>
<field name="arch" type="xml">
<search string="KPI">
<group>
<filter name="active"
icon="terp-document-new"
domain="[('active','=',True)]"
string="Active"
help="Only active KPIs are computed by the scheduler based on the periodicity configuration."/>
<separator orientation="vertical"/>
<field name="name"/>
<field name="category_id"/>
<field name="company_id" group="base.group_multi_company"/>
</group>
<newline/>
<group expand="0" string="Group By...">
<filter string="Category" context="{'group_by':'category_id'}"/>
<filter string="Type" context="{'group_by':'kpi_type'}"/>
</group>
</search>
</field>
</record>
<record id="view_kpi_form" model="ir.ui.view">
<field name="name">kpi.form</field>
<field name="model">kpi</field>
<field name="arch" type="xml">
<form string="Key Performance Indicator">
<group col="6" colspan="6">
<field name="name" colspan="2"/>
<field name="threshold_id" colspan="2"/>
<field name="category_id" colspan="2"/>
<newline/>
<field name="value" colspan="2"/>
<button name="compute_kpi_value" string="Compute KPI Now" colspan="2" type="object"/>
<field name="active" colspan="2"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<notebook colspan="6">
<page string="History">
<field name="history_ids" readonly="1" nolabel="1"/>
</page>
<page string="Computation">
<group col="6">
<field name="periodicity" colspan="2"/>
<field name="periodicity_uom" colspan="2"/>
<field name="next_execution_date" colspan="2"/>
<separator string="KPI Computation" colspan="6"/>
<newline/>
<field name="kpi_type" colspan="2"/>
<field name="dbsource_id" colspan="2" attrs="{'invisible' : [('kpi_type', '!=', 'external')]}"/>
<newline/>
<field name="kpi_code" colspan="6"/>
</group>
</page>
<page string="Description">
<field name="description" nolabel="1"/>
</page>
</notebook>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_kpi_list">
<field name="name">Key Performance Indicators</field>
<field name="res_model">kpi</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_kpi_tree"/>
<field name="search_view_id" ref="view_kpi_filter"/>
</record>
</data>
</odoo>

45
kpi/views/kpi_category.xml

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data>
<!-- Categories -->
<record id="view_kpi_category_tree" model="ir.ui.view">
<field name="name">kpi.category.tree</field>
<field name="model">kpi.category</field>
<field name="arch" type="xml">
<tree string="Categories">
<field name="name"/>
</tree>
</field>
</record>
<record id="view_kpi_category_form" model="ir.ui.view">
<field name="name">kpi.category.form</field>
<field name="model">kpi.category</field>
<field name="arch" type="xml">
<form string="Category">
<group col="2" colspan="2">
<field name="name" colspan="2"/>
<newline/>
<field name="description" colspan="2"/>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_category_list">
<field name="name">Categories</field>
<field name="res_model">kpi.category</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_kpi_category_tree"/>
</record>
</data>
</odoo>

38
kpi/views/kpi_history.xml

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data>
<!-- KPI history -->
<record id="view_kpi_history_tree" model="ir.ui.view">
<field name="name">kpi.history.tree</field>
<field name="model">kpi.history</field>
<field name="arch" type="xml">
<tree string="KPI History">
<field name="name"/>
<field name="date"/>
<field name="value"/>
<field name="color" widget="color"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_kpi_history_form" model="ir.ui.view">
<field name="name">kpi.history.form</field>
<field name="model">kpi.history</field>
<field name="arch" type="xml">
<form string="KPI History">
<group col="4" colspan="4">
<field name="kpi_id"/>
<field name="name"/>
<field name="date"/>
<field name="value"/>
<field name="color" widget="color"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</form>
</field>
</record>
</data>
</odoo>

54
kpi/views/kpi_threshold.xml

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data>
<!-- Thresholds -->
<record id="view_kpi_threshold_tree" model="ir.ui.view">
<field name="name">kpi.threshold.tree</field>
<field name="model">kpi.threshold</field>
<field name="arch" type="xml">
<tree string="Thresholds">
<field name="name"/>
<field name="invalid_message"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_kpi_threshold_form" model="ir.ui.view">
<field name="name">kpi.threshold.form</field>
<field name="model">kpi.threshold</field>
<field name="arch" type="xml">
<form string="Threshold">
<group col="6" colspan="6">
<field name="name" colspan="2"/>
<field name="company_id" groups="base.group_multi_company" colspan="2"/>
<newline/>
<separator string="Ranges" colspan="6"/>
<newline/>
<field name="range_ids" nolabel="1" colspan="6"/>
<newline/>
<separator string="KPIs" colspan="6"/>
<newline/>
<field name="kpi_ids" nolabel="1" colspan="6"/>
<newline/>
<field name="invalid_message" nolabel="1" attrs="{'invisible' : [('invalid_message', '=', '')]}" colspan="2"/>
<newline/>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_threshold_list">
<field name="name">Thresholds</field>
<field name="res_model">kpi.threshold</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_kpi_threshold_tree"/>
</record>
</data>
</odoo>

69
kpi/views/kpi_threshold_range.xml

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data>
<!-- Ranges -->
<record id="view_kpi_threshold_range_tree" model="ir.ui.view">
<field name="name">kpi.threshold.range.tree</field>
<field name="model">kpi.threshold.range</field>
<field name="arch" type="xml">
<tree string="Ranges">
<field name="name"/>
<field name="min_value"/>
<field name="max_value"/>
<field name="color" widget="color"/>
<field name="invalid_message"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_kpi_threshold_range_form" model="ir.ui.view">
<field name="name">kpi.threshold.range.form</field>
<field name="model">kpi.threshold.range</field>
<field name="arch" type="xml">
<form string="Range">
<group col="6" colspan="6">
<field name="name"/>
<field name="color"/>
<field name="company_id" groups="base.group_multi_company"/>
<newline/>
<separator string="Minimum" colspan="4"/>
<newline/>
<field name="min_type" colspan="2"/>
<field name="min_fixed_value" colspan="2" attrs="{'invisible' : [('min_type', '!=', 'static')]}"/>
<field name="min_dbsource_id" colspan="2" attrs="{'invisible' : [('min_type', '!=', 'external')]}"/>
<newline/>
<field name="min_code" colspan="4" attrs="{'invisible' : [('min_type', 'NOT IN', ('local','external','python'))]}"/>
<newline/>
<separator string="Maximum" colspan="4"/>
<newline/>
<field name="max_type"/>
<field name="max_fixed_value" attrs="{'invisible' : [('max_type', '!=', 'static')]}"/>
<field name="max_dbsource_id" attrs="{'invisible' : [('max_type', '!=', 'external')]}"/>
<newline/>
<field name="max_code" colspan="4" attrs="{'invisible' : [('max_type', 'NOT IN', ('local','external','python'))]}"/>
<newline/>
<separator string="Thresholds" colspan="4"/>
<field name="threshold_ids" nolabel="1" colspan="4"/>
<field name="invalid_message" nolabel="1" attrs="{'invisible' : [('invalid_message', '=', '')]}" colspan="4"/>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_threshold_range_list">
<field name="name">Ranges</field>
<field name="res_model">kpi.threshold.range</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_kpi_threshold_range_tree"/>
</record>
</data>
</odoo>

48
kpi/views/menu.xml

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<data>
<menuitem id="menu_reporting_kpi"
name="KPI"
action="open_kpi_list"
sequence="15"
parent="base.menu_reporting_dashboard"
groups="base.group_user"/>
<menuitem id="menu_configuration_kpi"
name="KPI"
parent="base.menu_reporting_config"
groups="base.group_user"
sequence="20"/>
<menuitem id="menu_configuration_kpi_category"
name="Categories"
action="open_category_list"
parent="menu_configuration_kpi"
groups="base.group_user"
sequence="10"/>
<menuitem id="menu_configuration_kpi_range"
name="Ranges"
action="open_threshold_range_list"
parent="menu_configuration_kpi"
groups="base.group_user"
sequence="20"/>
<menuitem id="menu_configuration_kpi_dbsource"
name="Data Sources"
action="base_external_dbsource.action_dbsource"
parent="menu_configuration_kpi"
groups="base.group_user"
sequence="20"/>
<menuitem id="menu_configuration_kpi_threshold"
name="Thresholds"
action="open_threshold_list"
parent="menu_configuration_kpi"
groups="base.group_user"
sequence="10"/>
</data>
</odoo>

3
mgmtsystem_kpi/__init__.py

@ -1,3 +0,0 @@
# -*- encoding: utf-8 -*-
from . import mgmtsystem_kpi

70
mgmtsystem_kpi/__openerp__.py

@ -1,70 +0,0 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
"name": "Key Performance Indicator",
"version": "7.0.1.1.1",
"author": "Savoir-faire Linux,Odoo Community Association (OCA)",
"website": "http://www.savoirfairelinux.com",
"license": "AGPL-3",
"category": "Management System",
"complexity": "normal",
"description": """\
This module provides the basis for creating key performance indicators,
including static and dynamic thresholds (SQL query or Python code),
on local and remote data sources.
The module also provides the mecanism to update KPIs automatically.
A scheduler is executed every hour and updates the KPI values, based
on the periodicity of each KPI. KPI computation can also be done
manually.
A threshold is a list of ranges and a range is:
* a name (like Good, Warning, Bad)
* a minimum value (fixed, sql query or python code)
* a maximum value (fixed, sql query or python code)
* color (RGB code like #00FF00 for green, #FFA500 for orange,
#FF0000 for red)
This module depends on:
* base_external_dbsource (available in lp:openobject-extension)
* web_color (available in lp:web-addons)
""",
"depends": [
'mgmtsystem',
'base_external_dbsource',
'web_color',
],
"data": [
'security/ir.model.access.csv',
'security/mgmtsystem_kpi_security.xml',
'mgmtsystem_kpi_view.xml',
],
"images": [
"images/kpi_definition.png",
"images/kpi_computation.png",
"images/kpi_threshold.png",
"images/kpi_range.png",
],
"demo": [],
"test": [],
'installable': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

532
mgmtsystem_kpi/mgmtsystem_kpi.py

@ -1,532 +0,0 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from datetime import datetime, timedelta
from openerp.osv import fields, orm
from openerp.tools.translate import _
from openerp.tools.safe_eval import safe_eval
from openerp.tools import (
DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT,
)
import time
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_select_query(query):
"""Check if sql query is a SELECT statement"""
return not RE_SELECT_QUERY.match(query.upper())
class mgmtsystem_kpi_category(orm.Model):
"""
KPI Category
"""
_name = "mgmtsystem.kpi.category"
_description = "KPI Category"
_columns = {
'name': fields.char('Name', size=50, required=True),
'description': fields.text('Description')
}
class mgmtsystem_kpi_threshold_range(orm.Model):
"""
KPI Threshold Range
"""
_name = "mgmtsystem.kpi.threshold.range"
_description = "KPI Threshold Range"
def compute_min_value(self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids):
value = None
if obj.min_type == 'local' and is_select_query(obj.min_code):
cr.execute(obj.min_code)
dic = cr.dictfetchall()
if is_one_value(dic):
value = dic[0]['value']
elif (obj.min_type == 'external'
and obj.min_dbsource_id.id
and is_select_query(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
result[obj.id] = value
return result
def compute_max_value(self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids, context):
value = None
if obj.max_type == 'local' and is_select_query(obj.max_code):
cr.execute(obj.max_code)
dic = 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_select_query(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
result[obj.id] = value
return result
def _is_valid_range(self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids, context):
if obj.max_value < obj.min_value:
result[obj.id] = False
else:
result[obj.id] = True
return result
def _generate_invalid_message(
self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids, context):
if obj.valid:
result[obj.id] = ""
else:
result[obj.id] = ("Minimum value is greater than the maximum "
"value! Please adjust them.")
return result
_columns = {
'name': fields.char('Name', size=50, required=True),
'valid': fields.function(
_is_valid_range,
string='Valid',
type='boolean',
required=True,
),
'invalid_message': fields.function(
_generate_invalid_message,
string='Message',
type='char',
size=100,
),
'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.function(
compute_min_value,
string='Minimum',
type='float',
),
'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.function(
compute_max_value,
string='Maximum',
type='float',
),
'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(
'Color',
help='RGB code with #',
size=7,
required=True,
),
'threshold_ids': fields.many2many(
'mgmtsystem.kpi.threshold',
'mgmtsystem_kpi_threshold_range_rel',
'range_id',
'threshold_id',
'Thresholds',
),
'company_id': fields.many2one('res.company', 'Company')
}
_defaults = {
'company_id': (
lambda self, cr, uid, c:
self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
'valid': True,
}
class mgmtsystem_kpi_threshold(orm.Model):
"""
KPI Threshold
"""
_name = "mgmtsystem.kpi.threshold"
_description = "KPI Threshold"
def _is_valid_threshold(self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids, context):
# 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
def _generate_invalid_message(
self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids, context):
if obj.valid:
result[obj.id] = ""
else:
result[obj.id] = ("2 of your ranges are overlapping! Please "
"make sure your ranges do not overlap.")
return result
_columns = {
'name': fields.char('Name', size=50, required=True),
'range_ids': fields.many2many(
'mgmtsystem.kpi.threshold.range',
'mgmtsystem_kpi_threshold_range_rel',
'threshold_id',
'range_id',
'Ranges'
),
'valid': fields.function(
_is_valid_threshold,
string='Valid',
type='boolean',
required=True,
),
'invalid_message': fields.function(
_generate_invalid_message,
string='Message',
type='char',
size=100,
),
'kpi_ids': fields.one2many('mgmtsystem.kpi', 'threshold_id', 'KPIs'),
'company_id': fields.many2one('res.company', 'Company')
}
_defaults = {
'company_id': (
lambda self, cr, uid, c:
self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
'valid': True,
}
def create(self, cr, uid, data, context=None):
if context is None:
context = {}
# check if ranges overlap
# TODO: This code can be done better
range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
for range1 in data['range_ids'][0][2]:
range_obj1 = range_obj1.browse(cr, uid, range1, context)
for range2 in data['range_ids'][0][2]:
range_obj2 = range_obj2.browse(cr, uid, range2, context)
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 orm.except_orm(
_("2 of your ranges are overlapping!"),
_("Please make sure your ranges do not overlap!")
)
range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
return super(mgmtsystem_kpi_threshold, self).create(
cr, uid, data, context
)
def get_color(self, cr, uid, ids, kpi_value, context=None):
if context is None:
context = {}
color = '#FFFFFF'
for obj in self.browse(cr, uid, ids, context):
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
class mgmtsystem_kpi_history(orm.Model):
"""
History of the KPI
"""
_name = "mgmtsystem.kpi.history"
_description = "History of the KPI"
_columns = {
'name': fields.char('Name', size=150, required=True),
'kpi_id': fields.many2one('mgmtsystem.kpi', 'KPI', required=True),
'date': fields.datetime(
'Execution Date',
required=True,
readonly=True,
),
'value': fields.float('Value', required=True, readonly=True),
'color': fields.text('Color', required=True, readonly=True),
'company_id': fields.many2one('res.company', 'Company')
}
_defaults = {
'company_id': (
lambda self, cr, uid, c:
self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
'name': lambda *a: time.strftime('%d %B %Y'),
'date': lambda *a: time.strftime(DATETIME_FORMAT),
'color': '#FFFFFF',
}
_order = "date desc"
class mgmtsystem_kpi(orm.Model):
"""
Key Performance Indicators
"""
_name = "mgmtsystem.kpi"
_description = "Key Performance Indicator"
def _display_last_kpi_value(
self, cr, uid, ids, field_name, arg, context=None):
if context is None:
context = {}
result = {}
for obj in self.browse(cr, uid, ids):
if obj.history_ids:
result[obj.id] = obj.history_ids[0].value
else:
result[obj.id] = 0
return result
def compute_kpi_value(self, cr, uid, ids, context=None):
if context is None:
context = {}
for obj in self.browse(cr, uid, ids):
kpi_value = 0
if obj.kpi_type == 'local' and is_select_query(obj.kpi_code):
cr.execute(obj.kpi_code)
dic = cr.dictfetchall()
if is_one_value(dic):
kpi_value = dic[0]['value']
elif (obj.kpi_type == 'external'
and obj.dbsource_id.id
and is_select_query(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.pool.get('mgmtsystem.kpi.history')
history_id = history_obj.create(cr, uid, values, context=context)
obj.history_ids.append(history_id)
return True
def update_next_execution_date(self, cr, uid, ids, context=None):
if context is None:
context = {}
for obj in self.browse(cr, uid, ids):
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
values = {
'next_execution_date': new_date.strftime(DATETIME_FORMAT),
}
obj.write(values)
return True
# Method called by the scheduler
def update_kpi_value(self, cr, uid, ids=None, context=None):
if context is None:
context = {}
if not ids:
filters = [
'&',
'|',
('active', '=', True),
('next_execution_date', '<=',
datetime.now().strftime(DATETIME_FORMAT)),
('next_execution_date', '=', False),
]
if 'filters' in context:
filters.extend(context['filters'])
ids = self.search(cr, uid, filters, context=context)
res = None
try:
res = self.compute_kpi_value(cr, uid, ids, context=context)
self.update_next_execution_date(cr, uid, ids, context=context)
except Exception:
_logger.exception("Failed updating KPI values")
return res
_columns = {
'name': fields.char('Name', size=50, required=True),
'description': fields.text('Description'),
'category_id': fields.many2one(
'mgmtsystem.kpi.category',
'Category',
required=True,
),
'threshold_id': fields.many2one(
'mgmtsystem.kpi.threshold',
'Threshold',
required=True,
),
'periodicity': fields.integer('Periodicity'),
'periodicity_uom': fields.selection((
('hour', 'Hour'),
('day', 'Day'),
('week', 'Week'),
('month', 'Month')
), 'Periodicity UoM', required=True),
'next_execution_date': fields.datetime(
'Next execution date',
readonly=True,
),
'value': fields.function(
_display_last_kpi_value,
string='Value',
type='float',
),
'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(
'mgmtsystem.kpi.history',
'kpi_id',
'History',
),
'active': fields.boolean(
'Active',
help=("Only active KPIs will be updated by the scheduler based on"
" the periodicity configuration."),
),
'company_id': fields.many2one('res.company', 'Company')
}
_defaults = {
'company_id': (
lambda self, cr, uid, c:
self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
'active': True,
'periodicity': 1,
'periodicity_uom': 'day',
}

300
mgmtsystem_kpi/mgmtsystem_kpi_view.xml

@ -1,300 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- KPI -->
<record id="view_mgmtsystem_kpi_tree" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.tree</field>
<field name="model">mgmtsystem.kpi</field>
<field name="arch" type="xml">
<tree string="Key Performance Indicators">
<field name="name"/>
<field name="value" widget="progressbar"/>
<field name="category_id"/>
<field name="kpi_type"/>
<field name="company_id" group="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_mgmtsystem_kpi_filter" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.filter</field>
<field name="model">mgmtsystem.kpi</field>
<field name="arch" type="xml">
<search string="KPI">
<group>
<filter name="active" icon="terp-document-new" domain="[('active','=',True)]" string="Active" help="Only active KPIs are computed by the scheduler based on the periodicity configuration."/>
<separator orientation="vertical"/>
<field name="name"/>
<field name="category_id"/>
<field name="company_id" group="base.group_multi_company"/>
</group>
<newline />
<group expand="0" string="Group By...">
<filter string="Category" context="{'group_by':'category_id'}"/>
<filter string="Type" context="{'group_by':'kpi_type'}"/>
</group>
</search>
</field>
</record>
<record id="view_mgmtsystem_kpi_form" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.form</field>
<field name="model">mgmtsystem.kpi</field>
<field name="arch" type="xml">
<form string="Key Performance Indicator">
<group col="6" colspan="6">
<field name="name" colspan="2"/>
<field name="threshold_id" colspan="2"/>
<field name="category_id" colspan="2"/>
<newline/>
<field name="value" colspan="2"/>
<button name="compute_kpi_value" string="Compute KPI Now" colspan="2" type="object"/>
<field name="active" colspan="2"/>
<field name="company_id" group="base.group_multi_company"/>
</group>
<notebook colspan="6">
<page string="History">
<field name="history_ids" readonly="1" nolabel="1"/>
</page>
<page string="Computation">
<group col="6">
<field name="periodicity" colspan="2"/>
<field name="periodicity_uom" colspan="2"/>
<field name="next_execution_date" colspan="2"/>
<separator string="KPI Computation" colspan="6"/>
<newline/>
<field name="kpi_type" colspan="2"/>
<field name="dbsource_id" colspan="2" attrs="{'invisible' : [('kpi_type', '!=', 'external')]}"/>
<newline/>
<field name="kpi_code" colspan="6"/>
</group>
</page>
<page string="Description">
<field name="description" nolabel="1"/>
</page>
</notebook>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_mgmtsystem_kpi_list">
<field name="name">Key Performance Indicators</field>
<field name="res_model">mgmtsystem.kpi</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_mgmtsystem_kpi_tree"/>
<field name="search_view_id" ref="view_mgmtsystem_kpi_filter"/>
</record>
<menuitem id="menu_mgmtsystem_kpi"
name="KPI"
action="open_mgmtsystem_kpi_list"
sequence="15"
parent="mgmtsystem.menu_mgmtsystem_main"
groups="base.group_user"/>
<!-- KPI history -->
<record id="view_mgmtsystem_kpi_history_tree" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.history.tree</field>
<field name="model">mgmtsystem.kpi.history</field>
<field name="arch" type="xml">
<tree string="KPI History">
<field name="name"/>
<field name="date"/>
<field name="value"/>
<field name="color" widget="color"/>
<field name="company_id" group="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- Configuration menu -->
<menuitem id="menu_mgmtsystem_configuration_kpi"
name="KPI"
parent="mgmtsystem.menu_mgmtsystem_configuration"
groups="mgmtsystem.group_mgmtsystem_manager"
sequence="20"/>
<!-- Categories -->
<record id="view_mgmtsystem_kpi_category_tree" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.category.tree</field>
<field name="model">mgmtsystem.kpi.category</field>
<field name="arch" type="xml">
<tree string="Categories">
<field name="name"/>
</tree>
</field>
</record>
<record id="view_mgmtsystem_kpi_category_form" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.category.form</field>
<field name="model">mgmtsystem.kpi.category</field>
<field name="arch" type="xml">
<form string="Category">
<field name="name"/>
<newline/>
<field name="description" colspan="4"/>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_mgmtsystem_category_list">
<field name="name">Categories</field>
<field name="res_model">mgmtsystem.kpi.category</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_mgmtsystem_kpi_category_tree"/>
</record>
<menuitem id="menu_mgmtsystem_configuration_kpi_category"
name="Categories"
action="open_mgmtsystem_category_list"
parent="menu_mgmtsystem_configuration_kpi"
groups="mgmtsystem.group_mgmtsystem_manager"
sequence="10"/>
<!-- Thresholds -->
<record id="view_mgmtsystem_kpi_threshold_tree" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.threshold.tree</field>
<field name="model">mgmtsystem.kpi.threshold</field>
<field name="arch" type="xml">
<tree string="Thresholds">
<field name="name"/>
<field name="invalid_message"/>
<field name="company_id" group="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_mgmtsystem_kpi_threshold_form" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.threshold.form</field>
<field name="model">mgmtsystem.kpi.threshold</field>
<field name="arch" type="xml">
<form string="Threshold">
<field name="name"/>
<newline/>
<separator string="Ranges" colspan="2"/>
<newline/>
<field name="range_ids" nolabel="1" colspan="2"/>
<newline/>
<separator string="KPIs" colspan="2"/>
<newline/>
<field name="kpi_ids" nolabel="1" colspan="2"/>
<newline/>
<field name="invalid_message" nolabel="1" attrs="{'invisible' : [('invalid_message', '=', '')]}" colspan="2"/>
<newline/>
<field name="company_id" group="base.group_multi_company"/>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_mgmtsystem_threshold_list">
<field name="name">Thresholds</field>
<field name="res_model">mgmtsystem.kpi.threshold</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_mgmtsystem_kpi_threshold_tree"/>
</record>
<menuitem id="menu_mgmtsystem_configuration_kpi_threshold"
name="Thresholds"
action="open_mgmtsystem_threshold_list"
parent="menu_mgmtsystem_configuration_kpi"
groups="mgmtsystem.group_mgmtsystem_manager"
sequence="10"/>
<!-- Ranges -->
<record id="view_mgmtsystem_kpi_threshold_range_tree" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.threshold.range.tree</field>
<field name="model">mgmtsystem.kpi.threshold.range</field>
<field name="arch" type="xml">
<tree string="Ranges">
<field name="name"/>
<field name="min_value"/>
<field name="max_value"/>
<field name="color" widget="color"/>
<field name="invalid_message"/>
<field name="company_id" group="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_mgmtsystem_kpi_threshold_range_form" model="ir.ui.view">
<field name="name">mgmtsystem.kpi.threshold.range.form</field>
<field name="model">mgmtsystem.kpi.threshold.range</field>
<field name="arch" type="xml">
<form string="Range">
<field name="name"/>
<field name="color"/>
<field name="company_id" group="base.group_multi_company"/>
<newline/>
<separator string="Minimum" colspan="4"/>
<newline/>
<field name="min_type"/>
<field name="min_fixed_value" attrs="{'invisible' : [('min_type', '!=', 'static')]}"/>
<field name="min_dbsource_id" attrs="{'invisible' : [('min_type', '!=', 'external')]}"/>
<newline/>
<field name="min_code" colspan="4" attrs="{'invisible' : [('min_type', 'NOT IN', ('local','external','python'))]}"/>
<newline/>
<separator string="Maximum" colspan="4"/>
<newline/>
<field name="max_type"/>
<field name="max_fixed_value" attrs="{'invisible' : [('max_type', '!=', 'static')]}"/>
<field name="max_dbsource_id" attrs="{'invisible' : [('max_type', '!=', 'external')]}"/>
<newline/>
<field name="max_code" colspan="4" attrs="{'invisible' : [('max_type', 'NOT IN', ('local','external','python'))]}"/>
<newline/>
<separator string="Thresholds" colspan="4"/>
<field name="threshold_ids" nolabel="1" colspan="4"/>
<field name="invalid_message" nolabel="1" attrs="{'invisible' : [('invalid_message', '=', '')]}" colspan="4"/>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_mgmtsystem_threshold_range_list">
<field name="name">Ranges</field>
<field name="res_model">mgmtsystem.kpi.threshold.range</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_mgmtsystem_kpi_threshold_range_tree"/>
</record>
<menuitem id="menu_mgmtsystem_configuration_kpi_range"
name="Ranges"
action="open_mgmtsystem_threshold_range_list"
parent="menu_mgmtsystem_configuration_kpi"
groups="mgmtsystem.group_mgmtsystem_manager"
sequence="20"/>
<menuitem id="menu_mgmtsystem_configuration_kpi_dbsource"
name="Data Sources"
action="base_external_dbsource.action_dbsource"
parent="menu_mgmtsystem_configuration_kpi"
groups="mgmtsystem.group_mgmtsystem_manager"
sequence="20"/>
</data>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_mgmtsystem_kpi_action" model="ir.cron">
<field name="name">Update KPI values</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field eval="'mgmtsystem.kpi'" name="model"/>
<field eval="'update_kpi_value'" name="function"/>
<field eval="'()'" name="args"/>
</record>
</data>
</openerp>

11
mgmtsystem_kpi/security/ir.model.access.csv

@ -1,11 +0,0 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_mgmtsystem_kpi_user","mgmtsystem.kpi.user","model_mgmtsystem_kpi","base.group_user",1,0,0,0
"access_mgmtsystem_kpi_history_user","mgmtsystem.kpi.history.user","model_mgmtsystem_kpi_history","base.group_user",1,0,0,0
"access_mgmtsystem_kpi_category_user","mgmtsystem.kpi.category.user","model_mgmtsystem_kpi_category","base.group_user",1,0,0,0
"access_mgmtsystem_kpi_threshold_user","mgmtsystem.kpi.threshold.user","model_mgmtsystem_kpi_threshold","base.group_user",1,0,0,0
"access_mgmtsystem_kpi_threshold_range_user","mgmtsystem.kpi.threshold.range.user","model_mgmtsystem_kpi_threshold_range","base.group_user",1,0,0,0
"access_mgmtsystem_kpi_manager","mgmtsystem.kpi.manager","model_mgmtsystem_kpi","mgmtsystem.group_mgmtsystem_manager",1,1,1,1
"access_mgmtsystem_kpi_category_manager","mgmtsystem.kpi.category.manager","model_mgmtsystem_kpi_category","mgmtsystem.group_mgmtsystem_manager",1,1,1,1
"access_mgmtsystem_kpi_threshold_manager","mgmtsystem.kpi.threshold.manager","model_mgmtsystem_kpi_threshold","mgmtsystem.group_mgmtsystem_manager",1,1,1,1
"access_mgmtsystem_kpi_threshold_range_manager","mgmtsystem.kpi.threshold.range.manager","model_mgmtsystem_kpi_threshold_range","mgmtsystem.group_mgmtsystem_manager",1,1,1,1
"access_base_external_dbsource_mgmtsystem_manager","base.external.dbsource.manager","base_external_dbsource.model_base_external_dbsource","mgmtsystem.group_mgmtsystem_manager",1,1,1,1

36
mgmtsystem_kpi/security/mgmtsystem_kpi_security.xml

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<!-- Rule -->
<record model="ir.rule" id="mgmtsystem_kpi_rule">
<field name="name">mgmtsystem_kpi multi-company</field>
<field name="model_id" ref="model_mgmtsystem_kpi"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="mgmtsystem_kpi_threshold_range_rule">
<field name="name">mgmtsystem_kpi_threshold_range multi-company</field>
<field name="model_id" ref="model_mgmtsystem_kpi_threshold_range"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="mgmtsystem_kpi_threshold_rule">
<field name="name">mgmtsystem_kpi_threshold multi-company</field>
<field name="model_id" ref="model_mgmtsystem_kpi_threshold"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="mgmtsystem_kpi_history_rule">
<field name="name">mgmtsystem_kpi_history multi-company</field>
<field name="model_id" ref="model_mgmtsystem_kpi_history"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save