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.

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