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.

462 lines
16 KiB

  1. # Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
  2. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
  3. import json
  4. from odoo import api, fields, models, tools
  5. from odoo.exceptions import UserError
  6. from odoo.tools.translate import _
  7. class BveView(models.Model):
  8. _name = 'bve.view'
  9. _description = 'BI View Editor'
  10. @api.depends('group_ids')
  11. @api.multi
  12. def _compute_users(self):
  13. for bve_view in self:
  14. group_ids = bve_view.sudo().group_ids
  15. if group_ids:
  16. bve_view.user_ids = group_ids.mapped('users')
  17. else:
  18. bve_view.user_ids = self.env['res.users'].sudo().search([])
  19. @api.depends('name')
  20. @api.multi
  21. def _compute_model_name(self):
  22. for bve_view in self:
  23. name = [x for x in bve_view.name.lower() if x.isalnum()]
  24. model_name = ''.join(name).replace('_', '.').replace(' ', '.')
  25. bve_view.model_name = 'x_bve.' + model_name
  26. name = fields.Char(required=True, copy=False)
  27. model_name = fields.Char(compute='_compute_model_name', store=True)
  28. note = fields.Text(string='Notes')
  29. state = fields.Selection(
  30. [('draft', 'Draft'),
  31. ('created', 'Created')],
  32. default='draft',
  33. copy=False)
  34. data = fields.Text(
  35. help="Use the special query builder to define the query "
  36. "to generate your report dataset. "
  37. "NOTE: To be edited, the query should be in 'Draft' status.")
  38. action_id = fields.Many2one('ir.actions.act_window', string='Action')
  39. view_id = fields.Many2one('ir.ui.view', string='View')
  40. group_ids = fields.Many2many(
  41. 'res.groups',
  42. string='Groups',
  43. help="User groups allowed to see the generated report; "
  44. "if NO groups are specified the report will be public "
  45. "for everyone.")
  46. user_ids = fields.Many2many(
  47. 'res.users',
  48. string='Users',
  49. compute='_compute_users',
  50. store=True)
  51. _sql_constraints = [
  52. ('name_uniq',
  53. 'unique(name)',
  54. _('Custom BI View names must be unique!')),
  55. ]
  56. @classmethod
  57. def _get_format_data(cls, data):
  58. data = data.replace('\'', '"')
  59. data = data.replace(': u"', ':"')
  60. return data
  61. @api.multi
  62. def _create_view_arch(self):
  63. self.ensure_one()
  64. def _get_field_def(name, def_type=''):
  65. if not def_type:
  66. return ''
  67. return """<field name="x_{}" type="{}" />""".format(
  68. name, def_type
  69. )
  70. def _get_field_type(field_info):
  71. row = field_info['row'] and 'row'
  72. column = field_info['column'] and 'col'
  73. measure = field_info['measure'] and 'measure'
  74. return row or column or measure
  75. def _get_field_list(fields_info):
  76. view_fields = []
  77. for field_info in fields_info:
  78. field_name = field_info['name']
  79. def_type = _get_field_type(field_info)
  80. if def_type:
  81. field_def = _get_field_def(field_name, def_type)
  82. view_fields.append(field_def)
  83. return view_fields
  84. fields_info = json.loads(self._get_format_data(self.data))
  85. view_fields = _get_field_list(fields_info)
  86. return view_fields
  87. @api.multi
  88. def _create_tree_view_arch(self):
  89. self.ensure_one()
  90. def _get_field_def(name):
  91. return """<field name="x_{}" />""".format(
  92. name
  93. )
  94. def _get_field_list(fields_info):
  95. view_fields = []
  96. for field_info in fields_info:
  97. field_name = field_info['name']
  98. if field_info['list'] and 'join_node' not in field_info:
  99. field_def = _get_field_def(field_name)
  100. view_fields.append(field_def)
  101. return view_fields
  102. fields_info = json.loads(self._get_format_data(self.data))
  103. view_fields = _get_field_list(fields_info)
  104. return view_fields
  105. @api.multi
  106. def _create_bve_view(self):
  107. self.ensure_one()
  108. # create views
  109. View = self.env['ir.ui.view']
  110. old_views = View.sudo().search([('model', '=', self.model_name)])
  111. old_views.unlink()
  112. view_vals = [{
  113. 'name': 'Pivot Analysis',
  114. 'type': 'pivot',
  115. 'model': self.model_name,
  116. 'priority': 16,
  117. 'arch': """<?xml version="1.0"?>
  118. <pivot string="Pivot Analysis">
  119. {}
  120. </pivot>
  121. """.format("".join(self._create_view_arch()))
  122. }, {
  123. 'name': 'Graph Analysis',
  124. 'type': 'graph',
  125. 'model': self.model_name,
  126. 'priority': 16,
  127. 'arch': """<?xml version="1.0"?>
  128. <graph string="Graph Analysis"
  129. type="bar" stacked="True">
  130. {}
  131. </graph>
  132. """.format("".join(self._create_view_arch()))
  133. }, {
  134. 'name': 'Search BI View',
  135. 'type': 'search',
  136. 'model': self.model_name,
  137. 'priority': 16,
  138. 'arch': """<?xml version="1.0"?>
  139. <search string="Search BI View">
  140. {}
  141. </search>
  142. """.format("".join(self._create_view_arch()))
  143. }]
  144. for vals in view_vals:
  145. View.sudo().create(vals)
  146. # create Tree view
  147. tree_view = View.sudo().create({
  148. 'name': 'Tree Analysis',
  149. 'type': 'tree',
  150. 'model': self.model_name,
  151. 'priority': 16,
  152. 'arch': """<?xml version="1.0"?>
  153. <tree string="List Analysis" create="false">
  154. {}
  155. </tree>
  156. """.format("".join(self._create_tree_view_arch()))
  157. })
  158. # set the Tree view as the default one
  159. action_vals = {
  160. 'name': self.name,
  161. 'res_model': self.model_name,
  162. 'type': 'ir.actions.act_window',
  163. 'view_type': 'form',
  164. 'view_mode': 'tree,graph,pivot',
  165. 'view_id': tree_view.id,
  166. 'context': "{'service_name': '%s'}" % self.name,
  167. }
  168. ActWindow = self.env['ir.actions.act_window']
  169. action_id = ActWindow.sudo().create(action_vals)
  170. self.write({
  171. 'action_id': action_id.id,
  172. 'view_id': tree_view.id,
  173. 'state': 'created'
  174. })
  175. @api.multi
  176. def _build_access_rules(self, model):
  177. self.ensure_one()
  178. def group_ids_with_access(model_name, access_mode):
  179. # pylint: disable=sql-injection
  180. self.env.cr.execute('''SELECT
  181. g.id
  182. FROM
  183. ir_model_access a
  184. JOIN ir_model m ON (a.model_id=m.id)
  185. JOIN res_groups g ON (a.group_id=g.id)
  186. WHERE
  187. m.model=%s AND
  188. a.active = true AND
  189. a.perm_''' + access_mode, (model_name,))
  190. res = self.env.cr.fetchall()
  191. return [x[0] for x in res]
  192. info = json.loads(self._get_format_data(self.data))
  193. model_names = list(set([f['model'] for f in info]))
  194. read_groups = set.intersection(*[set(
  195. group_ids_with_access(model_name, 'read')
  196. ) for model_name in model_names])
  197. if not read_groups and not self.group_ids:
  198. raise UserError(_('Please select at least one group'
  199. ' on the security tab.'))
  200. # read access
  201. for group in read_groups:
  202. self.env['ir.model.access'].sudo().create({
  203. 'name': 'read access to ' + self.model_name,
  204. 'model_id': model.id,
  205. 'group_id': group,
  206. 'perm_read': True,
  207. })
  208. # read and write access
  209. for group in self.group_ids:
  210. self.env['ir.model.access'].sudo().create({
  211. 'name': 'read-write access to ' + self.model_name,
  212. 'model_id': model.id,
  213. 'group_id': group.id,
  214. 'perm_read': True,
  215. 'perm_write': True,
  216. })
  217. @api.model
  218. def _create_sql_view(self):
  219. def get_fields_info(fields_data):
  220. fields_info = []
  221. for field_data in fields_data:
  222. field = self.env['ir.model.fields'].browse(field_data['id'])
  223. vals = {
  224. 'table': self.env[field.model_id.model]._table,
  225. 'table_alias': field_data['table_alias'],
  226. 'select_field': field.name,
  227. 'as_field': 'x_' + field_data['name'],
  228. 'join': False,
  229. 'model': field.model_id.model
  230. }
  231. if field_data.get('join_node'):
  232. vals.update({'join': field_data['join_node']})
  233. fields_info.append(vals)
  234. return fields_info
  235. def get_join_nodes(info):
  236. join_nodes = [
  237. (f['table_alias'],
  238. f['join'],
  239. f['select_field']) for f in info if f['join'] is not False]
  240. return join_nodes
  241. def get_tables(info):
  242. tables = set([(f['table'], f['table_alias']) for f in info])
  243. return tables
  244. def get_fields(info):
  245. return [("{}.{}".format(f['table_alias'],
  246. f['select_field']),
  247. f['as_field']) for f in info if 'join_node' not in f]
  248. def check_empty_data(data):
  249. if not data or data == '[]':
  250. raise UserError(_('No data to process.'))
  251. check_empty_data(self.data)
  252. formatted_data = json.loads(self._get_format_data(self.data))
  253. info = get_fields_info(formatted_data)
  254. select_fields = get_fields(info)
  255. tables = get_tables(info)
  256. join_nodes = get_join_nodes(info)
  257. table_name = self.model_name.replace('.', '_')
  258. # robustness in case something went wrong
  259. # pylint: disable=sql-injection
  260. self._cr.execute('DROP TABLE IF EXISTS "%s"' % table_name)
  261. basic_fields = [
  262. ("t0.id", "id")
  263. ]
  264. # pylint: disable=sql-injection
  265. q = """CREATE or REPLACE VIEW %s as (
  266. SELECT %s
  267. FROM %s
  268. WHERE %s
  269. )""" % (table_name, ','.join(
  270. ["{} AS {}".format(f[0], f[1])
  271. for f in basic_fields + select_fields]), ','.join(
  272. ["{} AS {}".format(t[0], t[1])
  273. for t in list(tables)]), " AND ".join(
  274. ["{}.{} = {}.id".format(j[0], j[2], j[1])
  275. for j in join_nodes] + ["TRUE"]))
  276. self.env.cr.execute(q)
  277. @api.multi
  278. def action_translations(self):
  279. self.ensure_one()
  280. model = self.env['ir.model'].sudo().search([
  281. ('model', '=', self.model_name)
  282. ])
  283. translation_obj = self.env['ir.translation'].sudo()
  284. translation_obj.translate_fields('ir.model', model.id)
  285. for field_id in model.field_id.ids:
  286. translation_obj.translate_fields('ir.model.fields', field_id)
  287. return {
  288. 'name': 'Translations',
  289. 'res_model': 'ir.translation',
  290. 'type': 'ir.actions.act_window',
  291. 'view_mode': 'tree',
  292. 'view_id': self.env.ref('base.view_translation_dialog_tree').id,
  293. 'target': 'current',
  294. 'flags': {'search_view': True, 'action_buttons': True},
  295. 'domain': [
  296. '|',
  297. '&',
  298. ('res_id', 'in', model.field_id.ids),
  299. ('name', '=', 'ir.model.fields,field_description'),
  300. '&',
  301. ('res_id', '=', model.id),
  302. ('name', '=', 'ir.model,name')
  303. ],
  304. }
  305. @api.multi
  306. def action_create(self):
  307. self.ensure_one()
  308. def _prepare_field(field_data):
  309. if not field_data['custom']:
  310. field = self.env['ir.model.fields'].browse(field_data['id'])
  311. vals = {
  312. 'name': 'x_' + field_data['name'],
  313. 'complete_name': field.complete_name,
  314. 'model': self.model_name,
  315. 'relation': field.relation,
  316. 'field_description': field_data.get(
  317. 'description', field.field_description),
  318. 'ttype': field.ttype,
  319. 'selection': field.selection,
  320. 'size': field.size,
  321. 'state': 'manual',
  322. 'readonly': True
  323. }
  324. if vals['ttype'] == 'monetary':
  325. vals.update({'ttype': 'float'})
  326. if field.ttype == 'selection' and not field.selection:
  327. model_obj = self.env[field.model_id.model]
  328. selection = model_obj._fields[field.name].selection
  329. if callable(selection):
  330. selection_domain = selection(model_obj)
  331. else:
  332. selection_domain = selection
  333. vals.update({'selection': str(selection_domain)})
  334. return vals
  335. # clean dirty view (in case something went wrong)
  336. self.action_reset()
  337. # create sql view
  338. self._create_sql_view()
  339. # create model and fields
  340. data = json.loads(self._get_format_data(self.data))
  341. model_vals = {
  342. 'name': self.name,
  343. 'model': self.model_name,
  344. 'state': 'manual',
  345. 'field_id': [
  346. (0, 0, _prepare_field(field))
  347. for field in data
  348. if 'join_node' not in field]
  349. }
  350. Model = self.env['ir.model'].sudo().with_context(bve=True)
  351. model = Model.create(model_vals)
  352. # give access rights
  353. self._build_access_rules(model)
  354. # create tree, graph and pivot views
  355. self._create_bve_view()
  356. @api.multi
  357. def open_view(self):
  358. self.ensure_one()
  359. [action] = self.action_id.read()
  360. action['display_name'] = _('BI View')
  361. return action
  362. @api.multi
  363. def copy(self, default=None):
  364. self.ensure_one()
  365. default = dict(default or {}, name=_("%s (copy)") % self.name)
  366. return super(BveView, self).copy(default=default)
  367. @api.multi
  368. def action_reset(self):
  369. self.ensure_one()
  370. has_menus = False
  371. if self.action_id:
  372. action = 'ir.actions.act_window,%d' % (self.action_id.id,)
  373. menus = self.env['ir.ui.menu'].sudo().search([
  374. ('action', '=', action)
  375. ])
  376. has_menus = True if menus else False
  377. menus.unlink()
  378. if self.action_id.view_id:
  379. self.action_id.view_id.sudo().unlink()
  380. self.action_id.sudo().unlink()
  381. self.env['ir.ui.view'].sudo().search(
  382. [('model', '=', self.model_name)]).unlink()
  383. ir_models = self.env['ir.model'].sudo().search([
  384. ('model', '=', self.model_name)
  385. ])
  386. for model in ir_models:
  387. model.unlink()
  388. table_name = self.model_name.replace('.', '_')
  389. tools.drop_view_if_exists(self.env.cr, table_name)
  390. self.state = 'draft'
  391. if has_menus:
  392. return {'type': 'ir.actions.client', 'tag': 'reload'}
  393. @api.multi
  394. def unlink(self):
  395. for view in self:
  396. if view.state == 'created':
  397. raise UserError(
  398. _('You cannot delete a created view! '
  399. 'Reset the view to draft first.'))
  400. return super(BveView, self).unlink()