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.

238 lines
8.1 KiB

  1. # Copyright 2020 Creu Blanca
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  3. from odoo import api, fields, models, _
  4. from odoo.exceptions import ValidationError
  5. import ast
  6. from odoo.tools.safe_eval import safe_eval
  7. from odoo.addons.base.models.ir_cron import _intervalTypes
  8. from odoo.tools.float_utils import float_compare
  9. import re
  10. import json
  11. import datetime
  12. from dateutil import relativedelta
  13. class KpiKpi(models.Model):
  14. _name = "kpi.kpi"
  15. _description = "Kpi Kpi"
  16. name = fields.Char(required=True)
  17. active = fields.Boolean(default=True)
  18. cron_id = fields.Many2one("ir.cron", readonly=True, copy=False)
  19. computation_method = fields.Selection(
  20. [("function", "Function"), ("code", "Code")], required=True
  21. )
  22. value = fields.Serialized()
  23. dashboard_item_ids = fields.One2many("kpi.dashboard.item", inverse_name="kpi_id")
  24. model_id = fields.Many2one("ir.model",)
  25. function = fields.Char()
  26. args = fields.Char()
  27. kwargs = fields.Char()
  28. widget = fields.Selection(
  29. [('integer', 'Integer'), ("number", "Number"), ("meter", "Meter"),
  30. ('counter', 'Counter'), ("graph", "Graph")],
  31. required=True,
  32. default="number",
  33. )
  34. value_last_update = fields.Datetime(readonly=True)
  35. prefix = fields.Char()
  36. suffix = fields.Char()
  37. action_ids = fields.One2many(
  38. "kpi.kpi.action",
  39. inverse_name='kpi_id',
  40. help="Actions that can be opened from the KPI"
  41. )
  42. code = fields.Text("Code")
  43. store_history = fields.Boolean()
  44. store_history_interval = fields.Selection(
  45. selection=lambda self:
  46. self.env['ir.cron']._fields['interval_type'].selection,
  47. )
  48. store_history_interval_number = fields.Integer()
  49. compute_on_fly = fields.Boolean()
  50. history_ids = fields.One2many("kpi.kpi.history", inverse_name="kpi_id")
  51. computed_value = fields.Serialized(compute='_compute_computed_value')
  52. computed_date = fields.Datetime(compute='_compute_computed_value')
  53. @api.depends('value', 'value_last_update', 'compute_on_fly')
  54. def _compute_computed_value(self):
  55. for record in self:
  56. if record.compute_on_fly:
  57. record.computed_value = record._compute_value()
  58. record.computed_date = fields.Datetime.now()
  59. else:
  60. record.computed_value = record.value
  61. record.computed_date = record.value_last_update
  62. def _cron_vals(self):
  63. return {
  64. "name": self.name,
  65. "model_id": self.env.ref("kpi_dashboard.model_kpi_kpi").id,
  66. "interval_number": 1,
  67. "interval_type": "hours",
  68. "state": "code",
  69. "code": "model.browse(%s).compute()" % self.id,
  70. "active": True,
  71. }
  72. def compute(self):
  73. for record in self:
  74. record._compute()
  75. return True
  76. def _generate_history_vals(self, value):
  77. return {
  78. "kpi_id": self.id,
  79. "value": value,
  80. "widget": self.widget,
  81. }
  82. def _compute_value(self):
  83. return getattr(self, "_compute_value_%s" % self.computation_method)()
  84. def _compute(self):
  85. value = self._compute_value()
  86. self.write({"value": value})
  87. if self.store_history:
  88. last = self.env['kpi.kpi.history'].search([
  89. ('kpi_id', '=', self.id)
  90. ], limit=1)
  91. if (
  92. not last or
  93. not self.store_history_interval or
  94. last.create_date + _intervalTypes[self.store_history_interval](
  95. self.store_history_interval_number) < fields.Datetime.now()
  96. ):
  97. self.env["kpi.kpi.history"].create(
  98. self._generate_history_vals(value)
  99. )
  100. notifications = []
  101. for dashboard_item in self.dashboard_item_ids:
  102. channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id
  103. notifications.append([channel, dashboard_item._read_dashboard()])
  104. if notifications:
  105. self.env["bus.bus"].sendmany(notifications)
  106. def _compute_value_function(self):
  107. obj = self
  108. if self.model_id:
  109. obj = self.env[self.model_id.model]
  110. args = ast.literal_eval(self.args or "[]")
  111. kwargs = ast.literal_eval(self.kwargs or "{}")
  112. return getattr(obj, self.function)(*args, **kwargs)
  113. def generate_cron(self):
  114. self.ensure_one()
  115. self.cron_id = self.env["ir.cron"].create(self._cron_vals())
  116. @api.multi
  117. def write(self, vals):
  118. if "value" in vals:
  119. vals["value_last_update"] = fields.Datetime.now()
  120. return super().write(vals)
  121. def _get_code_input_dict(self):
  122. return {
  123. "self": self,
  124. "model": self.browse(),
  125. "datetime": datetime,
  126. "float_compare": float_compare,
  127. "relativedelta": relativedelta.relativedelta,
  128. }
  129. def _forbidden_code(self):
  130. return ["commit", "rollback", "getattr", "execute"]
  131. def _compute_value_code(self):
  132. forbidden = self._forbidden_code()
  133. search_terms = "(" + ("|".join(forbidden)) + ")"
  134. if re.search(search_terms, (self.code or "").lower()):
  135. message = ", ".join(forbidden[:-1]) or ""
  136. if len(message) > 0:
  137. message += _(" or ")
  138. message += forbidden[-1]
  139. raise ValidationError(_(
  140. "The code cannot contain the following terms: %s."
  141. ) % message)
  142. results = self._get_code_input_dict()
  143. savepoint = "kpi_formula_%s" % self.id
  144. self.env.cr.execute("savepoint %s" % savepoint)
  145. safe_eval(self.code or "", results, mode="exec", nocopy=True)
  146. self.env.cr.execute("rollback to %s" % savepoint)
  147. return results.get("result", {})
  148. def show_value(self):
  149. self.ensure_one()
  150. action = self.env.ref('kpi_dashboard.kpi_kpi_act_window')
  151. result = action.read()[0]
  152. result.update({
  153. 'res_id': self.id,
  154. 'target': 'new',
  155. 'view_mode': 'form',
  156. 'views': [(self.env.ref(
  157. 'kpi_dashboard.kpi_kpi_widget_form_view'
  158. ).id, 'form')],
  159. })
  160. return result
  161. class KpiKpiAction(models.Model):
  162. _name = 'kpi.kpi.action'
  163. _description = 'KPI action'
  164. kpi_id = fields.Many2one('kpi.kpi', required=True, ondelete='cascade')
  165. action = fields.Reference(
  166. selection=[('ir.actions.report', 'ir.actions.report'),
  167. ('ir.actions.act_window', 'ir.actions.act_window'),
  168. ('ir.actions.act_url', 'ir.actions.act_url'),
  169. ('ir.actions.server', 'ir.actions.server'),
  170. ('ir.actions.client', 'ir.actions.client')],
  171. required=True,
  172. )
  173. context = fields.Char()
  174. def read_dashboard(self):
  175. result = {}
  176. for r in self:
  177. result[r.id] = {
  178. 'id': r.action.id,
  179. 'type': r.action._name,
  180. 'name': r.action.name,
  181. 'context': safe_eval(r.context or '{}')
  182. }
  183. return result
  184. class KpiKpiHistory(models.Model):
  185. _name = 'kpi.kpi.history'
  186. _description = 'KPI history'
  187. _order = 'create_date DESC'
  188. kpi_id = fields.Many2one(
  189. 'kpi.kpi', required=True, ondelete='cascade', readonly=True
  190. )
  191. value = fields.Serialized(readonly=True)
  192. raw_value = fields.Char(compute='_compute_raw_value')
  193. name = fields.Char(related='kpi_id.name')
  194. widget = fields.Selection(
  195. selection=lambda self:
  196. self.env['kpi.kpi']._fields['widget'].selection,
  197. required=True)
  198. @api.depends('value')
  199. def _compute_raw_value(self):
  200. for record in self:
  201. record.raw_value = json.dumps(record.value)
  202. def show_form(self):
  203. self.ensure_one()
  204. action = self.env.ref('kpi_dashboard.kpi_kpi_history_act_window')
  205. result = action.read()[0]
  206. result.update({
  207. 'res_id': self.id,
  208. 'target': 'new',
  209. 'view_mode': 'form',
  210. 'views': [(self.env.context.get('form_id'), 'form')],
  211. })
  212. return result