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.

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