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.

386 lines
16 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 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 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 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. 'company_id': fields.many2one('res.company', 'Company')
  141. }
  142. _defaults = {
  143. 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
  144. 'valid': True,
  145. }
  146. mgmtsystem_kpi_threshold_range()
  147. class mgmtsystem_kpi_threshold(osv.osv):
  148. """
  149. KPI Threshold
  150. """
  151. _name = "mgmtsystem.kpi.threshold"
  152. _description = "KPI Threshold"
  153. def _is_valid_threshold(self, cr, uid, ids, field_name, arg, context=None):
  154. if context is None:
  155. context = {}
  156. result = {}
  157. for obj in self.browse(cr, uid, ids, context):
  158. # check if ranges overlap
  159. for range_obj1 in obj.range_ids:
  160. for range_obj2 in obj.range_ids:
  161. if range_obj1.valid and range_obj2.valid and range_obj1.min_value < range_obj2.min_value:
  162. if range_obj1.max_value <= range_obj2.min_value:
  163. result[obj.id] = True
  164. else:
  165. result[obj.id] = False
  166. return result
  167. def _generate_invalid_message(self, cr, uid, ids, field_name, arg, context=None):
  168. if context is None:
  169. context = {}
  170. result = {}
  171. for obj in self.browse(cr, uid, ids, context):
  172. if obj.valid:
  173. result[obj.id] = ""
  174. else:
  175. result[obj.id] = "2 of your ranges are overlapping! Please make sure your ranges do not overlap."
  176. return result
  177. _columns = {
  178. 'name': fields.char('Name', size=50, required=True),
  179. 'range_ids': fields.many2many('mgmtsystem.kpi.threshold.range','mgmtsystem_kpi_threshold_range_rel', 'threshold_id', 'range_id', 'Ranges'),
  180. 'valid': fields.function(_is_valid_threshold, string='Valid', type='boolean', required=True),
  181. 'invalid_message': fields.function(_generate_invalid_message, string='Message', type='char', size=100),
  182. 'kpi_ids': fields.one2many('mgmtsystem.kpi', 'threshold_id', 'KPIs'),
  183. 'company_id': fields.many2one('res.company', 'Company')
  184. }
  185. _defaults = {
  186. 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
  187. 'valid': True,
  188. }
  189. def create(self, cr, uid, data, context=None):
  190. if context is None:
  191. context = {}
  192. # check if ranges overlap
  193. range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
  194. range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
  195. for range1 in data['range_ids'][0][2]:
  196. range_obj1 = range_obj1.browse(cr, uid, range1, context)
  197. for range2 in data['range_ids'][0][2]:
  198. range_obj2 = range_obj2.browse(cr, uid, range2, context)
  199. if range_obj1.valid and range_obj2.valid and range_obj1.min_value < range_obj2.min_value:
  200. if range_obj1.max_value > range_obj2.min_value:
  201. raise osv.except_osv(_("2 of your ranges are overlapping!"), _("Please make sure your ranges do not overlap!"))
  202. range_obj2 = self.pool.get('mgmtsystem.kpi.threshold.range')
  203. range_obj1 = self.pool.get('mgmtsystem.kpi.threshold.range')
  204. return super(mgmtsystem_kpi_threshold, self).create(cr, uid, data, context)
  205. def get_color(self, cr, uid, ids, kpi_value, context=None):
  206. if context is None:
  207. context = {}
  208. color = '#FFFFFF'
  209. for obj in self.browse(cr, uid, ids, context):
  210. for range_id in obj.range_ids:
  211. range_obj = self.pool.get('mgmtsystem.kpi.threshold.range').browse(cr, uid, range_id.id, context)
  212. if range_obj.min_value <= kpi_value <= range_obj.max_value and range_obj.valid:
  213. color = range_obj.color
  214. return color
  215. mgmtsystem_kpi_threshold()
  216. class mgmtsystem_kpi_history(osv.osv):
  217. """
  218. History of the KPI
  219. """
  220. _name = "mgmtsystem.kpi.history"
  221. _description = "History of the KPI"
  222. _columns = {
  223. 'name': fields.char('Name', size=150, required=True),
  224. 'kpi_id': fields.many2one('mgmtsystem.kpi', 'KPI', required=True),
  225. 'date': fields.datetime('Execution Date', required=True, readonly=True),
  226. 'value': fields.float('Value', required=True, readonly=True),
  227. 'color': fields.text('Color', required=True, readonly=True),
  228. 'company_id': fields.many2one('res.company', 'Company')
  229. }
  230. _defaults = {
  231. 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
  232. 'name': lambda *a: time.strftime('%d %B %Y'),
  233. 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
  234. 'color': '#FFFFFF',
  235. }
  236. _order = "date desc"
  237. mgmtsystem_kpi_threshold()
  238. class mgmtsystem_kpi(osv.osv):
  239. """
  240. Key Performance Indicators
  241. """
  242. _name = "mgmtsystem.kpi"
  243. _description = "Key Performance Indicator"
  244. def _display_last_kpi_value(self, cr, uid, ids, field_name, arg, context=None):
  245. if context is None:
  246. context = {}
  247. result = {}
  248. for obj in self.browse(cr, uid, ids):
  249. if obj.history_ids:
  250. result[obj.id] = obj.history_ids[0].value
  251. else:
  252. result[obj.id] = 0
  253. return result
  254. def compute_kpi_value(self, cr, uid, ids, context=None):
  255. if context is None:
  256. context = {}
  257. for obj in self.browse(cr, uid, ids):
  258. kpi_value = 0
  259. if obj.kpi_type == 'local' and is_select_query(obj.kpi_code):
  260. cr.execute(obj.kpi_code)
  261. dic = cr.dictfetchall()
  262. if is_one_value(dic):
  263. kpi_value = dic[0]['value']
  264. elif obj.kpi_type == 'external' and obj.dbsource_id.id and is_select_query(obj.kpi_code):
  265. dbsrc_obj = self.pool.get('base.external.dbsource').browse(cr, uid, obj.dbsource_id.id, context)
  266. res = dbsrc_obj.execute(obj.kpi_code)
  267. if is_one_value(res):
  268. kpi_value = res[0]['value']
  269. elif obj.kpi_type == 'python':
  270. kpi_value = eval(obj.kpi_code)
  271. threshold_obj = self.pool.get('mgmtsystem.kpi.threshold').browse(cr, uid, obj.threshold_id.id, context)
  272. values = {
  273. 'kpi_id': obj.id,
  274. 'value': kpi_value,
  275. 'color': threshold_obj.get_color(kpi_value),
  276. }
  277. history_obj = self.pool.get('mgmtsystem.kpi.history')
  278. history_id = history_obj.create(cr, uid, values, context=context)
  279. obj.history_ids.append(history_id)
  280. return True
  281. def update_next_execution_date(self, cr, uid, ids, context=None):
  282. if context is None:
  283. context = {}
  284. for obj in self.browse(cr, uid, ids):
  285. if obj.periodicity_uom == 'hour':
  286. new_date = datetime.now() + relativedelta( hours = +obj.periodicity )
  287. elif obj.periodicity_uom == 'day':
  288. new_date = datetime.now() + relativedelta( days = +obj.periodicity )
  289. elif obj.periodicity_uom == 'week':
  290. new_date = datetime.now() + relativedelta( weeks = +obj.periodicity )
  291. elif obj.periodicity_uom == 'month':
  292. new_date = datetime.now() + relativedelta( months = +obj.periodicity )
  293. values = {
  294. 'next_execution_date': new_date.strftime('%Y-%m-%d %H:%M:%S'),
  295. }
  296. obj.write(values)
  297. return True
  298. # Method called by the scheduler
  299. def update_kpi_value(self, cr, uid, ids=None, context=None):
  300. if context is None:
  301. context = {}
  302. if not ids:
  303. filters = ['&', '|', ('active', '=', True), ('next_execution_date', '<=', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), ('next_execution_date', '=', False)]
  304. if 'filters' in context:
  305. filters.extend(context['filters'])
  306. ids = self.search(cr, uid, filters, context=context)
  307. res = None
  308. try:
  309. res = self.compute_kpi_value(cr, uid, ids, context=context)
  310. self.update_next_execution_date(cr, uid, ids, context=context)
  311. except Exception:
  312. _logger.exception("Failed updating KPI values")
  313. return res
  314. _columns = {
  315. 'name': fields.char('Name', size=50, required=True),
  316. 'description': fields.text('Description'),
  317. 'category_id': fields.many2one('mgmtsystem.kpi.category','Category', required=True),
  318. 'threshold_id': fields.many2one('mgmtsystem.kpi.threshold','Threshold', required=True),
  319. 'periodicity': fields.integer('Periodicity'),
  320. 'periodicity_uom': fields.selection((('hour','Hour'), ('day','Day'), ('week','Week'), ('month','Month')), 'Periodicity UoM', required=True),
  321. 'next_execution_date': fields.datetime('Next execution date', readonly=True),
  322. 'value': fields.function(_display_last_kpi_value, string='Value', type='float'),
  323. 'kpi_type': fields.selection((('python','Python'), ('local','SQL - Local DB'), ('external','SQL - External DB')),'KPI Computation Type'),
  324. 'dbsource_id': fields.many2one('base.external.dbsource','External DB Source'),
  325. 'kpi_code': fields.text('KPI Code', help='SQL code must return the result as \'value\' (i.e. \'SELECT 5 AS value\').'),
  326. 'history_ids': fields.one2many('mgmtsystem.kpi.history', 'kpi_id', 'History'),
  327. 'active': fields.boolean('Active', help="Only active KPIs will be updated by the scheduler based on the periodicity configuration."),
  328. 'company_id': fields.many2one('res.company', 'Company')
  329. }
  330. _defaults = {
  331. 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
  332. 'active': True,
  333. 'periodicity': 1,
  334. 'periodicity_uom': 'day',
  335. }
  336. mgmtsystem_kpi()
  337. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: