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.

514 lines
17 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. # -*- coding: utf-8 -*-
  2. # Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
  3. # @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  5. import logging
  6. from psycopg2 import ProgrammingError
  7. from openerp import _, api, fields, models, SUPERUSER_ID
  8. from openerp.exceptions import Warning as UserError
  9. _logger = logging.getLogger(__name__)
  10. class BiSQLView(models.Model):
  11. _name = 'bi.sql.view'
  12. _inherit = ['sql.request.mixin']
  13. _sql_prefix = 'x_bi_sql_view_'
  14. _model_prefix = 'x_bi_sql_view.'
  15. _sql_request_groups_relation = 'bi_sql_view_groups_rel'
  16. _sql_request_users_relation = 'bi_sql_view_users_rel'
  17. _STATE_SQL_EDITOR = [
  18. ('model_valid', 'SQL View and Model Created'),
  19. ('ui_valid', 'Graph, action and Menu Created'),
  20. ]
  21. technical_name = fields.Char(
  22. string='Technical Name', required=True,
  23. help="Suffix of the SQL view. (SQL full name will be computed and"
  24. " prefixed by 'x_bi_sql_view_'. Should have correct"
  25. "syntax. For more information, see https://www.postgresql.org/"
  26. "docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS")
  27. view_name = fields.Char(
  28. string='View Name', compute='_compute_view_name', readonly=True,
  29. store=True, help="Full name of the SQL view")
  30. model_name = fields.Char(
  31. string='Model Name', compute='_compute_model_name', readonly=True,
  32. store=True, help="Full Qualified Name of the transient model that will"
  33. " be created.")
  34. is_materialized = fields.Boolean(
  35. string='Is Materialized View', default=True, readonly=True,
  36. states={'draft': [('readonly', False)]})
  37. materialized_text = fields.Char(
  38. compute='_compute_materialized_text', store=True)
  39. size = fields.Char(
  40. string='Database Size', readonly=True,
  41. help="Size of the materialized view and its indexes")
  42. state = fields.Selection(selection_add=_STATE_SQL_EDITOR)
  43. query = fields.Text(
  44. help="SQL Request that will be inserted as the view. Take care to :\n"
  45. " * set a name for all your selected fields, specially if you use"
  46. " SQL function (like EXTRACT, ...);\n"
  47. " * Do not use 'SELECT *' or 'SELECT table.*';\n"
  48. " * prefix the name of the selectable columns by 'x_';",
  49. default="SELECT\n"
  50. " my_field as x_my_field\n"
  51. "FROM my_table")
  52. domain_force = fields.Text(
  53. string='Extra Rule Definition', default="[]", help="Define here"
  54. " access restriction to data.\n"
  55. " Take care to use field name prefixed by 'x_'."
  56. " A global 'ir.rule' will be created."
  57. " A typical Multi Company rule is for exemple \n"
  58. " ['|', ('x_company_id','child_of', [user.company_id.id]),"
  59. "('x_company_id','=',False)].")
  60. has_group_changed = fields.Boolean(copy=False)
  61. bi_sql_view_field_ids = fields.One2many(
  62. string='SQL Fields', comodel_name='bi.sql.view.field',
  63. inverse_name='bi_sql_view_id')
  64. model_id = fields.Many2one(
  65. string='Odoo Model', comodel_name='ir.model', readonly=True)
  66. graph_view_id = fields.Many2one(
  67. string='Odoo Graph View', comodel_name='ir.ui.view', readonly=True)
  68. pivot_view_id = fields.Many2one(
  69. string='Odoo Pivot View', comodel_name='ir.ui.view', readonly=True)
  70. search_view_id = fields.Many2one(
  71. string='Odoo Search View', comodel_name='ir.ui.view', readonly=True)
  72. action_id = fields.Many2one(
  73. string='Odoo Action', comodel_name='ir.actions.act_window',
  74. readonly=True)
  75. menu_id = fields.Many2one(
  76. string='Odoo Menu', comodel_name='ir.ui.menu', readonly=True)
  77. cron_id = fields.Many2one(
  78. string='Odoo Cron', comodel_name='ir.cron', readonly=True,
  79. help="Cron Task that will refresh the materialized view")
  80. rule_id = fields.Many2one(
  81. string='Odoo Rule', comodel_name='ir.rule', readonly=True)
  82. # Compute Section
  83. @api.depends('is_materialized')
  84. @api.multi
  85. def _compute_materialized_text(self):
  86. for sql_view in self:
  87. sql_view.materialized_text =\
  88. sql_view.is_materialized and 'MATERIALIZED' or ''
  89. @api.depends('technical_name')
  90. @api.multi
  91. def _compute_view_name(self):
  92. for sql_view in self:
  93. sql_view.view_name = '%s%s' % (
  94. sql_view._sql_prefix, sql_view.technical_name)
  95. @api.depends('technical_name')
  96. @api.multi
  97. def _compute_model_name(self):
  98. for sql_view in self:
  99. sql_view.model_name = '%s%s' % (
  100. sql_view._model_prefix, sql_view.technical_name)
  101. @api.onchange('group_ids')
  102. def onchange_group_ids(self):
  103. if self.state not in ('draft', 'sql_valid'):
  104. self.has_group_changed = True
  105. # Overload Section
  106. @api.multi
  107. def unlink(self):
  108. non_draft_views = self.search([
  109. ('id', 'in', self.ids),
  110. ('state', 'not in', ('draft', 'sql_valid'))])
  111. if non_draft_views:
  112. raise UserError(_("You can only unlink draft views"))
  113. return self.unlink()
  114. @api.multi
  115. def copy(self, default=None):
  116. self.ensure_one()
  117. default = dict(default or {})
  118. default.update({
  119. 'name': _('%s (Copy)') % (self.name),
  120. 'technical_name': '%s_copy' % (self.technical_name),
  121. })
  122. return super(BiSQLView, self).copy(default=default)
  123. # Action Section
  124. @api.multi
  125. def button_create_sql_view_and_model(self):
  126. for sql_view in self:
  127. if sql_view.state != 'sql_valid':
  128. raise UserError(_(
  129. "You can only process this action on SQL Valid items"))
  130. # Create ORM and acess
  131. sql_view._create_model_and_fields()
  132. sql_view._create_model_access()
  133. # Create SQL View and indexes
  134. sql_view._create_view()
  135. sql_view._create_index()
  136. if sql_view.is_materialized:
  137. sql_view.cron_id = self.env['ir.cron'].create(
  138. sql_view._prepare_cron()).id
  139. sql_view.state = 'model_valid'
  140. @api.multi
  141. def button_set_draft(self):
  142. for sql_view in self:
  143. if sql_view.state in ('model_valid', 'ui_valid'):
  144. # Drop SQL View (and indexes by cascade)
  145. sql_view._drop_view()
  146. # Drop ORM
  147. sql_view._drop_model_and_fields()
  148. sql_view.graph_view_id.unlink()
  149. sql_view.pivot_view_id.unlink()
  150. sql_view.action_id.unlink()
  151. sql_view.menu_id.unlink()
  152. sql_view.rule_id.unlink()
  153. if sql_view.cron_id:
  154. sql_view.cron_id.unlink()
  155. sql_view.write({'state': 'draft', 'has_group_changed': False})
  156. @api.multi
  157. def button_create_ui(self):
  158. self.graph_view_id = self.env['ir.ui.view'].create(
  159. self._prepare_graph_view()).id
  160. self.pivot_view_id = self.env['ir.ui.view'].create(
  161. self._prepare_pivot_view()).id
  162. self.search_view_id = self.env['ir.ui.view'].create(
  163. self._prepare_search_view()).id
  164. self.action_id = self.env['ir.actions.act_window'].create(
  165. self._prepare_action()).id
  166. self.menu_id = self.env['ir.ui.menu'].create(
  167. self._prepare_menu()).id
  168. self.write({'state': 'ui_valid'})
  169. @api.multi
  170. def button_update_model_access(self):
  171. self._drop_model_access()
  172. self._create_model_access()
  173. self.write({'has_group_changed': False})
  174. @api.multi
  175. def button_refresh_materialized_view(self):
  176. self._refresh_materialized_view()
  177. @api.multi
  178. def button_open_view(self):
  179. return {
  180. 'type': 'ir.actions.act_window',
  181. 'res_model': self.model_id.model,
  182. 'search_view_id': self.search_view_id.id,
  183. 'view_type': 'form',
  184. 'view_mode': 'graph,pivot',
  185. }
  186. # Prepare Function
  187. @api.multi
  188. def _prepare_model(self):
  189. self.ensure_one()
  190. field_id = []
  191. for field in self.bi_sql_view_field_ids.filtered(
  192. lambda x: x.field_description is not False):
  193. field_id.append([0, False, field._prepare_model_field()])
  194. return {
  195. 'name': self.name,
  196. 'model': self.model_name,
  197. 'access_ids': [],
  198. 'field_id': field_id,
  199. }
  200. @api.multi
  201. def _prepare_model_access(self):
  202. self.ensure_one()
  203. res = []
  204. for group in self.group_ids:
  205. res.append({
  206. 'name': _('%s Access %s') % (
  207. self.model_name, group.full_name),
  208. 'model_id': self.model_id.id,
  209. 'group_id': group.id,
  210. 'perm_read': True,
  211. 'perm_create': False,
  212. 'perm_write': False,
  213. 'perm_unlink': False,
  214. })
  215. return res
  216. @api.multi
  217. def _prepare_cron(self):
  218. self.ensure_one()
  219. return {
  220. 'name': _('Refresh Materialized View %s') % (self.view_name),
  221. 'user_id': SUPERUSER_ID,
  222. 'model': 'bi.sql.view',
  223. 'function': 'button_refresh_materialized_view',
  224. 'args': repr(([self.id],))
  225. }
  226. @api.multi
  227. def _prepare_rule(self):
  228. self.ensure_one()
  229. return {
  230. 'name': _('Access %s') % (self.name),
  231. 'model_id': self.model_id.id,
  232. 'domain_force': self.domain_force,
  233. 'global': True,
  234. }
  235. @api.multi
  236. def _prepare_graph_view(self):
  237. self.ensure_one()
  238. return {
  239. 'name': self.name,
  240. 'type': 'graph',
  241. 'model': self.model_id.model,
  242. 'arch':
  243. """<?xml version="1.0"?>"""
  244. """<graph string="Analysis" type="pivot" stacked="True">{}"""
  245. """</graph>""".format("".join(
  246. [x._prepare_graph_field()
  247. for x in self.bi_sql_view_field_ids]))
  248. }
  249. @api.multi
  250. def _prepare_pivot_view(self):
  251. self.ensure_one()
  252. return {
  253. 'name': self.name,
  254. 'type': 'pivot',
  255. 'model': self.model_id.model,
  256. 'arch':
  257. """<?xml version="1.0"?>"""
  258. """<pivot string="Analysis" stacked="True">{}"""
  259. """</pivot>""".format("".join(
  260. [x._prepare_pivot_field()
  261. for x in self.bi_sql_view_field_ids]))
  262. }
  263. @api.multi
  264. def _prepare_search_view(self):
  265. self.ensure_one()
  266. return {
  267. 'name': self.name,
  268. 'type': 'search',
  269. 'model': self.model_id.model,
  270. 'arch':
  271. """<?xml version="1.0"?>"""
  272. """<search string="Analysis">{}"""
  273. """<group expand="1" string="Group By">{}</group>"""
  274. """</search>""".format(
  275. "".join(
  276. [x._prepare_search_field()
  277. for x in self.bi_sql_view_field_ids]),
  278. "".join(
  279. [x._prepare_search_filter_field()
  280. for x in self.bi_sql_view_field_ids]))
  281. }
  282. @api.multi
  283. def _prepare_action(self):
  284. self.ensure_one()
  285. return {
  286. 'name': self.name,
  287. 'res_model': self.model_id.model,
  288. 'type': 'ir.actions.act_window',
  289. 'view_type': 'form',
  290. 'view_mode': 'graph,pivot',
  291. 'search_view_id': self.search_view_id.id,
  292. }
  293. @api.multi
  294. def _prepare_menu(self):
  295. self.ensure_one()
  296. return {
  297. 'name': self.name,
  298. 'parent_id': self.env.ref('bi_sql_editor.menu_bi_sql_editor').id,
  299. 'action': 'ir.actions.act_window,%s' % (self.action_id.id),
  300. }
  301. # Custom Section
  302. def _log_execute(self, req):
  303. _logger.info("Executing SQL Request %s ..." % (req))
  304. self.env.cr.execute(req)
  305. @api.multi
  306. def _drop_view(self):
  307. for sql_view in self:
  308. self._log_execute(
  309. "DROP %s VIEW IF EXISTS %s" % (
  310. sql_view.materialized_text, sql_view.view_name))
  311. sql_view.size = False
  312. @api.multi
  313. def _create_view(self):
  314. for sql_view in self:
  315. sql_view._drop_view()
  316. try:
  317. self._log_execute(sql_view._prepare_request_for_execution())
  318. sql_view._refresh_size()
  319. except ProgrammingError as e:
  320. raise UserError(_(
  321. "SQL Error while creating %s VIEW %s :\n %s") % (
  322. sql_view.materialized_text, sql_view.view_name,
  323. e.message))
  324. @api.multi
  325. def _create_index(self):
  326. for sql_view in self:
  327. for sql_field in sql_view.bi_sql_view_field_ids.filtered(
  328. lambda x: x.is_index is True):
  329. self._log_execute(
  330. "CREATE INDEX %s ON %s (%s);" % (
  331. sql_field.index_name, sql_view.view_name,
  332. sql_field.name))
  333. @api.multi
  334. def _create_model_and_fields(self):
  335. for sql_view in self:
  336. # Create model
  337. sql_view.model_id = self.env['ir.model'].create(
  338. self._prepare_model()).id
  339. sql_view.rule_id = self.env['ir.rule'].create(
  340. self._prepare_rule()).id
  341. # Drop table, created by the ORM
  342. req = "DROP TABLE %s" % (sql_view.view_name)
  343. self.env.cr.execute(req)
  344. @api.multi
  345. def _create_model_access(self):
  346. for sql_view in self:
  347. for item in sql_view._prepare_model_access():
  348. self.env['ir.model.access'].create(item)
  349. @api.multi
  350. def _drop_model_access(self):
  351. for sql_view in self:
  352. self.env['ir.model.access'].search(
  353. [('model_id', '=', sql_view.model_name)]).unlink()
  354. @api.multi
  355. def _drop_model_and_fields(self):
  356. for sql_view in self:
  357. sql_view.model_id.unlink()
  358. @api.multi
  359. def _hook_executed_request(self):
  360. self.ensure_one()
  361. req = """
  362. SELECT attnum,
  363. attname AS column,
  364. format_type(atttypid, atttypmod) AS type
  365. FROM pg_attribute
  366. WHERE attrelid = '%s'::regclass
  367. AND NOT attisdropped
  368. AND attnum > 0
  369. ORDER BY attnum;""" % (self.view_name)
  370. self.env.cr.execute(req)
  371. return self.env.cr.fetchall()
  372. @api.multi
  373. def _prepare_request_check_execution(self):
  374. self.ensure_one()
  375. return "CREATE VIEW %s AS (%s);" % (self.view_name, self.query)
  376. @api.multi
  377. def _prepare_request_for_execution(self):
  378. self.ensure_one()
  379. query = """
  380. SELECT
  381. CAST(row_number() OVER () as integer) AS id,
  382. CAST(Null as timestamp without time zone) as create_date,
  383. CAST(Null as integer) as create_uid,
  384. CAST(Null as timestamp without time zone) as write_date,
  385. CAST(Null as integer) as write_uid,
  386. my_query.*
  387. FROM
  388. (%s) as my_query
  389. """ % (self.query)
  390. return "CREATE %s VIEW %s AS (%s);" % (
  391. self.materialized_text, self.view_name, query)
  392. @api.multi
  393. def _check_execution(self):
  394. """Ensure that the query is valid, trying to execute it.
  395. a non materialized view is created for this check.
  396. A rollback is done at the end.
  397. After the execution, and before the rollback, an analysis of
  398. the database structure is done, to know fields type."""
  399. self.ensure_one()
  400. sql_view_field_obj = self.env['bi.sql.view.field']
  401. columns = super(BiSQLView, self)._check_execution()
  402. field_ids = []
  403. for column in columns:
  404. existing_field = self.bi_sql_view_field_ids.filtered(
  405. lambda x: x.name == column[1])
  406. if existing_field:
  407. # Update existing field
  408. field_ids.append(existing_field.id)
  409. existing_field.write({
  410. 'sequence': column[0],
  411. 'sql_type': column[2],
  412. })
  413. else:
  414. # Create a new one if name is prefixed by x_
  415. if column[1][:2] == 'x_':
  416. field_ids.append(sql_view_field_obj.create({
  417. 'sequence': column[0],
  418. 'name': column[1],
  419. 'sql_type': column[2],
  420. 'bi_sql_view_id': self.id,
  421. }).id)
  422. # Drop obsolete view field
  423. self.bi_sql_view_field_ids.filtered(
  424. lambda x: x.id not in field_ids).unlink()
  425. if not self.bi_sql_view_field_ids:
  426. raise UserError(_(
  427. "No Column was found.\n"
  428. "Columns name should be prefixed by 'x_'."))
  429. return columns
  430. @api.multi
  431. def _refresh_materialized_view(self):
  432. for sql_view in self:
  433. req = "REFRESH %s VIEW %s" % (
  434. sql_view.materialized_text, sql_view.view_name)
  435. self._log_execute(req)
  436. sql_view._refresh_size()
  437. @api.multi
  438. def _refresh_size(self):
  439. for sql_view in self:
  440. req = "SELECT pg_size_pretty(pg_total_relation_size('%s'));" % (
  441. sql_view.view_name)
  442. self.env.cr.execute(req)
  443. sql_view.size = self.env.cr.fetchone()[0]