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.

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