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 correctedpull/637/head
-
77kpi/README.rst
-
5kpi/__init__.py
-
33kpi/__openerp__.py
-
18kpi/data/kpi.xml
-
0kpi/i18n/fr.po
-
0kpi/i18n/kpi.pot
-
0kpi/i18n/nb.po
-
0kpi/i18n/pt_BR.po
-
0kpi/i18n/vi.po
-
0kpi/images/kpi_computation.png
-
0kpi/images/kpi_definition.png
-
0kpi/images/kpi_range.png
-
0kpi/images/kpi_threshold.png
-
9kpi/models/__init__.py
-
189kpi/models/kpi.py
-
14kpi/models/kpi_category.py
-
29kpi/models/kpi_history.py
-
85kpi/models/kpi_threshold.py
-
161kpi/models/kpi_threshold_range.py
-
11kpi/security/ir.model.access.csv
-
39kpi/security/kpi_security.xml
-
0kpi/static/src/img/icon.png
-
97kpi/views/kpi.xml
-
45kpi/views/kpi_category.xml
-
38kpi/views/kpi_history.xml
-
54kpi/views/kpi_threshold.xml
-
69kpi/views/kpi_threshold_range.xml
-
48kpi/views/menu.xml
-
3mgmtsystem_kpi/__init__.py
-
70mgmtsystem_kpi/__openerp__.py
-
532mgmtsystem_kpi/mgmtsystem_kpi.py
-
300mgmtsystem_kpi/mgmtsystem_kpi_view.xml
-
11mgmtsystem_kpi/security/ir.model.access.csv
-
36mgmtsystem_kpi/security/mgmtsystem_kpi_security.xml
@ -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. |
@ -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 |
@ -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, |
||||
|
} |
@ -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> |
Before Width: 658 | Height: 352 | Size: 41 KiB After Width: 658 | Height: 352 | Size: 41 KiB |
Before Width: 655 | Height: 353 | Size: 46 KiB After Width: 655 | Height: 353 | Size: 46 KiB |
Before Width: 818 | Height: 449 | Size: 27 KiB After Width: 818 | Height: 449 | Size: 27 KiB |
Before Width: 655 | Height: 266 | Size: 25 KiB After Width: 655 | Height: 266 | Size: 25 KiB |
@ -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 |
@ -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 |
@ -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') |
@ -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) |
@ -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 |
@ -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 |
@ -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 |
@ -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> |
Before Width: 100 | Height: 100 | Size: 2.3 KiB After Width: 100 | Height: 100 | Size: 2.3 KiB |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -1,3 +0,0 @@ |
|||||
# -*- encoding: utf-8 -*- |
|
||||
|
|
||||
from . import mgmtsystem_kpi |
|
@ -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: |
|
@ -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', |
|
||||
} |
|
@ -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> |
|
@ -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 |
|
@ -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> |
|