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.

260 lines
8.4 KiB

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