You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

532 lines
18 KiB

  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # OpenERP, Open Source Management Solution
  5. # Copyright (C) 2012 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. from datetime import datetime, timedelta
  22. from openerp.osv import fields, orm
  23. from openerp.tools.translate import _
  24. from openerp.tools.safe_eval import safe_eval
  25. from openerp.tools import (
  26. DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT,
  27. )
  28. import time
  29. import re
  30. import logging
  31. _logger = logging.getLogger(__name__)
  32. def is_one_value(result):
  33. # check if sql query returns only one value
  34. if type(result) is dict and 'value' in result.dictfetchone():
  35. return True
  36. elif type(result) is list and 'value' in result[0]:
  37. return True
  38. else:
  39. return False
  40. RE_SELECT_QUERY = re.compile('.*(' + '|'.join((
  41. 'INSERT',
  42. 'UPDATE',
  43. 'DELETE',
  44. 'CREATE',
  45. 'ALTER',
  46. 'DROP',
  47. 'GRANT',
  48. 'REVOKE',
  49. 'INDEX',
  50. )) + ')')
  51. def is_select_query(query):
  52. """Check if sql query is a SELECT statement"""
  53. return not RE_SELECT_QUERY.match(query.upper())
  54. class mgmtsystem_kpi_category(orm.Model):
  55. """
  56. KPI Category
  57. """
  58. _name = "mgmtsystem.kpi.category"
  59. _description = "KPI Category"
  60. _columns = {
  61. 'name': fields.char('Name', size=50, required=True),
  62. 'description': fields.text('Description')
  63. }
  64. class mgmtsystem_kpi_threshold_range(orm.Model):
  65. """
  66. KPI Threshold Range
  67. """
  68. _name = "mgmtsystem.kpi.threshold.range"
  69. _description = "KPI Threshold Range"
  70. def compute_min_value(self, cr, uid, ids, field_name, arg, context=None):
  71. if context is None:
  72. context = {}
  73. result = {}
  74. for obj in self.browse(cr, uid, ids):
  75. value = None
  76. if obj.min_type == 'local' and is_select_query(obj.min_code):
  77. cr.execute(obj.min_code)
  78. dic = cr.dictfetchall()
  79. if is_one_value(dic):
  80. value = dic[0]['value']
  81. elif (obj.min_type == 'external'
  82. and obj.min_dbsource_id.id
  83. and is_select_query(obj.min_code)):
  84. dbsrc_obj = obj.min_dbsource_id
  85. res = dbsrc_obj.execute(obj.min_code)
  86. if is_one_value(res):
  87. value = res[0]['value']
  88. elif obj.min_type == 'python':
  89. value = safe_eval(obj.min_code)
  90. else:
  91. value = obj.min_fixed_value
  92. result[obj.id] = value
  93. return result
  94. def compute_max_value(self, cr, uid, ids, field_name, arg, context=None):
  95. if context is None:
  96. context = {}
  97. result = {}
  98. for obj in self.browse(cr, uid, ids, context):
  99. value = None
  100. if obj.max_type == 'local' and is_select_query(obj.max_code):
  101. cr.execute(obj.max_code)
  102. dic = cr.dictfetchall()
  103. if is_one_value(dic):
  104. value = dic[0]['value']
  105. elif obj.max_type == 'python':
  106. value = safe_eval(obj.max_code)
  107. elif (obj.max_type == 'external'
  108. and obj.max_dbsource_id.id
  109. and is_select_query(obj.max_code)):
  110. dbsrc_obj = obj.max_dbsource_id
  111. res = dbsrc_obj.execute(obj.max_code)
  112. if is_one_value(res):
  113. value = res[0]['value']
  114. else:
  115. value = obj.max_fixed_value
  116. result[obj.id] = value
  117. return result
  118. def _is_valid_range(self, cr, uid, ids, field_name, arg, context=None):
  119. if context is None:
  120. context = {}
  121. result = {}
  122. for obj in self.browse(cr, uid, ids, context):
  123. if obj.max_value < obj.min_value:
  124. result[obj.id] = False
  125. else:
  126. result[obj.id] = True
  127. return result
  128. def _generate_invalid_message(
  129. self, cr, uid, ids, field_name, arg, context=None):
  130. if context is None:
  131. context = {}
  132. result = {}
  133. for obj in self.browse(cr, uid, ids, context):
  134. if obj.valid:
  135. result[obj.id] = ""
  136. else:
  137. result[obj.id] = ("Minimum value is greater than the maximum "
  138. "value! Please adjust them.")
  139. return result
  140. _columns = {
  141. 'name': fields.char('Name', size=50, required=True),
  142. 'valid': fields.function(
  143. _is_valid_range,
  144. string='Valid',
  145. type='boolean',
  146. required=True,
  147. ),
  148. 'invalid_message': fields.function(
  149. _generate_invalid_message,
  150. string='Message',
  151. type='char',
  152. size=100,
  153. ),
  154. 'min_type': fields.selection((
  155. ('static', 'Fixed value'),
  156. ('python', 'Python Code'),
  157. ('local', 'SQL - Local DB'),
  158. ('external', 'SQL - Externa DB'),
  159. ), 'Min Type', required=True),
  160. 'min_value': fields.function(
  161. compute_min_value,
  162. string='Minimum',
  163. type='float',
  164. ),
  165. 'min_fixed_value': fields.float('Minimum'),
  166. 'min_code': fields.text('Minimum Computation Code'),
  167. 'min_dbsource_id': fields.many2one(
  168. 'base.external.dbsource',
  169. 'External DB Source',
  170. ),
  171. 'max_type': fields.selection((
  172. ('static', 'Fixed value'),
  173. ('python', 'Python Code'),
  174. ('local', 'SQL - Local DB'),
  175. ('external', 'SQL - External DB'),
  176. ), 'Max Type', required=True),
  177. 'max_value': fields.function(
  178. compute_max_value,
  179. string='Maximum',
  180. type='float',
  181. ),
  182. 'max_fixed_value': fields.float('Maximum'),
  183. 'max_code': fields.text('Maximum Computation Code'),
  184. 'max_dbsource_id': fields.many2one(
  185. 'base.external.dbsource',
  186. 'External DB Source',
  187. ),
  188. 'color': fields.char(
  189. 'Color',
  190. help='RGB code with #',
  191. size=7,
  192. required=True,
  193. ),
  194. 'threshold_ids': fields.many2many(
  195. 'mgmtsystem.kpi.threshold',
  196. 'mgmtsystem_kpi_threshold_range_rel',
  197. 'range_id',
  198. 'threshold_id',
  199. 'Thresholds',
  200. ),
  201. 'company_id': fields.many2one('res.company', 'Company')
  202. }
  203. _defaults = {
  204. 'company_id': (
  205. lambda self, cr, uid, c:
  206. self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
  207. 'valid': True,
  208. }
  209. class mgmtsystem_kpi_threshold(orm.Model):
  210. """
  211. KPI Threshold
  212. """
  213. _name = "mgmtsystem.kpi.threshold"
  214. _description = "KPI Threshold"
  215. def _is_valid_threshold(self, cr, uid, ids, field_name, arg, context=None):
  216. if context is None:
  217. context = {}
  218. result = {}
  219. for obj in self.browse(cr, uid, ids, context):
  220. # check if ranges overlap
  221. # TODO: This code can be done better
  222. for range1 in obj.range_ids:
  223. for range2 in obj.range_ids:
  224. if (range1.valid and range2.valid
  225. and range1.min_value < range2.min_value):
  226. result[obj.id] = range1.max_value <= range2.min_value
  227. return result
  228. def _generate_invalid_message(
  229. self, cr, uid, ids, field_name, arg, context=None):
  230. if context is None:
  231. context = {}
  232. result = {}
  233. for obj in self.browse(cr, uid, ids, context):
  234. if obj.valid:
  235. result[obj.id] = ""
  236. else:
  237. result[obj.id] = ("2 of your ranges are overlapping! Please "
  238. "make sure your ranges do not overlap.")
  239. return result
  240. _columns = {
  241. 'name': fields.char('Name', size=50, required=True),
  242. 'range_ids': fields.many2many(
  243. 'mgmtsystem.kpi.threshold.range',
  244. 'mgmtsystem_kpi_threshold_range_rel',
  245. 'threshold_id',
  246. 'range_id',
  247. 'Ranges'
  248. ),
  249. 'valid': fields.function(
  250. _is_valid_threshold,
  251. string='Valid',
  252. type='boolean',
  253. required=True,
  254. ),
  255. 'invalid_message': fields.function(
  256. _generate_invalid_message,
  257. string='Message',
  258. type='char',
  259. size=100,
  260. ),
  261. 'kpi_ids': fields.one2many('mgmtsystem.kpi', 'threshold_id', 'KPIs'),
  262. 'company_id': fields.many2one('res.company', 'Company')
  263. }
  264. _defaults = {
  265. 'company_id': (
  266. lambda self, cr, uid, c:
  267. self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
  268. 'valid': True,
  269. }
  270. def create(self, cr, uid, data, context=None):
  271. if context is None:
  272. context = {}
  273. # check if ranges overlap
  274. # TODO: This code can be done better
  275. range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
  276. range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
  277. for range1 in data['range_ids'][0][2]:
  278. range_obj1 = range_obj1.browse(cr, uid, range1, context)
  279. for range2 in data['range_ids'][0][2]:
  280. range_obj2 = range_obj2.browse(cr, uid, range2, context)
  281. if (range_obj1.valid and range_obj2.valid
  282. and range_obj1.min_value < range_obj2.min_value):
  283. if range_obj1.max_value > range_obj2.min_value:
  284. raise orm.except_orm(
  285. _("2 of your ranges are overlapping!"),
  286. _("Please make sure your ranges do not overlap!")
  287. )
  288. range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
  289. range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
  290. return super(mgmtsystem_kpi_threshold, self).create(
  291. cr, uid, data, context
  292. )
  293. def get_color(self, cr, uid, ids, kpi_value, context=None):
  294. if context is None:
  295. context = {}
  296. color = '#FFFFFF'
  297. for obj in self.browse(cr, uid, ids, context):
  298. for range_obj in obj.range_ids:
  299. if (range_obj.min_value <= kpi_value <= range_obj.max_value
  300. and range_obj.valid):
  301. color = range_obj.color
  302. return color
  303. class mgmtsystem_kpi_history(orm.Model):
  304. """
  305. History of the KPI
  306. """
  307. _name = "mgmtsystem.kpi.history"
  308. _description = "History of the KPI"
  309. _columns = {
  310. 'name': fields.char('Name', size=150, required=True),
  311. 'kpi_id': fields.many2one('mgmtsystem.kpi', 'KPI', required=True),
  312. 'date': fields.datetime(
  313. 'Execution Date',
  314. required=True,
  315. readonly=True,
  316. ),
  317. 'value': fields.float('Value', required=True, readonly=True),
  318. 'color': fields.text('Color', required=True, readonly=True),
  319. 'company_id': fields.many2one('res.company', 'Company')
  320. }
  321. _defaults = {
  322. 'company_id': (
  323. lambda self, cr, uid, c:
  324. self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
  325. 'name': lambda *a: time.strftime('%d %B %Y'),
  326. 'date': lambda *a: time.strftime(DATETIME_FORMAT),
  327. 'color': '#FFFFFF',
  328. }
  329. _order = "date desc"
  330. class mgmtsystem_kpi(orm.Model):
  331. """
  332. Key Performance Indicators
  333. """
  334. _name = "mgmtsystem.kpi"
  335. _description = "Key Performance Indicator"
  336. def _display_last_kpi_value(
  337. self, cr, uid, ids, field_name, arg, context=None):
  338. if context is None:
  339. context = {}
  340. result = {}
  341. for obj in self.browse(cr, uid, ids):
  342. if obj.history_ids:
  343. result[obj.id] = obj.history_ids[0].value
  344. else:
  345. result[obj.id] = 0
  346. return result
  347. def compute_kpi_value(self, cr, uid, ids, context=None):
  348. if context is None:
  349. context = {}
  350. for obj in self.browse(cr, uid, ids):
  351. kpi_value = 0
  352. if obj.kpi_type == 'local' and is_select_query(obj.kpi_code):
  353. cr.execute(obj.kpi_code)
  354. dic = cr.dictfetchall()
  355. if is_one_value(dic):
  356. kpi_value = dic[0]['value']
  357. elif (obj.kpi_type == 'external'
  358. and obj.dbsource_id.id
  359. and is_select_query(obj.kpi_code)):
  360. dbsrc_obj = obj.dbsource_id
  361. res = dbsrc_obj.execute(obj.kpi_code)
  362. if is_one_value(res):
  363. kpi_value = res[0]['value']
  364. elif obj.kpi_type == 'python':
  365. kpi_value = safe_eval(obj.kpi_code)
  366. threshold_obj = obj.threshold_id
  367. values = {
  368. 'kpi_id': obj.id,
  369. 'value': kpi_value,
  370. 'color': threshold_obj.get_color(kpi_value),
  371. }
  372. history_obj = self.pool.get('mgmtsystem.kpi.history')
  373. history_id = history_obj.create(cr, uid, values, context=context)
  374. obj.history_ids.append(history_id)
  375. return True
  376. def update_next_execution_date(self, cr, uid, ids, context=None):
  377. if context is None:
  378. context = {}
  379. for obj in self.browse(cr, uid, ids):
  380. if obj.periodicity_uom == 'hour':
  381. delta = timedelta(hours=obj.periodicity)
  382. elif obj.periodicity_uom == 'day':
  383. delta = timedelta(days=obj.periodicity)
  384. elif obj.periodicity_uom == 'week':
  385. delta = timedelta(weeks=obj.periodicity)
  386. elif obj.periodicity_uom == 'month':
  387. delta = timedelta(months=obj.periodicity)
  388. else:
  389. delta = timedelta()
  390. new_date = datetime.now() + delta
  391. values = {
  392. 'next_execution_date': new_date.strftime(DATETIME_FORMAT),
  393. }
  394. obj.write(values)
  395. return True
  396. # Method called by the scheduler
  397. def update_kpi_value(self, cr, uid, ids=None, context=None):
  398. if context is None:
  399. context = {}
  400. if not ids:
  401. filters = [
  402. '&',
  403. '|',
  404. ('active', '=', True),
  405. ('next_execution_date', '<=',
  406. datetime.now().strftime(DATETIME_FORMAT)),
  407. ('next_execution_date', '=', False),
  408. ]
  409. if 'filters' in context:
  410. filters.extend(context['filters'])
  411. ids = self.search(cr, uid, filters, context=context)
  412. res = None
  413. try:
  414. res = self.compute_kpi_value(cr, uid, ids, context=context)
  415. self.update_next_execution_date(cr, uid, ids, context=context)
  416. except Exception:
  417. _logger.exception("Failed updating KPI values")
  418. return res
  419. _columns = {
  420. 'name': fields.char('Name', size=50, required=True),
  421. 'description': fields.text('Description'),
  422. 'category_id': fields.many2one(
  423. 'mgmtsystem.kpi.category',
  424. 'Category',
  425. required=True,
  426. ),
  427. 'threshold_id': fields.many2one(
  428. 'mgmtsystem.kpi.threshold',
  429. 'Threshold',
  430. required=True,
  431. ),
  432. 'periodicity': fields.integer('Periodicity'),
  433. 'periodicity_uom': fields.selection((
  434. ('hour', 'Hour'),
  435. ('day', 'Day'),
  436. ('week', 'Week'),
  437. ('month', 'Month')
  438. ), 'Periodicity UoM', required=True),
  439. 'next_execution_date': fields.datetime(
  440. 'Next execution date',
  441. readonly=True,
  442. ),
  443. 'value': fields.function(
  444. _display_last_kpi_value,
  445. string='Value',
  446. type='float',
  447. ),
  448. 'kpi_type': fields.selection((
  449. ('python', 'Python'),
  450. ('local', 'SQL - Local DB'),
  451. ('external', 'SQL - External DB')
  452. ), 'KPI Computation Type'),
  453. 'dbsource_id': fields.many2one(
  454. 'base.external.dbsource',
  455. 'External DB Source',
  456. ),
  457. 'kpi_code': fields.text(
  458. 'KPI Code',
  459. help=("SQL code must return the result as 'value' "
  460. "(i.e. 'SELECT 5 AS value')."),
  461. ),
  462. 'history_ids': fields.one2many(
  463. 'mgmtsystem.kpi.history',
  464. 'kpi_id',
  465. 'History',
  466. ),
  467. 'active': fields.boolean(
  468. 'Active',
  469. help=("Only active KPIs will be updated by the scheduler based on"
  470. " the periodicity configuration."),
  471. ),
  472. 'company_id': fields.many2one('res.company', 'Company')
  473. }
  474. _defaults = {
  475. 'company_id': (
  476. lambda self, cr, uid, c:
  477. self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id),
  478. 'active': True,
  479. 'periodicity': 1,
  480. 'periodicity_uom': 'day',
  481. }