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.

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