diff --git a/bi_view_editor/README.rst b/bi_view_editor/README.rst index c8617df4..f347a1b7 100644 --- a/bi_view_editor/README.rst +++ b/bi_view_editor/README.rst @@ -33,7 +33,8 @@ To graphically design your analysis data-set: - Pick the interesting fields (Drag & Drop) - For each selected field, right-click on the Options column and select whether it's a row, column or measure - Save and click "Generate BI View" - +- Click "Open BI View" to view the result +- If module Dashboard (board) is installed, the standard "Add to My Dashboard" functionality would be available .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot @@ -45,9 +46,15 @@ Known issues / Roadmap * Non-stored fields are not supported * Provide graph view for table relations * Extend the capabilities of the tree views (e.g. add sums) -* Add possibility to store the BI view in user dashboard, like any other graph or cross table * Provide a tutorial (eg. a working example of usage) +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. Credits ======= diff --git a/bi_view_editor/__init__.py b/bi_view_editor/__init__.py index 1be685b8..d4da6d0f 100644 --- a/bi_view_editor/__init__.py +++ b/bi_view_editor/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -# © 2015-2016 ONESTEiN BV () +# Copyright 2015-2017 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models +from .hooks import uninstall_hook diff --git a/bi_view_editor/__openerp__.py b/bi_view_editor/__openerp__.py index fb26ff0a..52cefd71 100644 --- a/bi_view_editor/__openerp__.py +++ b/bi_view_editor/__openerp__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -# © 2015-2016 ONESTEiN BV () +# Copyright 2015-2017 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { 'name': 'BI View Editor', - 'summary': '''Graphical BI views builder for Odoo 8''', + 'summary': 'Graphical BI views builder for Odoo', 'images': ['static/description/main_screenshot.png'], - 'author': 'ONESTEiN BV,Odoo Community Association (OCA)', + 'author': 'Onestein,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'website': 'http://www.onestein.eu', 'category': 'Reporting', @@ -27,4 +27,6 @@ 'js': [ 'static/src/js/bve.js' ], + 'installable': True, + 'uninstall_hook': 'uninstall_hook' } diff --git a/bi_view_editor/hooks.py b/bi_view_editor/hooks.py new file mode 100644 index 00000000..8ed1dfab --- /dev/null +++ b/bi_view_editor/hooks.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Onestein () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +def uninstall_hook(cr, registry): + # delete dirty data that could cause problems + # while re-installing the module + cr.execute(""" + delete from ir_model where model like 'x_bve.%' + """) + cr.execute(""" + delete from bve_view where model_name like 'x_bve.%' + """) diff --git a/bi_view_editor/models/__init__.py b/bi_view_editor/models/__init__.py index 319d0dfb..204ac77a 100644 --- a/bi_view_editor/models/__init__.py +++ b/bi_view_editor/models/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2015-2016 ONESTEiN BV () +# Copyright 2015-2017 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import bve_view diff --git a/bi_view_editor/models/bve_view.py b/bi_view_editor/models/bve_view.py index 43a5355e..91e053bb 100644 --- a/bi_view_editor/models/bve_view.py +++ b/bi_view_editor/models/bve_view.py @@ -1,68 +1,63 @@ # -*- coding: utf-8 -*- -# © 2015-2016 ONESTEiN BV () +# Copyright 2015-2017 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import json -from openerp import tools -from openerp import SUPERUSER_ID -from openerp import models, fields, api +from openerp import api, fields, models, tools from openerp.exceptions import Warning as UserError -from openerp.modules.registry import RegistryManager from openerp.tools.translate import _ class BveView(models.Model): _name = 'bve.view' - _description = "BI View Editor" + _description = 'BI View Editor' @api.depends('group_ids') @api.multi def _compute_users(self): for bve_view in self: - if bve_view.sudo().group_ids: - bve_view.user_ids = self.env['res.users'].sudo().browse( - list(set([u.id for group in bve_view.sudo().group_ids - for u in group.users]))) + group_ids = bve_view.sudo().group_ids + if group_ids: + bve_view.user_ids = group_ids.mapped('users') else: bve_view.user_ids = self.env['res.users'].sudo().search([]) - name = fields.Char(size=128, string="Name", required=True) - model_name = fields.Char(size=128, string="Model Name") + name = fields.Char(required=True, copy=False) + model_name = fields.Char() - note = fields.Text(string="Notes") + note = fields.Text(string='Notes') state = fields.Selection( [('draft', 'Draft'), ('created', 'Created')], - string="State", - default="draft") + default='draft', + copy=False) data = fields.Text( - string="Data", help="Use the special query builder to define the query " "to generate your report dataset. " "NOTE: Te be edited, the query should be in 'Draft' status.") - action_id = fields.Many2one('ir.actions.act_window', string="Action") - view_id = fields.Many2one('ir.ui.view', string="View") + action_id = fields.Many2one('ir.actions.act_window', string='Action') + view_id = fields.Many2one('ir.ui.view', string='View') group_ids = fields.Many2many( 'res.groups', - string="Groups", + string='Groups', help="User groups allowed to see the generated report; " "if NO groups are specified the report will be public " "for everyone.") user_ids = fields.Many2many( 'res.users', - string="Users", + string='Users', compute=_compute_users, store=True) _sql_constraints = [ ('name_uniq', 'unique(name)', - 'Custom BI View names must be unique!'), + _('Custom BI View names must be unique!')), ] @api.multi @@ -72,20 +67,11 @@ class BveView(models.Model): raise UserError( _('You cannot delete a created view! ' 'Reset the view to draft first.')) - - super(BveView, self).unlink() - - @api.multi - def action_edit_query(self): - return { - 'type': 'ir.actions.client', - 'tag': 'bi_view_editor.open', - 'target': 'new', - 'params': {'bve_view_id': self.id} - } + return super(BveView, self).unlink() @api.multi def action_reset(self): + self.ensure_one() if self.action_id: if self.action_id.view_id: self.action_id.view_id.sudo().unlink() @@ -96,53 +82,129 @@ class BveView(models.Model): for model in models: model.sudo().unlink() - table_name = self.model_name.replace(".", "_") + table_name = self.model_name.replace('.', '_') tools.drop_view_if_exists(self.env.cr, table_name) - self.write({ - 'state': 'draft' - }) - return True - - def _create_graph_view(self): - fields_info = json.loads(self.data) - view_fields = ["""""".format( - field_info['name'], - (field_info['row'] and 'row') or - (field_info['column'] and 'col') or - (field_info['measure'] and 'measure')) - for field_info in fields_info if field_info['row'] or - field_info['column'] or field_info['measure']] - return view_fields + self.state = 'draft' - def _create_tree_view(self): - fields_info = json.loads(self.data) - view_fields = ["""""".format( - field_info['name'], - (field_info['row'] and 'row') or - (field_info['column'] and 'col') or - (field_info['measure'] and 'measure')) - for field_info in fields_info if field_info['row'] or - field_info['column'] or field_info['measure']] + @api.multi + def _create_view_arch(self): + self.ensure_one() + + def _get_field_def(field_name, def_type): + return """""".format( + field_name, def_type + ) + + def _get_field_type(field_info): + row = field_info['row'] and 'row' + column = field_info['column'] and 'col' + measure = field_info['measure'] and 'measure' + return row or column or measure + + fields_info = json.loads(self._get_format_data(self.data)) + view_fields = [] + for field_info in fields_info: + field_name = field_info['name'] + def_type = _get_field_type(field_info) + if def_type: + field_def = _get_field_def(field_name, def_type) + view_fields.append(field_def) return view_fields + @api.model + def _get_format_data(self, data): + data = data.replace('\'', '"') + data = data.replace(': u"', ':"') + return data + @api.multi def action_create(self): + self.ensure_one() + + self._create_bve_object() + self._create_bve_view() + + @api.multi + def _create_bve_view(self): + self.ensure_one() + + # create views + View = self.env['ir.ui.view'] + old_views = View.sudo().search([('model', '=', self.model_name)]) + old_views.sudo().unlink() + + view_vals = [{ + 'name': 'Pivot Analysis', + 'type': 'pivot', + 'model': self.model_name, + 'priority': 16, + 'arch': """ + {} + """.format("".join(self._create_view_arch())) + }, { + 'name': 'Graph Analysis', + 'type': 'graph', + 'model': self.model_name, + 'priority': 16, + 'arch': """ + {} + """.format("".join(self._create_view_arch())) + }] + + for vals in view_vals: + View.sudo().create(vals) + + # create Tree view + tree_view = View.sudo().create( + {'name': 'Tree Analysis', + 'type': 'tree', + 'model': self.model_name, + 'priority': 16, + 'arch': """ + {} + """.format("".join(self._create_view_arch())) + }) + + # set the Tree view as the default one + action_vals = { + 'name': self.name, + 'res_model': self.model_name, + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'tree,graph,pivot', + 'view_id': tree_view.id, + 'context': "{'service_name': '%s'}" % self.name, + } + + ActWindow = self.env['ir.actions.act_window'] + action_id = ActWindow.sudo().create(action_vals) + self.write({ + 'action_id': action_id.id, + 'view_id': tree_view.id, + 'state': 'created' + }) + + @api.multi + def _create_bve_object(self): + self.ensure_one() def _get_fields_info(fields_data): fields_info = [] for field_data in fields_data: - field = self.env['ir.model.fields'].browse(field_data["id"]) + field = self.env['ir.model.fields'].browse(field_data['id']) vals = { - "table": self.env[field.model_id.model]._table, - "table_alias": field_data["table_alias"], - "select_field": field.name, - "as_field": "x_" + field_data["name"], - "join": False, - "model": field.model_id.model + 'table': self.env[field.model_id.model]._table, + 'table_alias': field_data['table_alias'], + 'select_field': field.name, + 'as_field': 'x_' + field_data['name'], + 'join': False, + 'model': field.model_id.model } - if field_data.get("join_node"): - vals.update({"join": field_data["join_node"]}) + if field_data.get('join_node'): + vals.update({'join': field_data['join_node']}) fields_info.append(vals) return fields_info @@ -150,17 +212,19 @@ class BveView(models.Model): data = self.data if not data: raise UserError(_('No data to process.')) - info = _get_fields_info(json.loads(data)) - fields = [("{}.{}".format(f["table_alias"], - f["select_field"]), - f["as_field"]) for f in info if 'join_node' not in f] - tables = set([(f["table"], f["table_alias"]) for f in info]) + + formatted_data = json.loads(self._get_format_data(data)) + info = _get_fields_info(formatted_data) + fields = [("{}.{}".format(f['table_alias'], + f['select_field']), + f['as_field']) for f in info if 'join_node' not in f] + tables = set([(f['table'], f['table_alias']) for f in info]) join_nodes = [ - (f["table_alias"], - f["join"], - f["select_field"]) for f in info if f["join"] is not False] + (f['table_alias'], + f['join'], + f['select_field']) for f in info if f['join'] is not False] - table_name = self.model_name.replace(".", "_") + table_name = self.model_name.replace('.', '_') tools.drop_view_if_exists(self.env.cr, table_name) basic_fields = [ @@ -186,19 +250,19 @@ class BveView(models.Model): self.env.cr.execute(q) def _prepare_field(field_data): - if not field_data["custom"]: - field = self.env['ir.model.fields'].browse(field_data["id"]) + if not field_data['custom']: + field = self.env['ir.model.fields'].browse(field_data['id']) vals = { - "name": "x_" + field_data["name"], - "complete_name": field.complete_name, + 'name': 'x_' + field_data['name'], + 'complete_name': field.complete_name, 'model': self.model_name, 'relation': field.relation, - "field_description": field_data.get( - "description", field.field_description), - "ttype": field.ttype, - "selection": field.selection, - "size": field.size, - 'state': "manual" + 'field_description': field_data.get( + 'description', field.field_description), + 'ttype': field.ttype, + 'selection': field.selection, + 'size': field.size, + 'state': 'manual' } if vals['ttype'] == 'monetary': vals.update({'ttype': 'float'}) @@ -206,24 +270,26 @@ class BveView(models.Model): model_obj = self.env[field.model_id.model] selection = model_obj._columns[field.name].selection selection_domain = str(selection) - vals.update({"selection": selection_domain}) + vals.update({'selection': selection_domain}) return vals def _prepare_object(): + data = json.loads(self._get_format_data(self.data)) return { 'name': self.name, 'model': self.model_name, 'field_id': [ (0, 0, _prepare_field(field)) - for field in json.loads(self.data) + for field in data if 'join_node' not in field] } def _build_object(): - res_id = self.env['ir.model'].sudo().create(_prepare_object()) + vals = _prepare_object() + Model = self.env['ir.model'] + res_id = Model.sudo().with_context(bve=True).create(vals) return res_id - # read access def group_ids_with_access(model_name, access_mode): self.env.cr.execute('''SELECT g.id @@ -239,11 +305,12 @@ class BveView(models.Model): return [x[0] for x in self.env.cr.fetchall()] def _build_access_rules(obj): - info = json.loads(self.data) - models = list(set([f["model"] for f in info])) + info = json.loads(self._get_format_data(self.data)) + models = list(set([f['model'] for f in info])) read_groups = set.intersection(*[set( group_ids_with_access(model, 'read')) for model in models]) + # read access for group in read_groups: self.env['ir.model.access'].sudo().create({ 'name': 'read access to ' + self.model_name, @@ -252,101 +319,36 @@ class BveView(models.Model): 'perm_read': True, }) - # edit access + # read and write access for group in self.group_ids: self.env['ir.model.access'].sudo().create({ - 'name': 'read access to ' + self.model_name, + 'name': 'read-write access to ' + self.model_name, 'model_id': obj.id, 'group_id': group.id, 'perm_read': True, 'perm_write': True, }) - return - - self.model_name = "x_bve." + ''.join( + self.model_name = 'x_bve.' + ''.join( [x for x in self.name.lower() - if x.isalnum()]).replace("_", ".").replace(" ", ".") - + if x.isalnum()]).replace('_', '.').replace(' ', '.') _build_query() obj = _build_object() _build_access_rules(obj) - self.env.cr.commit() - - self.env.registry = RegistryManager.new(self.env.cr.dbname) - RegistryManager.signal_registry_change(self.env.cr.dbname) - self.pool = self.env.registry - - ui_view_obj = self.pool.get('ir.ui.view') - view_ids = ui_view_obj.search( - self.env.cr, SUPERUSER_ID, [('model', '=', self.model_name)], - context={}) - - [ui_view_obj.unlink(self.env.cr, SUPERUSER_ID, view_id, context={}) - for view_id in view_ids] - - view_ids = [] - view_id = self.pool.get('ir.ui.view').create( - self.env.cr, SUPERUSER_ID, - {'name': "Pivot Analysis", - 'type': 'pivot', - 'model': self.model_name, - 'priority': 16, - 'arch': """ - {} - """.format("".join(self._create_graph_view())) - }, context={}) - view_ids.append(view_id) - view_id = self.pool.get('ir.ui.view').create( - self.env.cr, SUPERUSER_ID, - {'name': "Graph Analysis", - 'type': 'graph', - 'model': self.model_name, - 'priority': 16, - 'arch': """ - {} - """.format("".join(self._create_graph_view())) - }, context={}) - view_ids.append(view_id) - - view_id = self.pool.get('ir.ui.view').create( - self.env.cr, SUPERUSER_ID, - {'name': "Tree Analysis", - 'type': 'tree', - 'model': self.model_name, - 'priority': 16, - 'arch': """ - {} - """.format("".join(self._create_tree_view())) - }, context={}) - view_ids.append(view_id) - - action_vals = {'name': self.name, - 'res_model': self.model_name, - 'type': 'ir.actions.act_window', - 'view_type': 'form', - 'view_mode': 'tree,graph,pivot', - 'view_id': view_ids and view_ids[0] or 0, - 'context': "{'service_name': '%s'}" % self.name, - } - act_window = self.env['ir.actions.act_window'] - action_id = act_window.sudo().create(action_vals) - - self.write({ - 'action_id': action_id.id, - 'view_id': view_id, - 'state': 'created' - }) - - return True @api.multi def open_view(self): + self.ensure_one() return { + 'name': _('BI View'), 'type': 'ir.actions.act_window', 'res_model': self.model_name, 'view_type': 'form', 'view_mode': 'tree,graph,pivot', } + + @api.multi + def copy(self, default=None): + self.ensure_one() + default = dict(default or {}, name=_("%s (copy)") % self.name) + return super(BveView, self).copy(default=default) diff --git a/bi_view_editor/models/ir_model.py b/bi_view_editor/models/ir_model.py index f898b026..44507974 100644 --- a/bi_view_editor/models/ir_model.py +++ b/bi_view_editor/models/ir_model.py @@ -1,143 +1,233 @@ # -*- coding: utf-8 -*- -# © 2015-2016 ONESTEiN BV () +# Copyright 2015-2017 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, api +from openerp import api, models from openerp.modules.registry import RegistryManager NO_BI_MODELS = [ - "temp.range", - "account.statement.operation.template", - "fetchmail.server" + 'temp.range', + 'account.statement.operation.template', + 'fetchmail.server' ] NO_BI_FIELDS = [ - "id", - "create_uid", - "create_date", - "write_uid", - "write_date" + 'id', + 'create_uid', + 'create_date', + 'write_uid', + 'write_date' ] - -def dict_for_field(field): - return { - "id": field.id, - "name": field.name, - "description": field.field_description, - "type": field.ttype, - "relation": field.relation, - "custom": False, - - "model_id": field.model_id.id, - "model": field.model_id.model, - "model_name": field.model_id.name - } +NO_BI_TTYPES = [ + 'many2many', + 'one2many', + 'html', + 'binary', + 'reference' +] -def dict_for_model(model): +def dict_for_field(field): return { - "id": model.id, - "name": model.name, - "model": model.model + 'id': field.id, + 'name': field.name, + 'description': field.field_description, + 'type': field.ttype, + 'relation': field.relation, + 'custom': False, + 'model_id': field.model_id.id, + 'model': field.model_id.model, + 'model_name': field.model_id.name } class IrModel(models.Model): _inherit = 'ir.model' + @api.model def _filter_bi_fields(self, ir_model_field_obj): name = ir_model_field_obj.name model = ir_model_field_obj.model_id model_name = model.model - if name in self.env[model_name]._columns: - f = self.env[model_name]._columns[name] + Model = self.env[model_name] + if name in Model._columns: + f = Model._columns[name] return f._classic_write return False @api.model def _filter_bi_models(self, model): - model_name = model["model"] - if model_name in NO_BI_MODELS or \ - model_name.startswith("workflow") or \ - model_name.startswith("ir.") or \ - model["name"] == "Unknow" or \ - "report" in model_name or \ - model_name.startswith("base_") or \ - "_" in model_name or \ - "." in model["name"] or \ - "mail" in model_name or \ - "edi." in model_name: - return False - return self.env['ir.model.access'].check( - model["model"], 'read', False) + + def _check_name(model_model): + if model_model in NO_BI_MODELS: + return 1 + return 0 + + def _check_startswith(model_model): + if model_model.startswith('workflow') or \ + model_model.startswith('ir.') or \ + model_model.startswith('base_'): + return 1 + return 0 + + def _check_contains(model_model): + if 'mail' in model_model or \ + '_' in model_model or \ + 'report' in model_model or \ + 'edi.' in model_model: + return 1 + return 0 + + def _check_unknow(model_name): + if model_name == 'Unknow' or '.' in model_name: + return 1 + return 0 + + model_model = model['model'] + model_name = model['name'] + count_check = 0 + count_check += _check_name(model_model) + count_check += _check_startswith(model_model) + count_check += _check_contains(model_model) + count_check += _check_unknow(model_name) + if not count_check: + return self.env['ir.model.access'].check( + model['model'], 'read', False) + return False @api.model def get_related_fields(self, model_ids): """ Return list of field dicts for all fields that can be joined with models in model_ids """ - model_names = dict([(model.id, model.model) - for model in self.env['ir.model'].sudo().search( - [('id', 'in', model_ids.values())])]) - filter_bi_fields = self._filter_bi_fields - if filter_bi_fields: - rfields = [ - dict(dict_for_field(field), - join_node=-1, - table_alias=model[0]) - for field in filter( - filter_bi_fields, - self.env['ir.model.fields'].sudo().search( - [('model_id', 'in', model_ids.values()), - ('ttype', 'in', ['many2one'])])) - for model in model_ids.items() - if model[1] == field.model_id.id - ] - - lfields = [ - dict(dict_for_field(field), - join_node=model[0], - table_alias=-1) - for field in filter( - filter_bi_fields, - self.env['ir.model.fields'].sudo().search( - [('relation', 'in', model_names.values()), - ('ttype', 'in', ['many2one'])])) - for model in model_ids.items() - if model_names[model[1]] == field['relation'] - ] - - return [dict(field, join_node=model[0]) - for field in lfields - if model_names[model[1]] == field['relation']] + [ - dict(field, table_alias=model[0]) - for model in model_ids.items() - for field in rfields if model[1] == field['model_id']] + Model = self.env['ir.model'] + domain = [('id', 'in', model_ids.values())] + models = Model.sudo().search(domain) + model_names = {} + for model in models: + model_names.update({model.id: model.model}) + + related_fields = self._get_related_fields_list(model_ids, model_names) + return related_fields + + @api.model + def _get_related_fields_list(self, model_ids, model_names): + + def _get_right_fields(model_ids, model_names): + Fields = self.env['ir.model.fields'] + rfields = [] + domain = [('model_id', 'in', model_ids.values()), + ('ttype', 'in', ['many2one'])] + for field in filter( + self._filter_bi_fields, + Fields.sudo().search(domain)): + for model in model_ids.items(): + if model[1] == field.model_id.id: + rfields.append( + dict(dict_for_field(field), + join_node=-1, + table_alias=model[0]) + ) + return rfields + + def _get_left_fields(model_ids, model_names): + Fields = self.env['ir.model.fields'] + lfields = [] + domain = [('relation', 'in', model_names.values()), + ('ttype', 'in', ['many2one'])] + for field in filter( + self._filter_bi_fields, + Fields.sudo().search(domain)): + for model in model_ids.items(): + if model_names[model[1]] == field['relation']: + lfields.append( + dict(dict_for_field(field), + join_node=model[0], + table_alias=-1) + ) + return lfields + + def _get_relation_list(model_ids, model_names, lfields): + relation_list = [] + for model in model_ids.items(): + for field in lfields: + if model_names[model[1]] == field['relation']: + relation_list.append( + dict(field, join_node=model[0]) + ) + return relation_list + + def _get_model_list(model_ids, rfields): + model_list = [] + for model in model_ids.items(): + for field in rfields: + if model[1] == field['model_id']: + model_list.append( + dict(field, table_alias=model[0]) + ) + return model_list + + lfields = _get_left_fields(model_ids, model_names) + rfields = _get_right_fields(model_ids, model_names) + + relation_list = _get_relation_list(model_ids, model_names, lfields) + model_list = _get_model_list(model_ids, rfields) + + related_fields = relation_list + model_list + return related_fields @api.model def get_related_models(self, model_ids): """ Return list of model dicts for all models that can be joined with models in model_ids """ + def _get_field(fields, orig, target): + field_list = [] + for f in fields: + if f[orig] == -1: + field_list.append(f[target]) + return field_list + + def _get_list_id(model_ids, fields): + list_model = model_ids.values() + list_model += _get_field(fields, 'table_alias', 'model_id') + return list_model + + def _get_list_relation(fields): + list_model = _get_field(fields, 'join_node', 'relation') + return list_model + + models_list = [] related_fields = self.get_related_fields(model_ids) - return sorted(filter( - self._filter_bi_models, - [{"id": model.id, "name": model.name, "model": model.model} - for model in self.env['ir.model'].sudo().search( - ['|', - ('id', 'in', model_ids.values() + [ - f['model_id'] - for f in related_fields if f['table_alias'] == -1]), - ('model', 'in', [ - f['relation'] - for f in related_fields if f['join_node'] == -1])])]), - key=lambda x: x['name']) + list_id = _get_list_id(model_ids, related_fields) + list_model = _get_list_relation(related_fields) + domain = ['|', + ('id', 'in', list_id), + ('model', 'in', list_model)] + models = self.env['ir.model'].sudo().search(domain) + for model in models: + models_list.append({ + 'id': model.id, + 'name': model.name, + 'model': model.model + }) + return sorted( + filter(self._filter_bi_models, models_list), + key=lambda x: x['name'] + ) @api.model def get_models(self): """ Return list of model dicts for all available models. """ + def dict_for_model(model): + return { + 'id': model.id, + 'name': model.name, + 'model': model.model + } + models_domain = [('transient', '=', False)] return sorted(filter( self._filter_bi_models, @@ -152,18 +242,31 @@ class IrModel(models.Model): Return all possible join nodes to add new_field to the query containing model_ids. """ - model_ids = dict([(field['table_alias'], - field['model_id']) for field in field_data]) + def _get_model_ids(field_data): + model_ids = dict([(field['table_alias'], + field['model_id']) for field in field_data]) + return model_ids + + def _get_join_nodes_dict(model_ids, new_field): + join_nodes = [] + for alias, model_id in model_ids.items(): + if model_id == new_field['model_id']: + join_nodes.append({'table_alias': alias}) + for dict_field in self.get_related_fields(model_ids): + condition = [ + dict_field['join_node'] == -1, + dict_field['table_alias'] == -1 + ] + relation = (new_field['model'] == dict_field['relation']) + model = (new_field['model_id'] == dict_field['model_id']) + if (relation and condition[0]) or (model and condition[1]): + join_nodes.append(dict_field) + return join_nodes + + model_ids = _get_model_ids(field_data) keys = [(field['table_alias'], field['id']) for field in field_data if field.get('join_node', -1) != -1] - join_nodes = ([{'table_alias': alias} - for alias, model_id in model_ids.items() - if model_id == new_field['model_id']] + [ - d for d in self.get_related_fields(model_ids) - if (d['relation'] == new_field['model'] and - d['join_node'] == -1) or - (d['model_id'] == new_field['model_id'] and - d['table_alias'] == -1)]) + join_nodes = _get_join_nodes_dict(model_ids, new_field) return filter( lambda x: 'id' not in x or (x['table_alias'], x['id']) not in keys, join_nodes) @@ -172,47 +275,53 @@ class IrModel(models.Model): def get_fields(self, model_id): bi_field_domain = [ ('model_id', '=', model_id), - ("name", "not in", NO_BI_FIELDS), - ('ttype', 'not in', [ - 'many2many', "one2many", "html", "binary", "reference"]) + ('name', 'not in', NO_BI_FIELDS), + ('ttype', 'not in', NO_BI_TTYPES) ] - filter_bi_fields = self._filter_bi_fields - fields_obj = self.env['ir.model.fields'] - fields = filter(filter_bi_fields, - fields_obj.sudo().search(bi_field_domain)) - return sorted([{"id": field.id, - "model_id": model_id, - "name": field.name, - "description": field.field_description, - "type": field.ttype, - "custom": False, - "model": field.model_id.model, - "model_name": field.model_id.name} - for field in fields], key=lambda x: x['description'], - reverse=True) - - def create(self, cr, user, vals, context=None): - if context is None: - context = {} - if context and context.get('bve'): - vals['state'] = 'base' - res = super(IrModel, self).create(cr, user, vals, context) - if vals.get('state', 'base') == 'bve': - vals['state'] = 'manual' + Fields = self.env['ir.model.fields'] + fields = filter( + self._filter_bi_fields, + Fields.sudo().search(bi_field_domain) + ) + fields_dict = [] + for field in fields: + fields_dict.append( + {'id': field.id, + 'model_id': model_id, + 'name': field.name, + 'description': field.field_description, + 'type': field.ttype, + 'custom': False, + 'model': field.model_id.model, + 'model_name': field.model_id.name + } + ) + sorted_fields = sorted( + fields_dict, + key=lambda x: x['description'], + reverse=True + ) + return sorted_fields - # add model in registry - self.instanciate(cr, user, vals['model'], context) - self.pool.setup_models(cr, partial=(not self.pool.ready)) - - RegistryManager.signal_registry_change(cr.dbname) + @api.model + def create(self, vals): + if self._context and self._context.get('bve'): + vals['state'] = 'base' + res = super(IrModel, self).create(vals) - # Following commented line (write method) is not working anymore - # as in Odoo V9 a new orm constraint is restricting the modification - # of the state while updating ir.model - # self.write(cr, user, [res], {'state': 'manual'}) + # this sql update is necessary since a write method here would + # be not working (an orm constraint is restricting the modification + # of the state field while updating ir.model) q = ("""UPDATE ir_model SET state = 'manual' - WHERE id = """ + str(res)) + WHERE id = """ + str(res.id)) + self.env.cr.execute(q) + + # update registry + if self._context.get('bve'): + # setup models; this reloads custom models in registry + self.pool.setup_models(self._cr, partial=(not self.pool.ready)) - cr.execute(q) + # signal that registry has changed + RegistryManager.signal_registry_change(self.env.cr.dbname) return res diff --git a/bi_view_editor/security/rules.xml b/bi_view_editor/security/rules.xml index d68db5db..b417a13a 100644 --- a/bi_view_editor/security/rules.xml +++ b/bi_view_editor/security/rules.xml @@ -1,13 +1,11 @@ - - + - - bve_view read access - - - ['|',('user_ids','=',False),('user_ids','in',user.id)] - + + bve_view read access + + + ['|',('user_ids','=',False),('user_ids','in',user.id)] + - - + diff --git a/bi_view_editor/static/src/css/bve.css b/bi_view_editor/static/src/css/bve.css index 0b71d1e2..fddcebe8 100644 --- a/bi_view_editor/static/src/css/bve.css +++ b/bi_view_editor/static/src/css/bve.css @@ -32,7 +32,7 @@ } .oe_form_field_bi_editor .body { - padding-bottom: 0px; + padding-bottom: 0; } .oe_form_field_bi_editor .body .left { @@ -50,15 +50,15 @@ .oe_form_field_bi_editor .body .left .search-bar input { width: 100%; - border-radius: 0px; - border-left: 0px; - border-right: 0px; - border-top: 0px; + border-radius: 0; + border-left: 0; + border-right: 0; + border-top: 0; padding-left: 18px; padding-top: 4px; position: absolute; - left: 0px; - top: 0px; + left: 0; + top: 0; z-index: 1; } @@ -70,7 +70,7 @@ } .oe_form_field_bi_editor .body .left .class-list { - height: 400px; /* FIXME */ + height: 400px; overflow-y: scroll; overflow-x: hidden; } @@ -100,7 +100,7 @@ width: 70%; float: left; box-sizing: border-box; - height: 423px; /* FIXME */ + height: 423px; overflow-y: scroll; overflow-x: hidden; } @@ -138,7 +138,7 @@ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); border: 1px solid #DDDDDD; list-style-type: none; - padding: 0px; + padding: 0; width: 175px; } diff --git a/bi_view_editor/static/src/js/bve.js b/bi_view_editor/static/src/js/bve.js index c0be9b52..455adaf8 100644 --- a/bi_view_editor/static/src/js/bve.js +++ b/bi_view_editor/static/src/js/bve.js @@ -8,7 +8,6 @@ openerp.bi_view_editor = function (instance, local) { this._super.apply(this, arguments); }, start: function() { - var self = this; this._super(); this.on("change:effective_readonly", this, function() { this.display_field(); @@ -49,28 +48,28 @@ openerp.bi_view_editor = function (instance, local) { } }, filter: function(val) { - val = (typeof val != 'undefined') ? val.toLowerCase() : this.currentFilter; + val = (typeof val !== 'undefined') ? val.toLowerCase() : this.currentFilter; this.currentFilter = val; - this.$el.find(".class-list .class-container").each(function() { + this.$el.find(".class-list .class-container").each(function() { var modelData = $(this).find(".class").data('model-data'); //TODO: filter on all model fields (name, technical name, etc) - if(typeof modelData == 'undefined' || (modelData.name.toLowerCase().indexOf(val) == -1 && modelData.model.toLowerCase().indexOf(val) == -1)) + if(typeof modelData === 'undefined' || (modelData.name.toLowerCase().indexOf(val) === -1 && modelData.model.toLowerCase().indexOf(val) === -1)) $(this).hide(); else $(this).show(); }); }, get_field_icons: function(field) { - icons = ""; + var icons = ""; if(field.column) icons += " "; if(field.row) icons += " "; if(field.measure) icons += " "; - - return icons; + + return icons; }, update_field_view: function(row) { row.find("td:nth-child(3)").html(this.get_field_icons(row.data('field-data'))); @@ -79,13 +78,13 @@ openerp.bi_view_editor = function (instance, local) { this.set_fields(JSON.parse(this.get('value'))); }, load_classes: function(scrollTo) { - scrollTo = (typeof scrollTo == 'undefined') ? false : scrollTo; + scrollTo = (typeof scrollTo === 'undefined') ? false : scrollTo; var self = this; var model = new instance.web.Model("ir.model"); if (this.$el.find(".field-list tbody tr").length > 0) { model.call("get_related_models", [this.get_model_ids()], { context: new instance.web.CompoundContext() }).then(function(result) { self.show_classes(result); - }); + }); } else { model.call("get_models", { context: new instance.web.CompoundContext() }).then(function(result) { self.show_classes(result); @@ -98,177 +97,147 @@ openerp.bi_view_editor = function (instance, local) { self.$el.find(".class-list .class").remove(); self.$el.find(".class-list .field").remove(); var css = this.get('effective_readonly') ? 'cursor: default' : 'cursor: pointer'; - + function addField() { + if (!self.get("effective_readonly")) { + self.add_field($(this)); + } + } + function clickHandler(evt) { + if(self.get("effective_readonly")) return; + var classel = $(this); + if (classel.data('bve-processed')) { + classel.parent().find('.field').remove(); + classel.data('bve-processed', false); + var index = self.activeModelMenus.indexOf(classel.data('model-data').id); + if(index !== -1) self.activeModelMenus.splice(index, 1); + } else { + self.activeModelMenus.push(classel.data('model-data').id); + model.call("get_fields", [classel.data('model-data').id], { context: new instance.web.CompoundContext() }).then(function(result) { + for (var i = 0; i < result.length; i++) { + classel.find("#bve-field-" + result[i].name).remove(); + self._render_field(self, i, result, classel, addField) + } + }); + $(this).data('bve-processed', true); + } + } + function renderFields(result) { + console.log(result); + var item = self.$el.find(".class-list #bve-class-" + result[0].model_id); + for (var o = 0; o < result.length; o++) { + self._render_field(self, o, result, item, addField) + } + item.data('bve-processed', true); + } for (var i = 0; i < result.length; i++) { var item = $("
" + result[i].name + "
") .data('model-data', result[i]) - .click(function (evt) { - if(self.get("effective_readonly")) return; - var classel = $(this); - - if (classel.data('bve-processed')) { - classel.parent().find('.field').remove(); - classel.data('bve-processed', false); - var index = self.activeModelMenus.indexOf(classel.data('model-data').id); - if(index != -1) self.activeModelMenus.splice(index, 1); - } else { - self.activeModelMenus.push(classel.data('model-data').id); - model.call("get_fields", [classel.data('model-data').id], { context: new instance.web.CompoundContext() }).then(function(result) { - for (var i = 0; i < result.length; i++) { - classel.find("#bve-field-" + result[i].name).remove(); - if(self.$el.find(".field-list tbody [name=label-" + result[i].id + "]").length > 0) continue; - classel.after($("
" + result[i].description + "
") - .data('field-data', result[i]) - .click(function () { - if (!self.get("effective_readonly")) { - self.add_field($(this)); - } - }) - .draggable({ - 'revert': 'invalid', - 'scroll': false, - 'helper': 'clone', - 'appendTo': 'body', - 'containment': 'window' - }) - ); - } - }); - - $(this).data('bve-processed', true); - } - }) + .click(clickHandler) .wrap("
").parent(); self.$el.find(".class-list").append(item); - + var index = self.activeModelMenus.indexOf(item.find(".class").data('model-data').id); - if(index != -1 && !self.get("effective_readonly")) { - model.call("get_fields", [self.activeModelMenus[index]], { context: new instance.web.CompoundContext() }).then(function(result) { - console.log(result); - var item = self.$el.find(".class-list #bve-class-" + result[0].model_id); - for (var o = 0; o < result.length; o++) { - if(self.$el.find(".field-list tbody [name=label-" + result[o].id + "]").length > 0) continue; - item.after($("
" + result[o].description + "
") - .data('field-data', result[o]) - .click(function () { - if (!self.get("effective_readonly")) { - self.add_field($(this)); - } - }) - .draggable({ - 'revert': 'invalid', - 'scroll': false, - 'helper': 'clone', - 'appendTo': 'body', - 'containment': 'window' - })); - } - item.data('bve-processed', true); - }); + if(index !== -1 && !self.get("effective_readonly")) { + model.call("get_fields", [self.activeModelMenus[index]], { context: new instance.web.CompoundContext() }).then(renderFields); } self.filter(); } - + + }, + _render_field(_self, _index, _result, _item, _addField) { + if(_self.$el.find(".field-list tbody [name=label-" + _result[_index].id + "]").length > 0) return; + _item.after($("
" + _result[_index].description + "
") + .data('field-data', _result[_index]) + .click(_addField) + .draggable({ + 'revert': 'invalid', + 'scroll': false, + 'helper': 'clone', + 'appendTo': 'body', + 'containment': 'window' + }) + ); + }, + set_checkbox: function(check, identifier, _contextMenu) { + if(check) + _contextMenu.find(identifier).attr('checked', true); + else + _contextMenu.find(identifier).attr('checked', false); + }, + _false_if_undefined: function(to_check) { + if (typeof check === 'undefined') return false; + return check; }, add_field_to_table: function(data, options) { var self = this; - if (typeof data.row == 'undefined') { - data.row = false; - } - if (typeof data.column == 'undefined') { - data.column = false; - } - if (typeof data.measure == 'undefined') { - data.measure = false; - } + + data.row = self._false_if_undefined(data.row); + data.column = self._false_if_undefined(data.column); + data.measure = self._false_if_undefined(data.measure); var n = 1; var name = data.name; - while ($.grep(self.get_fields(), function (el) { return el.name == data.name;}).length > 0) { + function checkNameMatches(el) { return el.name === data.name;} + while ($.grep(self.get_fields(), checkNameMatches).length > 0) { data.name = name + '_' + n; n += 1; } var classes = ""; - if (typeof data.join_node != 'undefined') { + if (typeof data.join_node !== 'undefined') { classes = "join-node displaynone"; } var delete_button = ""; var disabled = " disabled=\"disabled\" "; - if (!this.get("effective_readonly")) { + if (!this.get("effective_readonly")) { delete_button = ""; disabled = ""; } self.$el.find(".field-list tbody") - .append($("" + data.model_name + "" + self.get_field_icons(data) + "" + delete_button + "") + .append($("" + data.model_name + "" + self.get_field_icons(data) + "" + delete_button + "") .data('field-data', data) .contextmenu(function(e) { e.preventDefault(); if (self.get("effective_readonly")) return; var target = $(e.currentTarget); var currentFieldData = target.data('field-data'); - + var contextMenu = self.$el.find(".context-menu"); contextMenu.css("left", e.pageX + "px"); contextMenu.css("top", e.pageY + "px"); - contextMenu.mouseleave(function() { + contextMenu.mouseleave(function() { contextMenu.hide(); }); - contextMenu.find("li").hover(function() { + contextMenu.find("li").hover(function() { $(this).find("ul").css("color", "#000"); $(this).find("ul").show(); - }, function() { + }, function() { $(this).find("ul").hide(); }); - + + //Set checkboxes - if(currentFieldData.column) - contextMenu.find('#column-checkbox').attr('checked', true); - else - contextMenu.find('#column-checkbox').attr('checked', false); - - if(currentFieldData.row) - contextMenu.find('#row-checkbox').attr('checked', true); - else - contextMenu.find('#row-checkbox').attr('checked', false); - - if(currentFieldData.measure) - contextMenu.find('#measure-checkbox').attr('checked', true); - else - contextMenu.find('#measure-checkbox').attr('checked', false); - - if(currentFieldData.type == "float" || currentFieldData.type == "integer" || currentFieldData.type == "monetary") { - contextMenu.find('#column-checkbox').attr('disabled', true); - contextMenu.find('#row-checkbox').attr('disabled', true); - contextMenu.find('#measure-checkbox').attr('disabled', false); - } - else { - contextMenu.find('#column-checkbox').attr('disabled', false); - contextMenu.find('#row-checkbox').attr('disabled', false); - contextMenu.find('#measure-checkbox').attr('disabled', true); - } - + self.set_checkbox(currentFieldData.column, '#column-checkbox', contextMenu); + self.set_checkbox(currentFieldData.row, '#row-checkbox', contextMenu); + self.set_checkbox(currentFieldData.measure, '#measure-checkbox', contextMenu); + + var to_disable = false; + if(currentFieldData.type === "float" || currentFieldData.type === "integer" || currentFieldData.type === "monetary") to_disable = true; + var identifiers = [['#column-checkbox', 'column', to_disable], ['#row-checkbox', 'row', to_disable], ['#measure-checkbox', 'measure', !to_disable]]; + identifiers.forEach(function (element) { + contextMenu.find(element[0]).attr('disabled', element[2]); + }); + //Add change events - contextMenu.find('#column-checkbox').unbind("change"); - contextMenu.find('#column-checkbox').change(function() { - currentFieldData.column = $(this).is(":checked"); - target.data('field-data', currentFieldData); - self.update_field_view(target); - self.internal_set_value(JSON.stringify(self.get_fields())); - }); - contextMenu.find('#row-checkbox').unbind("change"); - contextMenu.find('#row-checkbox').change(function() { - currentFieldData.row = $(this).is(":checked"); - target.data('field-data', currentFieldData); - self.update_field_view(target); - self.internal_set_value(JSON.stringify(self.get_fields())); - }); - contextMenu.find('#measure-checkbox').unbind("change"); - contextMenu.find('#measure-checkbox').change(function() { - currentFieldData.measure = $(this).is(":checked"); - target.data('field-data', currentFieldData); - self.update_field_view(target); - self.internal_set_value(JSON.stringify(self.get_fields())); + identifiers.forEach(function (element) { + contextMenu.find(element[0]).unbind("change"); + contextMenu.find(element[0]).change(function() { + currentFieldData[element[1]] = $(this).is(":checked"); + target.data('field-data', currentFieldData); + self.update_field_view(target); + self.internal_set_value(JSON.stringify(self.get_fields())); + }); + }); contextMenu.show(); @@ -289,7 +258,7 @@ openerp.bi_view_editor = function (instance, local) { self.$el.find('.delete-button').click(function() { $(this).closest('tr').remove(); self.clean_join_nodes(); - self.internal_set_value(JSON.stringify(self.get_fields())); + self.internal_set_value(JSON.stringify(self.get_fields())); self.load_classes(); return false; }); @@ -299,10 +268,10 @@ openerp.bi_view_editor = function (instance, local) { var d = $(this).data('field-data'); return d.table_alias; })); - + this.$el.find(".field-list tbody tr").each(function (idx, el) { var d = $(this).data('field-data'); - if (typeof d.join_node != 'undefined' && aliases.indexOf(d.join_node) === -1) { + if (typeof d.join_node !== 'undefined' && aliases.indexOf(d.join_node) === -1) { $(this).remove(); } }); @@ -321,31 +290,31 @@ openerp.bi_view_editor = function (instance, local) { var d = $(this).data('field-data'); model_data[d.table_alias] = {model_id: d.model_id, model_name: d.model_name}; }); - return model_data; + return model_data; }, get_table_alias: function(field) { - if (typeof field.table_alias != 'undefined') { + if (typeof field.table_alias !== 'undefined') { return field.table_alias; } else { var model_ids = this.get_model_ids(); var n = 0; - while (typeof model_ids["t" + n] != 'undefined') n++; + while (typeof model_ids["t" + n] !== 'undefined') n++; return "t" + n; } }, add_field_and_join_node: function(field, join_node) { var self = this; - if (join_node.join_node == -1) { - field.table_alias = self.get_table_alias(field); - join_node.join_node = field.table_alias; - self.add_field_to_table(join_node); - } else if (join_node.table_alias == -1) { + + var go_to_else = true; + if (join_node.join_node === -1 || join_node.table_alias === -1){ + go_to_else = false; field.table_alias = self.get_table_alias(field); - join_node.table_alias = field.table_alias; + if (join_node.join_node === -1) join_node.join_node = field.table_alias; + else join_node.table_alias = field.table_alias; self.add_field_to_table(join_node); - } else { - field.table_alias = join_node.table_alias; } + else field.table_alias = join_node.table_alias; + self.add_field_to_table(field); self.internal_set_value(JSON.stringify(self.get_fields())); self.load_classes(field); @@ -358,7 +327,7 @@ openerp.bi_view_editor = function (instance, local) { var self = this; model.call('get_join_nodes', [field_data, data], {context: new instance.web.CompoundContext()}).then(function(result) { - if (result.length == 1) { + if (result.length === 1) { self.add_field_and_join_node(data, result[0]); self.internal_set_value(JSON.stringify(self.get_fields())); //self.load_classes(data); @@ -367,7 +336,7 @@ openerp.bi_view_editor = function (instance, local) { pop.display_popup(result, self.get_model_data(), self.add_field_and_join_node.bind(self), data); } else { // first field and table only. - table_alias = self.get_table_alias(data); + var table_alias = self.get_table_alias(data); data.table_alias = table_alias; self.add_field_to_table(data); self.internal_set_value(JSON.stringify(self.get_fields())); @@ -396,7 +365,6 @@ openerp.bi_view_editor = function (instance, local) { }); instance.web.form.widgets.add('BVEEditor', 'instance.bi_view_editor.BVEEditor'); - local.JoinNodePopup = instance.web.Widget.extend({ template: "JoinNodePopup", start: function() { @@ -410,20 +378,20 @@ openerp.bi_view_editor = function (instance, local) { joinnodes.empty(); for (var i=0; i" + description+ "") .data('idx', i) .wrap("

") .parent()); - + } var dialog = new instance.web.Dialog(this, { dialogClass: 'oe_act_window', @@ -439,4 +407,5 @@ openerp.bi_view_editor = function (instance, local) { this.start(); } }); + }; diff --git a/bi_view_editor/templates/assets_template.xml b/bi_view_editor/templates/assets_template.xml index ad6014c5..94ea7cd5 100644 --- a/bi_view_editor/templates/assets_template.xml +++ b/bi_view_editor/templates/assets_template.xml @@ -2,7 +2,7 @@ -