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.

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