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.

377 lines
16 KiB

13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
  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 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 General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. from dateutil.relativedelta import relativedelta
  22. from datetime import datetime
  23. from osv import fields, osv
  24. from openerp.tools.translate import _
  25. import openerp.tools as tools
  26. import time
  27. import logging
  28. _logger = logging.getLogger(__name__)
  29. def is_one_value(result):
  30. # check if sql query returns only one value
  31. if type(result) is dict and 'value' in result.dictfetchone():
  32. return True
  33. elif type(result) is list and 'value' in result[0]:
  34. return True
  35. else:
  36. return False
  37. def is_select_query(query):
  38. # check if sql query is a SELECT statement
  39. for statement in ('INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP', 'GRANT', 'REVOKE', 'INDEX'):
  40. if statement in query:
  41. return False
  42. return True
  43. class mgmtsystem_kpi_category(osv.osv):
  44. """
  45. KPI Category
  46. """
  47. _name = "mgmtsystem.kpi.category"
  48. _description = "KPI Category"
  49. _columns = {
  50. 'name': fields.char('Name', size=50, required=True),
  51. 'description': fields.text('Description')
  52. }
  53. mgmtsystem_kpi_category()
  54. class mgmtsystem_kpi_threshold_range(osv.osv):
  55. """
  56. KPI Threshold Range
  57. """
  58. _name = "mgmtsystem.kpi.threshold.range"
  59. _description = "KPI Threshold Range"
  60. def compute_min_value(self, cr, uid, ids, field_name, arg, context=None):
  61. if context is None:
  62. context = {}
  63. result = {}
  64. for obj in self.browse(cr, uid, ids):
  65. value = None
  66. if obj.min_type == 'local' and is_select_query(obj.min_code):
  67. cr.execute(obj.min_code)
  68. dic = cr.dictfetchall()
  69. if is_one_value(dic):
  70. value = dic[0]['value']
  71. elif obj.min_type == 'external' and obj.min_dbsource_id.id and is_select_query(obj.min_code):
  72. dbsrc_obj = self.pool.get('base.external.dbsource').browse(cr, uid, obj.min_dbsource_id.id, context)
  73. res = dbsrc_obj.execute(obj.min_code)
  74. if is_one_value(res):
  75. value = res[0]['value']
  76. elif obj.min_type == 'python':
  77. value = eval(obj.min_code)
  78. else:
  79. value = obj.min_fixed_value
  80. result[obj.id] = value
  81. return result
  82. def compute_max_value(self, cr, uid, ids, field_name, arg, context=None):
  83. if context is None:
  84. context = {}
  85. result = {}
  86. for obj in self.browse(cr, uid, ids, context):
  87. value = None
  88. if obj.max_type == 'local' and is_select_query(obj.max_code):
  89. cr.execute(obj.max_code)
  90. dic = cr.dictfetchall()
  91. if is_one_value(dic):
  92. value = dic[0]['value']
  93. elif obj.max_type == 'python':
  94. value = eval(obj.max_code)
  95. elif obj.max_type == 'external' and obj.max_dbsource_id.id and is_select_query(obj.max_code):
  96. dbsrc_obj = self.pool.get('base.external.dbsource').browse(cr, uid, obj.max_dbsource_id.id, context)
  97. res = dbsrc_obj.execute(obj.max_code)
  98. if is_one_value(res):
  99. value = res[0]['value']
  100. else:
  101. value = obj.max_fixed_value
  102. result[obj.id] = value
  103. return result
  104. def _is_valid_range(self, cr, uid, ids, field_name, arg, context=None):
  105. if context is None:
  106. context = {}
  107. result = {}
  108. for obj in self.browse(cr, uid, ids, context):
  109. if obj.max_value < obj.min_value:
  110. result[obj.id] = False
  111. else:
  112. result[obj.id] = True
  113. return result
  114. def _generate_invalid_message(self, cr, uid, ids, field_name, arg, context=None):
  115. if context is None:
  116. context = {}
  117. result = {}
  118. for obj in self.browse(cr, uid, ids, context):
  119. if obj.valid:
  120. result[obj.id] = ""
  121. else:
  122. result[obj.id] = "Minimum value is greater than the maximum value! Please adjust them."
  123. return result
  124. _columns = {
  125. 'name': fields.char('Name', size=50, required=True),
  126. 'valid': fields.function(_is_valid_range, string='Valid', type='boolean', required=True),
  127. 'invalid_message': fields.function(_generate_invalid_message, string='Message', type='char', size=100),
  128. 'min_type': fields.selection((('static','Fixed value'), ('python','Python Code'), ('local', 'SQL - Local DB'), ('external', 'SQL - Externa DB')), 'Min Type', required=True),
  129. 'min_value': fields.function(compute_min_value, string='Minimum', type='float'),
  130. 'min_fixed_value': fields.float('Minimum'),
  131. 'min_code': fields.text('Minimum Computation Code'),
  132. 'min_dbsource_id': fields.many2one('base.external.dbsource','External DB Source'),
  133. 'max_type': fields.selection((('static','Fixed value'), ('python','Python Code'), ('local', 'SQL - Local DB'), ('external', 'SQL - External DB')), 'Max Type', required=True),
  134. 'max_value': fields.function(compute_max_value, string='Maximum', type='float'),
  135. 'max_fixed_value': fields.float('Maximum'),
  136. 'max_code': fields.text('Maximum Computation Code'),
  137. 'max_dbsource_id': fields.many2one('base.external.dbsource','External DB Source'),
  138. 'color': fields.char('Color', help='RGB code with #', size=7, required=True),
  139. 'threshold_ids': fields.many2many('mgmtsystem.kpi.threshold','mgmtsystem_kpi_threshold_range_rel', 'range_id', 'Threshold_id', 'Thresholds'),
  140. }
  141. _defaults = {
  142. 'valid': True,
  143. }
  144. mgmtsystem_kpi_threshold_range()
  145. class mgmtsystem_kpi_threshold(osv.osv):
  146. """
  147. KPI Threshold
  148. """
  149. _name = "mgmtsystem.kpi.threshold"
  150. _description = "KPI Threshold"
  151. def _is_valid_threshold(self, cr, uid, ids, field_name, arg, context=None):
  152. if context is None:
  153. context = {}
  154. result = {}
  155. for obj in self.browse(cr, uid, ids, context):
  156. # check if ranges overlap
  157. for range_obj1 in obj.range_ids:
  158. for range_obj2 in obj.range_ids:
  159. if range_obj1.valid and range_obj2.valid and range_obj1.min_value < range_obj2.min_value:
  160. if range_obj1.max_value <= range_obj2.min_value:
  161. result[obj.id] = True
  162. else:
  163. result[obj.id] = False
  164. return result
  165. def _generate_invalid_message(self, cr, uid, ids, field_name, arg, context=None):
  166. if context is None:
  167. context = {}
  168. result = {}
  169. for obj in self.browse(cr, uid, ids, context):
  170. if obj.valid:
  171. result[obj.id] = ""
  172. else:
  173. result[obj.id] = "2 of your ranges are overlapping! Please make sure your ranges do not overlap."
  174. return result
  175. _columns = {
  176. 'name': fields.char('Name', size=50, required=True),
  177. 'range_ids': fields.many2many('mgmtsystem.kpi.threshold.range','mgmtsystem_kpi_threshold_range_rel', 'threshold_id', 'range_id', 'Ranges'),
  178. 'valid': fields.function(_is_valid_threshold, string='Valid', type='boolean', required=True),
  179. 'invalid_message': fields.function(_generate_invalid_message, string='Message', type='char', size=100),
  180. 'kpi_ids': fields.one2many('mgmtsystem.kpi', 'threshold_id', 'KPIs'),
  181. }
  182. _defaults = {
  183. 'valid': True,
  184. }
  185. def create(self, cr, uid, data, context=None):
  186. if context is None:
  187. context = {}
  188. # check if ranges overlap
  189. range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
  190. range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
  191. for range1 in data['range_ids'][0][2]:
  192. range_obj1 = range_obj1.browse(cr, uid, range1, context)
  193. for range2 in data['range_ids'][0][2]:
  194. range_obj2 = range_obj2.browse(cr, uid, range2, context)
  195. if range_obj1.valid and range_obj2.valid and range_obj1.min_value < range_obj2.min_value:
  196. if range_obj1.max_value > range_obj2.min_value:
  197. raise osv.except_osv(_("2 of your ranges are overlapping!"), _("Please make sure your ranges do not overlap!"))
  198. range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
  199. range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
  200. return super(mgmtsystem_kpi_threshold, self).create(cr, uid, data, context)
  201. def get_color(self, cr, uid, ids, kpi_value, context=None):
  202. if context is None:
  203. context = {}
  204. color = '#FFFFFF'
  205. for obj in self.browse(cr, uid, ids, context):
  206. for range_id in obj.range_ids:
  207. range_obj = self.pool.get('mgmtsystem.kpi.threshold.range').browse(cr, uid, range_id.id, context)
  208. if range_obj.min_value <= kpi_value <= range_obj.max_value and range_obj.valid:
  209. color = range_obj.color
  210. return color
  211. mgmtsystem_kpi_threshold()
  212. class mgmtsystem_kpi_history(osv.osv):
  213. """
  214. History of the KPI
  215. """
  216. _name = "mgmtsystem.kpi.history"
  217. _description = "History of the KPI"
  218. _columns = {
  219. 'name': fields.char('Name', size=150, required=True),
  220. 'kpi_id': fields.many2one('mgmtsystem.kpi', 'KPI', required=True),
  221. 'date': fields.datetime('Execution Date', required=True, readonly=True),
  222. 'value': fields.float('Value', required=True, readonly=True),
  223. 'color': fields.text('Color', required=True, readonly=True),
  224. }
  225. _defaults = {
  226. 'name': lambda *a: time.strftime('%d %B %Y'),
  227. 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
  228. 'color': '#FFFFFF',
  229. }
  230. _order = "date desc"
  231. mgmtsystem_kpi_threshold()
  232. class mgmtsystem_kpi(osv.osv):
  233. """
  234. Key Performance Indicators
  235. """
  236. _name = "mgmtsystem.kpi"
  237. _description = "Key Performance Indicator"
  238. def _display_last_kpi_value(self, cr, uid, ids, field_name, arg, context=None):
  239. if context is None:
  240. context = {}
  241. result = {}
  242. for obj in self.browse(cr, uid, ids):
  243. if obj.history_ids:
  244. result[obj.id] = obj.history_ids[0].value
  245. else:
  246. result[obj.id] = 0
  247. return result
  248. def compute_kpi_value(self, cr, uid, ids, context=None):
  249. if context is None:
  250. context = {}
  251. for obj in self.browse(cr, uid, ids):
  252. kpi_value = 0
  253. if obj.kpi_type == 'local' and is_select_query(obj.kpi_code):
  254. cr.execute(obj.kpi_code)
  255. if is_one_value(cr.dictfetchall()):
  256. kpi_value = cr.dictfetchone()['value']
  257. elif obj.kpi_type == 'external' and obj.dbsource_id.id and is_select_query(obj.kpi_code):
  258. dbsrc_obj = self.pool.get('base.external.dbsource').browse(cr, uid, obj.dbsource_id.id, context)
  259. res = dbsrc_obj.execute(obj.kpi_code)
  260. if is_one_value(res):
  261. kpi_value = res[0]['value']
  262. elif obj.kpi_type == 'python':
  263. kpi_value = eval(obj.kpi_code)
  264. threshold_obj = self.pool.get('mgmtsystem.kpi.threshold').browse(cr, uid, obj.threshold_id.id, context)
  265. values = {
  266. 'kpi_id': obj.id,
  267. 'value': kpi_value,
  268. 'color': threshold_obj.get_color(kpi_value),
  269. }
  270. history_obj = self.pool.get('mgmtsystem.kpi.history')
  271. history_id = history_obj.create(cr, uid, values, context=context)
  272. obj.history_ids.append(history_id)
  273. return True
  274. def update_next_execution_date(self, cr, uid, ids, context=None):
  275. if context is None:
  276. context = {}
  277. for obj in self.browse(cr, uid, ids):
  278. if obj.periodicity_uom == 'hour':
  279. new_date = datetime.now() + relativedelta( hours = +obj.periodicity )
  280. elif obj.periodicity_uom == 'day':
  281. new_date = datetime.now() + relativedelta( days = +obj.periodicity )
  282. elif obj.periodicity_uom == 'week':
  283. new_date = datetime.now() + relativedelta( weeks = +obj.periodicity )
  284. elif obj.periodicity_uom == 'month':
  285. new_date = datetime.now() + relativedelta( months = +obj.periodicity )
  286. values = {
  287. 'next_execution_date': new_date.strftime('%Y-%m-%d %H:%M:%S'),
  288. }
  289. obj.write(values)
  290. return True
  291. # Method called by the scheduler
  292. def update_kpi_value(self, cr, uid, ids=None, context=None):
  293. if context is None:
  294. context = {}
  295. if not ids:
  296. filters = ['&', '|', ('active', '=', True), ('next_execution_date', '<=', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), ('next_execution_date', '=', False)]
  297. if 'filters' in context:
  298. filters.extend(context['filters'])
  299. ids = self.search(cr, uid, filters, context=context)
  300. res = None
  301. try:
  302. res = self.compute_kpi_value(cr, uid, ids, context=context)
  303. self.update_next_execution_date(cr, uid, ids, context=context)
  304. except Exception:
  305. _logger.exception("Failed updating KPI values")
  306. return res
  307. _columns = {
  308. 'name': fields.char('Name', size=50, required=True),
  309. 'description': fields.text('Description'),
  310. 'category_id': fields.many2one('mgmtsystem.kpi.category','Category', required=True),
  311. 'threshold_id': fields.many2one('mgmtsystem.kpi.threshold','Threshold', required=True),
  312. 'periodicity': fields.integer('Periodicity'),
  313. 'periodicity_uom': fields.selection((('hour','Hour'), ('day','Day'), ('week','Week'), ('month','Month')), 'Periodicity UoM', required=True),
  314. 'next_execution_date': fields.datetime('Next execution date', readonly=True),
  315. 'value': fields.function(_display_last_kpi_value, string='Value', type='float'),
  316. 'kpi_type': fields.selection((('python','Python'), ('local','SQL - Local DB'), ('external','SQL - External DB')),'KPI Computation Type'),
  317. 'dbsource_id': fields.many2one('base.external.dbsource','External DB Source'),
  318. 'kpi_code': fields.text('KPI Code', help='SQL code must return the result as \'value\' (i.e. \'SELECT 5 AS value\').'),
  319. 'history_ids': fields.one2many('mgmtsystem.kpi.history', 'kpi_id', 'History'),
  320. 'active': fields.boolean('Active', help="Only active KPIs will be updated by the scheduler based on the periodicity configuration."),
  321. }
  322. _defaults = {
  323. 'active': True,
  324. 'periodicity': 1,
  325. 'periodicity_uom': 'day',
  326. }
  327. mgmtsystem_kpi()
  328. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: