OCA reporting engine fork for dev and update.
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.

208 lines
6.4 KiB

  1. # Copyright 2012 - Now Savoir-faire Linux <https://www.savoirfairelinux.com/>
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  3. from datetime import datetime
  4. from dateutil.relativedelta import relativedelta
  5. from odoo import fields, models, api
  6. from odoo.tools.safe_eval import safe_eval
  7. from odoo.tools import (
  8. DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT,
  9. )
  10. import re
  11. import logging
  12. _logger = logging.getLogger(__name__)
  13. def is_one_value(result):
  14. # check if sql query returns only one value
  15. if type(result) is dict and 'value' in result.dictfetchone():
  16. return True
  17. elif type(result) is list and 'value' in result[0]:
  18. return True
  19. else:
  20. return False
  21. RE_SELECT_QUERY = re.compile('.*(' + '|'.join((
  22. 'INSERT',
  23. 'UPDATE',
  24. 'DELETE',
  25. 'CREATE',
  26. 'ALTER',
  27. 'DROP',
  28. 'GRANT',
  29. 'REVOKE',
  30. 'INDEX',
  31. )) + ')')
  32. def is_sql_or_ddl_statement(query):
  33. """Check if sql query is a SELECT statement"""
  34. return not RE_SELECT_QUERY.match(query.upper())
  35. class KPI(models.Model):
  36. """Key Performance Indicators."""
  37. _name = "kpi"
  38. _description = "Key Performance Indicator"
  39. name = fields.Char('Name', required=True)
  40. description = fields.Text('Description')
  41. category_id = fields.Many2one(
  42. 'kpi.category',
  43. 'Category',
  44. required=True,
  45. )
  46. threshold_id = fields.Many2one(
  47. 'kpi.threshold',
  48. 'Threshold',
  49. required=True,
  50. )
  51. periodicity = fields.Integer('Periodicity', default=1)
  52. periodicity_uom = fields.Selection((
  53. ('minute', 'Minute'),
  54. ('hour', 'Hour'),
  55. ('day', 'Day'),
  56. ('week', 'Week'),
  57. ('month', 'Month')
  58. ), 'Periodicity UoM', required=True, default='day')
  59. next_execution_date = fields.Datetime(
  60. 'Next execution date',
  61. readonly=True,
  62. )
  63. value = fields.Float(string='Value',
  64. compute="_compute_display_last_kpi_value",
  65. )
  66. color = fields.Text('Color', compute="_compute_display_last_kpi_value",)
  67. last_execution = fields.Datetime(
  68. 'Last execution', compute="_compute_display_last_kpi_value",)
  69. kpi_type = fields.Selection((
  70. ('python', 'Python'),
  71. ('local', 'SQL - Local DB'),
  72. ('external', 'SQL - External DB')
  73. ), 'KPI Computation Type')
  74. dbsource_id = fields.Many2one(
  75. 'base.external.dbsource',
  76. 'External DB Source',
  77. )
  78. kpi_code = fields.Text(
  79. 'KPI Code',
  80. help=("SQL code must return the result as 'value' "
  81. "(i.e. 'SELECT 5 AS value')."),
  82. )
  83. history_ids = fields.One2many(
  84. 'kpi.history',
  85. 'kpi_id',
  86. 'History',
  87. )
  88. active = fields.Boolean(
  89. 'Active',
  90. help=("Only active KPIs will be updated by the scheduler based on"
  91. " the periodicity configuration."), default=True
  92. )
  93. company_id = fields.Many2one(
  94. 'res.company', 'Company',
  95. default=lambda self: self.env.user.company_id.id)
  96. @api.multi
  97. def _compute_display_last_kpi_value(self):
  98. history_obj = self.env['kpi.history']
  99. for obj in self:
  100. history_ids = history_obj.search([("kpi_id", "=", obj.id)])
  101. if history_ids:
  102. his = obj.history_ids[0]
  103. obj.value = his.value
  104. obj.color = his.color
  105. obj.last_execution = his.date
  106. else:
  107. obj.value = 0
  108. obj.color = '#FFFFFF'
  109. obj.last_execution = False
  110. @api.multi
  111. def _get_kpi_value(self):
  112. self.ensure_one()
  113. kpi_value = 0
  114. if self.kpi_code:
  115. if self.kpi_type == 'local' and is_sql_or_ddl_statement(
  116. self.kpi_code):
  117. self.env.cr.execute(self.kpi_code)
  118. dic = self.env.cr.dictfetchall()
  119. if is_one_value(dic):
  120. kpi_value = dic[0]['value']
  121. elif (self.kpi_type == 'external' and self.dbsource_id.id and
  122. is_sql_or_ddl_statement(self.kpi_code)):
  123. dbsrc_obj = self.dbsource_id
  124. res = dbsrc_obj.execute(self.kpi_code)
  125. if is_one_value(res):
  126. kpi_value = res[0]['value']
  127. elif self.kpi_type == 'python':
  128. kpi_value = safe_eval(self.kpi_code, {'self': self})
  129. if isinstance(kpi_value, dict):
  130. res = kpi_value
  131. else:
  132. threshold_obj = self.threshold_id
  133. res = {
  134. 'value': kpi_value,
  135. 'color': threshold_obj.get_color(kpi_value),
  136. }
  137. res.update({'kpi_id': self.id})
  138. return res
  139. @api.multi
  140. def compute_kpi_value(self):
  141. for obj in self:
  142. history_vals = obj._get_kpi_value()
  143. history_obj = self.env['kpi.history']
  144. history_obj.sudo().create(history_vals)
  145. return True
  146. @api.multi
  147. def update_next_execution_date(self):
  148. for obj in self:
  149. if obj.periodicity_uom == 'hour':
  150. delta = relativedelta(hours=obj.periodicity)
  151. elif obj.periodicity_uom == 'minute':
  152. delta = relativedelta(minutes=obj.periodicity)
  153. elif obj.periodicity_uom == 'day':
  154. delta = relativedelta(days=obj.periodicity)
  155. elif obj.periodicity_uom == 'week':
  156. delta = relativedelta(weeks=obj.periodicity)
  157. elif obj.periodicity_uom == 'month':
  158. delta = relativedelta(months=obj.periodicity)
  159. else:
  160. delta = relativedelta()
  161. new_date = datetime.now() + delta
  162. obj.next_execution_date = new_date.strftime(DATETIME_FORMAT)
  163. return True
  164. # Method called by the scheduler
  165. @api.model
  166. def update_kpi_value(self):
  167. filters = [
  168. '&',
  169. '|',
  170. ('active', '=', True),
  171. ('next_execution_date', '<=', datetime.now().strftime(
  172. DATETIME_FORMAT)),
  173. ('next_execution_date', '=', False),
  174. ]
  175. if 'filters' in self.env.context:
  176. filters.extend(self.env.context['filters'])
  177. obj_ids = self.search(filters)
  178. res = None
  179. try:
  180. for obj in obj_ids:
  181. obj.compute_kpi_value()
  182. obj.update_next_execution_date()
  183. except Exception:
  184. _logger.exception("Failed updating KPI values")
  185. return res