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.

253 lines
8.3 KiB

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