mirror of
https://github.com/OCA/reporting-engine.git
synced 2025-02-16 16:30:38 +02:00
[IMP] kpi: black, isort
This commit is contained in:
@@ -8,20 +8,17 @@
|
||||
"website": "http://www.savoirfairelinux.com",
|
||||
"license": "AGPL-3",
|
||||
"category": "Report",
|
||||
"depends": [
|
||||
'base_external_dbsource',
|
||||
'web_widget_color',
|
||||
],
|
||||
"depends": ["base_external_dbsource", "web_widget_color",],
|
||||
"data": [
|
||||
'security/kpi_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/kpi_category_views.xml',
|
||||
'views/kpi_history_views.xml',
|
||||
'views/kpi_threshold_range_views.xml',
|
||||
'views/kpi_threshold_views.xml',
|
||||
'views/kpi_views.xml',
|
||||
'views/menu.xml',
|
||||
'data/kpi_data.xml',
|
||||
"security/kpi_security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/kpi_category_views.xml",
|
||||
"views/kpi_history_views.xml",
|
||||
"views/kpi_threshold_range_views.xml",
|
||||
"views/kpi_threshold_views.xml",
|
||||
"views/kpi_views.xml",
|
||||
"views/menu.xml",
|
||||
"data/kpi_data.xml",
|
||||
],
|
||||
"images": [
|
||||
"images/kpi_definition.png",
|
||||
@@ -29,5 +26,5 @@
|
||||
"images/kpi_threshold.png",
|
||||
"images/kpi_range.png",
|
||||
],
|
||||
'installable': True,
|
||||
"installable": True,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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 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="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_kpi"/>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_kpi" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.update_kpi_value()</field>
|
||||
</record>
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
# 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
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import fields, models, api
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo.tools import (
|
||||
DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT,
|
||||
)
|
||||
import re
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
_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():
|
||||
if type(result) is dict and "value" in result.dictfetchone():
|
||||
return True
|
||||
elif type(result) is list and 'value' in result[0]:
|
||||
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',
|
||||
)) + ')')
|
||||
RE_SELECT_QUERY = re.compile(
|
||||
".*("
|
||||
+ "|".join(
|
||||
(
|
||||
"INSERT",
|
||||
"UPDATE",
|
||||
"DELETE",
|
||||
"CREATE",
|
||||
"ALTER",
|
||||
"DROP",
|
||||
"GRANT",
|
||||
"REVOKE",
|
||||
"INDEX",
|
||||
)
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
|
||||
def is_sql_or_ddl_statement(query):
|
||||
@@ -47,70 +54,63 @@ class KPI(models.Model):
|
||||
_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)
|
||||
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((
|
||||
('minute', 'Minute'),
|
||||
('hour', 'Hour'),
|
||||
('day', 'Day'),
|
||||
('week', 'Week'),
|
||||
('month', 'Month')
|
||||
), 'Periodicity UoM', required=True, default='day')
|
||||
|
||||
next_execution_date = fields.Datetime(
|
||||
'Next execution date',
|
||||
readonly=True,
|
||||
periodicity_uom = fields.Selection(
|
||||
(
|
||||
("minute", "Minute"),
|
||||
("hour", "Hour"),
|
||||
("day", "Day"),
|
||||
("week", "Week"),
|
||||
("month", "Month"),
|
||||
),
|
||||
"Periodicity UoM",
|
||||
required=True,
|
||||
default="day",
|
||||
)
|
||||
value = fields.Float(string='Value',
|
||||
compute="_compute_display_last_kpi_value",
|
||||
)
|
||||
color = fields.Text('Color', compute="_compute_display_last_kpi_value",)
|
||||
|
||||
next_execution_date = fields.Datetime("Next execution date", readonly=True,)
|
||||
value = fields.Float(string="Value", compute="_compute_display_last_kpi_value",)
|
||||
color = fields.Text("Color", compute="_compute_display_last_kpi_value",)
|
||||
last_execution = fields.Datetime(
|
||||
'Last execution', compute="_compute_display_last_kpi_value",)
|
||||
kpi_type = fields.Selection((
|
||||
('python', 'Python'),
|
||||
('local', 'SQL - Local DB'),
|
||||
('external', 'SQL - External DB')
|
||||
), 'KPI Computation Type')
|
||||
"Last execution", 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',
|
||||
)
|
||||
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',
|
||||
"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
|
||||
"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)
|
||||
"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']
|
||||
history_obj = self.env["kpi.history"]
|
||||
for obj in self:
|
||||
history_ids = history_obj.search([("kpi_id", "=", obj.id)])
|
||||
if history_ids:
|
||||
@@ -120,7 +120,7 @@ class KPI(models.Model):
|
||||
obj.last_execution = his.date
|
||||
else:
|
||||
obj.value = 0
|
||||
obj.color = '#FFFFFF'
|
||||
obj.color = "#FFFFFF"
|
||||
obj.last_execution = False
|
||||
|
||||
@api.multi
|
||||
@@ -128,51 +128,53 @@ class KPI(models.Model):
|
||||
self.ensure_one()
|
||||
kpi_value = 0
|
||||
if self.kpi_code:
|
||||
if self.kpi_type == 'local' and is_sql_or_ddl_statement(
|
||||
self.kpi_code):
|
||||
if self.kpi_type == "local" and is_sql_or_ddl_statement(self.kpi_code):
|
||||
self.env.cr.execute(self.kpi_code)
|
||||
dic = self.env.cr.dictfetchall()
|
||||
if is_one_value(dic):
|
||||
kpi_value = dic[0]['value']
|
||||
elif (self.kpi_type == 'external' and self.dbsource_id.id and
|
||||
is_sql_or_ddl_statement(self.kpi_code)):
|
||||
kpi_value = dic[0]["value"]
|
||||
elif (
|
||||
self.kpi_type == "external"
|
||||
and self.dbsource_id.id
|
||||
and is_sql_or_ddl_statement(self.kpi_code)
|
||||
):
|
||||
dbsrc_obj = self.dbsource_id
|
||||
res = dbsrc_obj.execute(self.kpi_code)
|
||||
if is_one_value(res):
|
||||
kpi_value = res[0]['value']
|
||||
elif self.kpi_type == 'python':
|
||||
kpi_value = safe_eval(self.kpi_code, {'self': self})
|
||||
kpi_value = res[0]["value"]
|
||||
elif self.kpi_type == "python":
|
||||
kpi_value = safe_eval(self.kpi_code, {"self": self})
|
||||
if isinstance(kpi_value, dict):
|
||||
res = kpi_value
|
||||
else:
|
||||
threshold_obj = self.threshold_id
|
||||
res = {
|
||||
'value': kpi_value,
|
||||
'color': threshold_obj.get_color(kpi_value),
|
||||
"value": kpi_value,
|
||||
"color": threshold_obj.get_color(kpi_value),
|
||||
}
|
||||
res.update({'kpi_id': self.id})
|
||||
res.update({"kpi_id": self.id})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def compute_kpi_value(self):
|
||||
for obj in self:
|
||||
history_vals = obj._get_kpi_value()
|
||||
history_obj = self.env['kpi.history']
|
||||
history_obj = self.env["kpi.history"]
|
||||
history_obj.sudo().create(history_vals)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def update_next_execution_date(self):
|
||||
for obj in self:
|
||||
if obj.periodicity_uom == 'hour':
|
||||
if obj.periodicity_uom == "hour":
|
||||
delta = relativedelta(hours=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'minute':
|
||||
elif obj.periodicity_uom == "minute":
|
||||
delta = relativedelta(minutes=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'day':
|
||||
elif obj.periodicity_uom == "day":
|
||||
delta = relativedelta(days=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'week':
|
||||
elif obj.periodicity_uom == "week":
|
||||
delta = relativedelta(weeks=obj.periodicity)
|
||||
elif obj.periodicity_uom == 'month':
|
||||
elif obj.periodicity_uom == "month":
|
||||
delta = relativedelta(months=obj.periodicity)
|
||||
else:
|
||||
delta = relativedelta()
|
||||
@@ -186,15 +188,14 @@ class KPI(models.Model):
|
||||
@api.model
|
||||
def update_kpi_value(self):
|
||||
filters = [
|
||||
'&',
|
||||
'|',
|
||||
('active', '=', True),
|
||||
('next_execution_date', '<=', datetime.now().strftime(
|
||||
DATETIME_FORMAT)),
|
||||
('next_execution_date', '=', False),
|
||||
"&",
|
||||
"|",
|
||||
("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'])
|
||||
if "filters" in self.env.context:
|
||||
filters.extend(self.env.context["filters"])
|
||||
obj_ids = self.search(filters)
|
||||
res = None
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@ class KPICategory(models.Model):
|
||||
|
||||
_name = "kpi.category"
|
||||
_description = "KPI Category"
|
||||
name = fields.Char('Name', size=50, required=True)
|
||||
description = fields.Text('Description')
|
||||
name = fields.Char("Name", size=50, required=True)
|
||||
description = fields.Text("Description")
|
||||
|
||||
@@ -11,18 +11,16 @@ class KPIHistory(models.Model):
|
||||
_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)
|
||||
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',
|
||||
"Execution Date",
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda r: fields.Datetime.now()
|
||||
default=lambda r: fields.Datetime.now(),
|
||||
)
|
||||
value = fields.Float('Value', required=True, readonly=True)
|
||||
color = fields.Text('Color', required=True,
|
||||
readonly=True, default='#FFFFFF')
|
||||
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)
|
||||
"res.company", "Company", default=lambda self: self.env.user.company_id.id
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models, api, exceptions, _
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class KPIThreshold(models.Model):
|
||||
@@ -20,9 +20,11 @@ class KPIThreshold(models.Model):
|
||||
if not range1.valid:
|
||||
obj.valid = False
|
||||
break
|
||||
for range2 in (obj.range_ids-range1):
|
||||
if (range1.max_value >= range2.min_value and
|
||||
range1.min_value <= range2.max_value):
|
||||
for range2 in obj.range_ids - range1:
|
||||
if (
|
||||
range1.max_value >= range2.min_value
|
||||
and range1.min_value <= range2.max_value
|
||||
):
|
||||
obj.valid = False
|
||||
break
|
||||
if obj.valid:
|
||||
@@ -30,53 +32,64 @@ class KPIThreshold(models.Model):
|
||||
else:
|
||||
obj.invalid_message = (
|
||||
"Some ranges are invalid or overlapping. "
|
||||
"Please make sure your ranges do not overlap.")
|
||||
"Please make sure your ranges do not overlap."
|
||||
)
|
||||
|
||||
name = fields.Char('Name', size=50, required=True)
|
||||
name = fields.Char("Name", size=50, required=True)
|
||||
range_ids = fields.Many2many(
|
||||
'kpi.threshold.range',
|
||||
'kpi_threshold_range_rel',
|
||||
'threshold_id',
|
||||
'range_id',
|
||||
'Ranges'
|
||||
"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_is_valid_threshold")
|
||||
kpi_ids = fields.One2many('kpi', 'threshold_id', 'KPIs')
|
||||
valid = fields.Boolean(
|
||||
string="Valid",
|
||||
required=True,
|
||||
compute="_compute_is_valid_threshold",
|
||||
default=True,
|
||||
)
|
||||
invalid_message = fields.Char(
|
||||
string="Message", size=100, compute="_compute_is_valid_threshold"
|
||||
)
|
||||
kpi_ids = fields.One2many("kpi", "threshold_id", "KPIs")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
"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 = 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]:
|
||||
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.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!")
|
||||
_("Make sure your ranges do not overlap!"),
|
||||
)
|
||||
range_obj2 = self.env['kpi.threshold.range']
|
||||
range_obj1 = self.env['kpi.threshold.range']
|
||||
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'
|
||||
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):
|
||||
if (
|
||||
range_obj.min_value <= kpi_value <= range_obj.max_value
|
||||
and range_obj.valid
|
||||
):
|
||||
color = range_obj.color
|
||||
return color
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
# Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models, api
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
def is_one_value(result):
|
||||
# check if sql query returns only one value
|
||||
if type(result) is dict and 'value' in result.dictfetchone():
|
||||
if type(result) is dict and "value" in result.dictfetchone():
|
||||
return True
|
||||
elif type(result) is list and 'value' in result[0]:
|
||||
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',
|
||||
)) + ')')
|
||||
RE_SELECT_QUERY = re.compile(
|
||||
".*("
|
||||
+ "|".join(
|
||||
(
|
||||
"INSERT",
|
||||
"UPDATE",
|
||||
"DELETE",
|
||||
"CREATE",
|
||||
"ALTER",
|
||||
"DROP",
|
||||
"GRANT",
|
||||
"REVOKE",
|
||||
"INDEX",
|
||||
)
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
|
||||
def is_sql_or_ddl_statement(query):
|
||||
@@ -38,60 +45,64 @@ 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_is_valid_range")
|
||||
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 Value',
|
||||
compute="_compute_min_value")
|
||||
min_fixed_value = fields.Float('Minimum Fixed Value')
|
||||
min_code = fields.Text('Minimum Computation Code')
|
||||
min_error = fields.Char('Minimum Error', compute="_compute_min_value")
|
||||
min_dbsource_id = fields.Many2one(
|
||||
'base.external.dbsource',
|
||||
'External DB Source Minimum',
|
||||
name = fields.Char("Name", size=50, required=True)
|
||||
valid = fields.Boolean(
|
||||
string="Valid", required=True, compute="_compute_is_valid_range", default=True
|
||||
)
|
||||
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 Value',
|
||||
compute="_compute_max_value")
|
||||
max_fixed_value = fields.Float('Maximum Fixed Value')
|
||||
max_code = fields.Text('Maximum Computation Code')
|
||||
max_error = fields.Char('Maximum Error', compute="_compute_max_value")
|
||||
invalid_message = fields.Char(
|
||||
string="Message", size=100, compute="_compute_is_valid_range"
|
||||
)
|
||||
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 Value", compute="_compute_min_value")
|
||||
min_fixed_value = fields.Float("Minimum Fixed Value")
|
||||
min_code = fields.Text("Minimum Computation Code")
|
||||
min_error = fields.Char("Minimum Error", compute="_compute_min_value")
|
||||
min_dbsource_id = fields.Many2one(
|
||||
"base.external.dbsource", "External DB Source Minimum",
|
||||
)
|
||||
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 Value", compute="_compute_max_value")
|
||||
max_fixed_value = fields.Float("Maximum Fixed Value")
|
||||
max_code = fields.Text("Maximum Computation Code")
|
||||
max_error = fields.Char("Maximum Error", compute="_compute_max_value")
|
||||
max_dbsource_id = fields.Many2one(
|
||||
'base.external.dbsource',
|
||||
'External DB Source Maximum',
|
||||
"base.external.dbsource", "External DB Source Maximum",
|
||||
)
|
||||
|
||||
color = fields.Char(
|
||||
string="Color",
|
||||
help="Choose your color"
|
||||
)
|
||||
color = fields.Char(string="Color", help="Choose your color")
|
||||
|
||||
threshold_ids = fields.Many2many(
|
||||
'kpi.threshold',
|
||||
'kpi_threshold_range_rel',
|
||||
'range_id',
|
||||
'threshold_id',
|
||||
'Thresholds',
|
||||
"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)
|
||||
"res.company", "Company", default=lambda self: self.env.user.company_id.id
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def _compute_min_value(self):
|
||||
@@ -99,19 +110,21 @@ class KPIThresholdRange(models.Model):
|
||||
value = None
|
||||
error = None
|
||||
try:
|
||||
if obj.min_type == 'local' and is_sql_or_ddl_statement(
|
||||
obj.min_code):
|
||||
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)):
|
||||
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 = res[0]["value"]
|
||||
elif obj.min_type == "python":
|
||||
value = safe_eval(obj.min_code)
|
||||
else:
|
||||
value = obj.min_fixed_value
|
||||
@@ -127,19 +140,21 @@ class KPIThresholdRange(models.Model):
|
||||
value = None
|
||||
error = None
|
||||
try:
|
||||
if obj.max_type == 'local' and is_sql_or_ddl_statement(
|
||||
obj.max_code):
|
||||
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 == 'external' and obj.max_dbsource_id.id and
|
||||
is_sql_or_ddl_statement(obj.max_code)):
|
||||
value = dic[0]["value"]
|
||||
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']
|
||||
elif obj.max_type == 'python':
|
||||
value = res[0]["value"]
|
||||
elif obj.max_type == "python":
|
||||
value = safe_eval(obj.max_code)
|
||||
else:
|
||||
value = obj.max_fixed_value
|
||||
@@ -156,12 +171,14 @@ class KPIThresholdRange(models.Model):
|
||||
obj.valid = False
|
||||
obj.invalid_message = (
|
||||
"Either minimum or maximum value has "
|
||||
"computation errors. Please fix them.")
|
||||
"computation errors. Please fix them."
|
||||
)
|
||||
elif obj.max_value < obj.min_value:
|
||||
obj.valid = False
|
||||
obj.invalid_message = (
|
||||
"Minimum value is greater than the maximum "
|
||||
"value! Please adjust them.")
|
||||
"value! Please adjust them."
|
||||
)
|
||||
else:
|
||||
obj.valid = True
|
||||
obj.invalid_message = ""
|
||||
|
||||
@@ -12,4 +12,4 @@ 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)
|
||||
* color (RGB code like #00FF00 for green, #FFA500 for orange, #FF0000 for red)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
Example of usage:
|
||||
https://www.youtube.com/watch?v=OC4-y2klzIk
|
||||
https://www.youtube.com/watch?v=OC4-y2klzIk
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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="0">
|
||||
<record id="group_kpi_manager" model="res.groups">
|
||||
<field name="name">Manage KPI's</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
|
||||
<field name="category_id" ref="base.module_category_hidden" />
|
||||
<field name="users" eval="[(4, ref('base.user_admin'))]" />
|
||||
</record>
|
||||
</data>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -4,113 +4,125 @@ from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestKPI(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestKPI, self).setUp()
|
||||
|
||||
def test_invalid_threshold_range(self):
|
||||
range1 = self.env['kpi.threshold.range'].create({
|
||||
'name': 'Range1',
|
||||
'min_type': 'static',
|
||||
'max_type': 'static',
|
||||
'min_fixed_value': 3,
|
||||
'max_fixed_value': 1,
|
||||
})
|
||||
range2 = self.env['kpi.threshold.range'].create({
|
||||
'name': 'Range2',
|
||||
'min_type': 'static',
|
||||
'max_type': 'static',
|
||||
'min_fixed_value': 4,
|
||||
'max_fixed_value': 10,
|
||||
})
|
||||
range1 = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "Range1",
|
||||
"min_type": "static",
|
||||
"max_type": "static",
|
||||
"min_fixed_value": 3,
|
||||
"max_fixed_value": 1,
|
||||
}
|
||||
)
|
||||
range2 = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "Range2",
|
||||
"min_type": "static",
|
||||
"max_type": "static",
|
||||
"min_fixed_value": 4,
|
||||
"max_fixed_value": 10,
|
||||
}
|
||||
)
|
||||
self.assertFalse(range1.valid)
|
||||
self.assertTrue(range2.valid)
|
||||
|
||||
def test_invalid_threshold(self):
|
||||
range1 = self.env['kpi.threshold.range'].create({
|
||||
'name': 'Range1',
|
||||
'min_type': 'static',
|
||||
'max_type': 'static',
|
||||
'min_fixed_value': 1,
|
||||
'max_fixed_value': 4,
|
||||
})
|
||||
range2 = self.env['kpi.threshold.range'].create({
|
||||
'name': 'Range2',
|
||||
'min_type': 'static',
|
||||
'max_type': 'static',
|
||||
'min_fixed_value': 4,
|
||||
'max_fixed_value': 10,
|
||||
})
|
||||
range3 = self.env['kpi.threshold.range'].create({
|
||||
'name': 'Range3',
|
||||
'min_type': 'static',
|
||||
'max_type': 'static',
|
||||
'min_fixed_value': 1,
|
||||
'max_fixed_value': 3,
|
||||
})
|
||||
range_invalid = self.env['kpi.threshold.range'].create({
|
||||
'name': 'RangeInvalid',
|
||||
'min_type': 'static',
|
||||
'max_type': 'static',
|
||||
'min_fixed_value': 3,
|
||||
'max_fixed_value': 1,
|
||||
})
|
||||
range1 = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "Range1",
|
||||
"min_type": "static",
|
||||
"max_type": "static",
|
||||
"min_fixed_value": 1,
|
||||
"max_fixed_value": 4,
|
||||
}
|
||||
)
|
||||
range2 = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "Range2",
|
||||
"min_type": "static",
|
||||
"max_type": "static",
|
||||
"min_fixed_value": 4,
|
||||
"max_fixed_value": 10,
|
||||
}
|
||||
)
|
||||
range3 = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "Range3",
|
||||
"min_type": "static",
|
||||
"max_type": "static",
|
||||
"min_fixed_value": 1,
|
||||
"max_fixed_value": 3,
|
||||
}
|
||||
)
|
||||
range_invalid = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "RangeInvalid",
|
||||
"min_type": "static",
|
||||
"max_type": "static",
|
||||
"min_fixed_value": 3,
|
||||
"max_fixed_value": 1,
|
||||
}
|
||||
)
|
||||
|
||||
threshold1 = self.env['kpi.threshold'].create({
|
||||
'name': 'Threshold1',
|
||||
'range_ids': [(6, 0, [range1.id, range2.id])],
|
||||
})
|
||||
threshold1 = self.env["kpi.threshold"].create(
|
||||
{"name": "Threshold1", "range_ids": [(6, 0, [range1.id, range2.id])],}
|
||||
)
|
||||
|
||||
threshold2 = self.env['kpi.threshold'].create({
|
||||
'name': 'Threshold1',
|
||||
'range_ids': [(6, 0, [range3.id, range2.id])],
|
||||
})
|
||||
threshold2 = self.env["kpi.threshold"].create(
|
||||
{"name": "Threshold1", "range_ids": [(6, 0, [range3.id, range2.id])],}
|
||||
)
|
||||
|
||||
threshold3 = self.env['kpi.threshold'].create({
|
||||
'name': 'Threshold1',
|
||||
'range_ids': [(6, 0, [range_invalid.id, range2.id])],
|
||||
})
|
||||
threshold3 = self.env["kpi.threshold"].create(
|
||||
{
|
||||
"name": "Threshold1",
|
||||
"range_ids": [(6, 0, [range_invalid.id, range2.id])],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(threshold1.valid)
|
||||
self.assertTrue(threshold2.valid)
|
||||
self.assertFalse(threshold3.valid)
|
||||
|
||||
def test_invalid_threshold_range_exception(self):
|
||||
range_error = self.env['kpi.threshold.range'].create({
|
||||
'name': 'RangeError',
|
||||
'min_type': 'python',
|
||||
'min_code': '<Not a valid python expression>',
|
||||
'max_type': 'static',
|
||||
'max_fixed_value': 1,
|
||||
})
|
||||
range_error = self.env["kpi.threshold.range"].create(
|
||||
{
|
||||
"name": "RangeError",
|
||||
"min_type": "python",
|
||||
"min_code": "<Not a valid python expression>",
|
||||
"max_type": "static",
|
||||
"max_fixed_value": 1,
|
||||
}
|
||||
)
|
||||
self.assertFalse(range_error.valid)
|
||||
|
||||
def test_kpi_python(self):
|
||||
kpi_category = self.env['kpi.category'].create({
|
||||
'name': 'Dynamic KPIs'
|
||||
})
|
||||
kpi_threshold = self.env['kpi.threshold'].create({
|
||||
'name': 'KPI Threshold for dynamic KPIs'
|
||||
})
|
||||
kpi_category = self.env["kpi.category"].create({"name": "Dynamic KPIs"})
|
||||
kpi_threshold = self.env["kpi.threshold"].create(
|
||||
{"name": "KPI Threshold for dynamic KPIs"}
|
||||
)
|
||||
kpi_code = """
|
||||
{
|
||||
'value': 1.0,
|
||||
'color': '#00FF00'
|
||||
}
|
||||
"""
|
||||
kpi = self.env['kpi'].create({
|
||||
'name': 'Dynamic python kpi',
|
||||
'description': 'Dynamic python kpi',
|
||||
'category_id': kpi_category.id,
|
||||
'threshold_id': kpi_threshold.id,
|
||||
'periodicity': 1,
|
||||
'periodicity_uom': 'day',
|
||||
'kpi_type': 'python',
|
||||
'kpi_code': kpi_code,
|
||||
})
|
||||
kpi = self.env["kpi"].create(
|
||||
{
|
||||
"name": "Dynamic python kpi",
|
||||
"description": "Dynamic python kpi",
|
||||
"category_id": kpi_category.id,
|
||||
"threshold_id": kpi_threshold.id,
|
||||
"periodicity": 1,
|
||||
"periodicity_uom": "day",
|
||||
"kpi_type": "python",
|
||||
"kpi_code": kpi_code,
|
||||
}
|
||||
)
|
||||
kpi.update_kpi_value()
|
||||
kpi_history = self.env['kpi.history'].search(
|
||||
[('kpi_id', '=', kpi.id)])
|
||||
kpi_history = self.env["kpi.history"].search([("kpi_id", "=", kpi.id)])
|
||||
self.assertEqual(len(kpi_history), 1)
|
||||
self.assertEqual(kpi_history.color, '#00FF00')
|
||||
self.assertEqual(kpi_history.color, "#00FF00")
|
||||
self.assertEqual(kpi_history.value, 1.0)
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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>
|
||||
<!-- Categories -->
|
||||
|
||||
<!-- 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"/>
|
||||
<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>
|
||||
@@ -22,20 +19,19 @@
|
||||
<form string="Category">
|
||||
<sheet>
|
||||
<group col="2" colspan="2">
|
||||
<field name="name" colspan="2"/>
|
||||
<newline/>
|
||||
<field name="description" colspan="2"/>
|
||||
<field name="name" colspan="2" />
|
||||
<newline />
|
||||
<field name="description" colspan="2" />
|
||||
</group>
|
||||
</sheet>
|
||||
</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"/>
|
||||
<field name="view_id" ref="view_kpi_category_tree" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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>
|
||||
<!-- KPI history -->
|
||||
<record id="view_kpi_history_tree" model="ir.ui.view">
|
||||
@@ -9,11 +8,11 @@
|
||||
<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"/>
|
||||
<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>
|
||||
@@ -24,12 +23,12 @@
|
||||
<form string="KPI History">
|
||||
<sheet>
|
||||
<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"/>
|
||||
<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>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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>
|
||||
<!-- Ranges -->
|
||||
<record id="view_kpi_threshold_range_tree" model="ir.ui.view">
|
||||
@@ -9,16 +8,15 @@
|
||||
<field name="model">kpi.threshold.range</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Ranges" decoration-danger="invalid_message">
|
||||
<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"/>
|
||||
<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>
|
||||
@@ -26,46 +24,90 @@
|
||||
<form string="Range">
|
||||
<sheet>
|
||||
<group col="6" colspan="6">
|
||||
<field name="name" colspan="2"/>
|
||||
<field name="color" colspan="2" widget="color"/>
|
||||
<field name="company_id" groups="base.group_multi_company" colspan="2" />
|
||||
<field name="name" colspan="2" />
|
||||
<field name="color" colspan="2" widget="color" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
colspan="2"
|
||||
/>
|
||||
</group>
|
||||
<group col="6">
|
||||
<separator string="Minimum"/>
|
||||
<newline/>
|
||||
<field name="min_type" colspan="3"/>
|
||||
<field name="min_fixed_value" colspan="3" attrs="{'invisible' : [('min_type', '!=', 'static')]}"/>
|
||||
<field name="min_dbsource_id" colspan="3" attrs="{'invisible' : [('min_type', '!=', 'external')]}"/>
|
||||
<field name="min_code" colspan="6" attrs="{'invisible' : [('min_type', 'not in', ('local','external','python'))]}"/>
|
||||
<field name="min_error" colspan="6" attrs="{'invisible': [('min_error', '=', False)]}" />
|
||||
<newline/>
|
||||
<separator string="Maximum"/>
|
||||
<newline/>
|
||||
<field name="max_type" colspan="3"/>
|
||||
<field name="max_fixed_value" colspan="3" attrs="{'invisible' : [('max_type', '!=', 'static')]}"/>
|
||||
<field name="max_dbsource_id" colspan="3" attrs="{'invisible' : [('max_type', '!=', 'external')]}"/>
|
||||
<newline/>
|
||||
<field name="max_code" colspan="6" attrs="{'invisible' : [('max_type', 'not in', ('local','external','python'))]}"/>
|
||||
<newline/>
|
||||
<field name="max_error" colspan="6" attrs="{'invisible': [('max_error', '=', False)]}" />
|
||||
<newline/>
|
||||
<separator string="Minimum" />
|
||||
<newline />
|
||||
<field name="min_type" colspan="3" />
|
||||
<field
|
||||
name="min_fixed_value"
|
||||
colspan="3"
|
||||
attrs="{'invisible' : [('min_type', '!=', 'static')]}"
|
||||
/>
|
||||
<field
|
||||
name="min_dbsource_id"
|
||||
colspan="3"
|
||||
attrs="{'invisible' : [('min_type', '!=', 'external')]}"
|
||||
/>
|
||||
<field
|
||||
name="min_code"
|
||||
colspan="6"
|
||||
attrs="{'invisible' : [('min_type', 'not in', ('local','external','python'))]}"
|
||||
/>
|
||||
<field
|
||||
name="min_error"
|
||||
colspan="6"
|
||||
attrs="{'invisible': [('min_error', '=', False)]}"
|
||||
/>
|
||||
<newline />
|
||||
<separator string="Maximum" />
|
||||
<newline />
|
||||
<field name="max_type" colspan="3" />
|
||||
<field
|
||||
name="max_fixed_value"
|
||||
colspan="3"
|
||||
attrs="{'invisible' : [('max_type', '!=', 'static')]}"
|
||||
/>
|
||||
<field
|
||||
name="max_dbsource_id"
|
||||
colspan="3"
|
||||
attrs="{'invisible' : [('max_type', '!=', 'external')]}"
|
||||
/>
|
||||
<newline />
|
||||
<field
|
||||
name="max_code"
|
||||
colspan="6"
|
||||
attrs="{'invisible' : [('max_type', 'not in', ('local','external','python'))]}"
|
||||
/>
|
||||
<newline />
|
||||
<field
|
||||
name="max_error"
|
||||
colspan="6"
|
||||
attrs="{'invisible': [('max_error', '=', False)]}"
|
||||
/>
|
||||
<newline />
|
||||
</group>
|
||||
<group col="6" colspan="6">
|
||||
<separator string="Thresholds" colspan="4"/>
|
||||
<field name="threshold_ids" nolabel="1" colspan="4"/>
|
||||
<separator string="Errors" attrs="{'invisible' : [('invalid_message', '=', False)]}" colspan="4"/>
|
||||
<field name="invalid_message" nolabel="1" attrs="{'invisible' : [('invalid_message', '=', False)]}" colspan="4"/>
|
||||
<separator string="Thresholds" colspan="4" />
|
||||
<field name="threshold_ids" nolabel="1" colspan="4" />
|
||||
<separator
|
||||
string="Errors"
|
||||
attrs="{'invisible' : [('invalid_message', '=', False)]}"
|
||||
colspan="4"
|
||||
/>
|
||||
<field
|
||||
name="invalid_message"
|
||||
nolabel="1"
|
||||
attrs="{'invisible' : [('invalid_message', '=', False)]}"
|
||||
colspan="4"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</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"/>
|
||||
<field name="view_id" ref="view_kpi_threshold_range_tree" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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>
|
||||
<!-- Thresholds -->
|
||||
<record id="view_kpi_threshold_tree" model="ir.ui.view">
|
||||
@@ -9,13 +8,12 @@
|
||||
<field name="model">kpi.threshold</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Thresholds" decoration-danger="invalid_message">
|
||||
<field name="name"/>
|
||||
<field name="invalid_message"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<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>
|
||||
@@ -23,31 +21,43 @@
|
||||
<form string="Threshold">
|
||||
<sheet>
|
||||
<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/>
|
||||
<separator string="Errors" attrs="{'invisible' : [('invalid_message', '=', False)]}" colspan="4"/>
|
||||
<field name="invalid_message" nolabel="1" attrs="{'invisible' : [('invalid_message', '=', False)]}" colspan="4"/>
|
||||
<newline/>
|
||||
<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 />
|
||||
<separator
|
||||
string="Errors"
|
||||
attrs="{'invisible' : [('invalid_message', '=', False)]}"
|
||||
colspan="4"
|
||||
/>
|
||||
<field
|
||||
name="invalid_message"
|
||||
nolabel="1"
|
||||
attrs="{'invisible' : [('invalid_message', '=', False)]}"
|
||||
colspan="4"
|
||||
/>
|
||||
<newline />
|
||||
</group>
|
||||
</sheet>
|
||||
</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"/>
|
||||
<field name="view_id" ref="view_kpi_threshold_tree" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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>
|
||||
@@ -6,14 +6,14 @@
|
||||
<record id="view_kpi_tree" model="ir.ui.view">
|
||||
<field name="name">kpi.tree</field>
|
||||
<field name="model">kpi</field>
|
||||
<field eval="8" name="priority"/>
|
||||
<field eval="8" name="priority" />
|
||||
<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"/>
|
||||
<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>
|
||||
@@ -22,20 +22,32 @@
|
||||
<field name="model">kpi</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kpi_kanban" create="false" edit="false" delete="false">
|
||||
<field name="id"/>
|
||||
<field name="display_name"/>
|
||||
<field name="color"/>
|
||||
<field name="value"/>
|
||||
<field name="last_execution"/>
|
||||
<field name="id" />
|
||||
<field name="display_name" />
|
||||
<field name="color" />
|
||||
<field name="value" />
|
||||
<field name="last_execution" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kpi_kanban">
|
||||
<div class="oe_kanban_details">
|
||||
<div class="o_kanban_record_title oe_partner_heading"><strong><field name="display_name"/></strong></div>
|
||||
<div class="o_kanban_tags_section oe_kanban_partner_categories" t-attf-style="color:#{record.color.raw_value}">
|
||||
<strong><field name="value"/></strong>
|
||||
</div><div class="o_kanban_tags_section oe_kanban_partner_categories">
|
||||
<field name="last_execution"/>
|
||||
<div class="o_kanban_record_title oe_partner_heading">
|
||||
<strong>
|
||||
<field name="display_name" />
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="o_kanban_tags_section oe_kanban_partner_categories"
|
||||
t-attf-style="color:#{record.color.raw_value}"
|
||||
>
|
||||
<strong>
|
||||
<field name="value" />
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="o_kanban_tags_section oe_kanban_partner_categories"
|
||||
>
|
||||
<field name="last_execution" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,32 +56,32 @@
|
||||
</kanban>
|
||||
</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"/>
|
||||
<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/>
|
||||
<newline />
|
||||
<group expand="0" name="Group By...">
|
||||
<filter name="Category" context="{'group_by':'category_id'}"/>
|
||||
<filter name="Type" context="{'group_by':'kpi_type'}"/>
|
||||
<filter name="Category" context="{'group_by':'category_id'}" />
|
||||
<filter name="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>
|
||||
@@ -79,76 +91,78 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="threshold_id"/>
|
||||
<field name="category_id"/>
|
||||
<field name="threshold_id" />
|
||||
<field name="category_id" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="value" colspan="2"/>
|
||||
<field name="active" colspan="2"/>
|
||||
<field name="company_id"
|
||||
groups="base.group_multi_company"/>
|
||||
<button name="compute_kpi_value"
|
||||
string="Compute KPI Now"
|
||||
colspan="2"
|
||||
type="object"
|
||||
groups="kpi.group_kpi_manager"/>
|
||||
<field name="value" colspan="2" />
|
||||
<field name="active" colspan="2" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<button
|
||||
name="compute_kpi_value"
|
||||
string="Compute KPI Now"
|
||||
colspan="2"
|
||||
type="object"
|
||||
groups="kpi.group_kpi_manager"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook colspan="6">
|
||||
<page string="History">
|
||||
<field name="history_ids" readonly="1" nolabel="1"/>
|
||||
<field name="history_ids" readonly="1" nolabel="1" />
|
||||
</page>
|
||||
<page string="Computation" groups="kpi.group_kpi_manager">
|
||||
<group col="6">
|
||||
<field name="periodicity" colspan="3"/>
|
||||
<field name="periodicity_uom" colspan="3"/>
|
||||
<field name="next_execution_date" colspan="3"/>
|
||||
<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"/>
|
||||
<field name="periodicity" colspan="3" />
|
||||
<field name="periodicity_uom" colspan="3" />
|
||||
<field name="next_execution_date" colspan="3" />
|
||||
<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"/>
|
||||
<field name="description" nolabel="1" />
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="open_kpi_dashboard">
|
||||
<field name="name">KPI Dashboard</field>
|
||||
<field name="res_model">kpi</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="search_view_id" ref="view_kpi_filter"/>
|
||||
<field name="search_view_id" ref="view_kpi_filter" />
|
||||
</record>
|
||||
|
||||
<record id="action_view_kpi_kanban" model="ir.actions.act_window.view">
|
||||
<field eval="1" name="sequence"/>
|
||||
<field eval="1" name="sequence" />
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="view_kpi_kanban"/>
|
||||
<field name="act_window_id" ref="open_kpi_dashboard"/>
|
||||
<field name="view_id" ref="view_kpi_kanban" />
|
||||
<field name="act_window_id" ref="open_kpi_dashboard" />
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="open_kpi_list">
|
||||
<field name="name">KPI Maintenance</field>
|
||||
<field name="res_model">kpi</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_kpi_filter"/>
|
||||
<field name="search_view_id" ref="view_kpi_filter" />
|
||||
</record>
|
||||
|
||||
<record id="action_view_kpi_tree" model="ir.actions.act_window.view">
|
||||
<field eval="2" name="sequence"/>
|
||||
<field eval="2" name="sequence" />
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_kpi_tree"/>
|
||||
<field name="act_window_id" ref="open_kpi_list"/>
|
||||
<field name="view_id" ref="view_kpi_tree" />
|
||||
<field name="act_window_id" ref="open_kpi_list" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,49 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?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>
|
||||
<menuitem id="menu_kpi_dasboard"
|
||||
name="Key Performance Indicators"
|
||||
action="open_kpi_dashboard"
|
||||
sequence="15"
|
||||
parent="base.menu_reporting_dashboard"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<!-- CONFIGURATION -->
|
||||
<menuitem id="menu_configuration_kpi"
|
||||
name="Key Performance Indicators"
|
||||
parent="base.menu_reporting_config"
|
||||
groups="kpi.group_kpi_manager"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_configuration_kpi_kpi"
|
||||
name="Key Performance Indicators"
|
||||
action="open_kpi_list"
|
||||
sequence="10"
|
||||
parent="menu_configuration_kpi"/>
|
||||
|
||||
<menuitem id="menu_configuration_kpi_category"
|
||||
name="Categories"
|
||||
action="open_category_list"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_configuration_kpi_range"
|
||||
name="Ranges"
|
||||
action="open_threshold_range_list"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_configuration_kpi_dbsource"
|
||||
name="Data Sources"
|
||||
action="base_external_dbsource.action_dbsource"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_configuration_kpi_threshold"
|
||||
name="Thresholds"
|
||||
action="open_threshold_list"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="50"/>
|
||||
</odoo>
|
||||
<menuitem
|
||||
id="menu_kpi_dasboard"
|
||||
name="Key Performance Indicators"
|
||||
action="open_kpi_dashboard"
|
||||
sequence="15"
|
||||
parent="base.menu_reporting_dashboard"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
<!-- CONFIGURATION -->
|
||||
<menuitem
|
||||
id="menu_configuration_kpi"
|
||||
name="Key Performance Indicators"
|
||||
parent="base.menu_reporting_config"
|
||||
groups="kpi.group_kpi_manager"
|
||||
sequence="10"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_configuration_kpi_kpi"
|
||||
name="Key Performance Indicators"
|
||||
action="open_kpi_list"
|
||||
sequence="10"
|
||||
parent="menu_configuration_kpi"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_configuration_kpi_category"
|
||||
name="Categories"
|
||||
action="open_category_list"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="20"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_configuration_kpi_range"
|
||||
name="Ranges"
|
||||
action="open_threshold_range_list"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="30"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_configuration_kpi_dbsource"
|
||||
name="Data Sources"
|
||||
action="base_external_dbsource.action_dbsource"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="40"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_configuration_kpi_threshold"
|
||||
name="Thresholds"
|
||||
action="open_threshold_list"
|
||||
parent="menu_configuration_kpi"
|
||||
sequence="50"
|
||||
/>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user