diff --git a/bi_view_editor/__manifest__.py b/bi_view_editor/__manifest__.py index 77b6e44b..49bb3094 100644 --- a/bi_view_editor/__manifest__.py +++ b/bi_view_editor/__manifest__.py @@ -1,4 +1,4 @@ -# Copyright 2015-2018 Onestein () +# Copyright 2015-2019 Onestein () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { @@ -9,11 +9,10 @@ 'license': 'AGPL-3', 'website': 'https://github.com/OCA/reporting-engine', 'category': 'Reporting', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', + 'development_status': 'Beta', 'depends': [ - 'base', 'web', - 'base_sparse_field' ], 'data': [ 'security/ir.model.access.csv', diff --git a/bi_view_editor/hooks.py b/bi_view_editor/hooks.py index 75746381..dee77f94 100644 --- a/bi_view_editor/hooks.py +++ b/bi_view_editor/hooks.py @@ -1,4 +1,4 @@ -# Copyright 2015-2018 Onestein () +# Copyright 2015-2019 Onestein () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging @@ -12,7 +12,7 @@ _logger = logging.getLogger(__name__) def _bi_view(_name): - return _name[0:6] == 'x_bve.' + return _name.startswith('x_bve.') def post_load(): diff --git a/bi_view_editor/migrations/10.0.1.0.2/post-migrate.py b/bi_view_editor/migrations/10.0.1.0.2/post-migrate.py deleted file mode 100644 index 806bb2d6..00000000 --- a/bi_view_editor/migrations/10.0.1.0.2/post-migrate.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2018 Simone Rubino - Agile Business Group -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from openupgradelib.openupgrade import logged_query, migrate -import json - - -@migrate() -def migrate(env, version): - cr = env.cr - convert_text_to_serialized( - cr, env['bve.view']._table, env['bve.view']._fields['data'].name) - pass - - -def convert_text_to_serialized( - cr, table, text_field_name, serialized_field_name=None): - """ - Convert Text field value to Serialized value. - """ - if not serialized_field_name: - serialized_field_name = text_field_name - select_query = """ -SELECT - id, - %(text_field_name)s -FROM %(table)s -WHERE %(text_field_name)s IS NOT NULL -""" - cr.execute( - select_query % { - 'text_field_name': text_field_name, - 'table': table, - } - ) - update_query = """ -UPDATE %(table)s - SET %(serialized_field_name)s = %%(field_value)s - WHERE id = %(record_id)d -""" - for row in cr.fetchall(): - # Fill in the field_value later because it needs escaping - row_update_query = update_query % { - 'serialized_field_name': serialized_field_name, - 'table': table, - 'record_id': row[0]} - logged_query( - cr, row_update_query, { - 'field_value': json.dumps(row[1]) - }) diff --git a/bi_view_editor/models/__init__.py b/bi_view_editor/models/__init__.py index cacf524b..b8e17515 100644 --- a/bi_view_editor/models/__init__.py +++ b/bi_view_editor/models/__init__.py @@ -2,4 +2,5 @@ from . import models from . import bve_view +from . import bve_view_line from . import ir_model diff --git a/bi_view_editor/models/bve_view.py b/bi_view_editor/models/bve_view.py index d7042cfd..c3a8fe03 100644 --- a/bi_view_editor/models/bve_view.py +++ b/bi_view_editor/models/bve_view.py @@ -1,49 +1,81 @@ -# Copyright 2015-2018 Onestein () +# Copyright 2015-2019 Onestein () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import json +from psycopg2.extensions import AsIs -from odoo import api, fields, models, tools -from odoo.exceptions import UserError -from odoo.tools.translate import _ - -from odoo.addons.base_sparse_field.models.fields import Serialized +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError, ValidationError class BveView(models.Model): _name = 'bve.view' _description = 'BI View Editor' - @api.depends('group_ids') - @api.multi + @api.depends('group_ids', 'group_ids.users') def _compute_users(self): - for bve_view in self: - group_ids = bve_view.sudo().group_ids - if group_ids: - bve_view.user_ids = group_ids.mapped('users') + for bve_view in self.sudo(): + if bve_view.group_ids: + bve_view.user_ids = bve_view.group_ids.mapped('users') else: bve_view.user_ids = self.env['res.users'].sudo().search([]) @api.depends('name') - @api.multi def _compute_model_name(self): for bve_view in self: name = [x for x in bve_view.name.lower() if x.isalnum()] model_name = ''.join(name).replace('_', '.').replace(' ', '.') bve_view.model_name = 'x_bve.' + model_name + def _compute_serialized_data(self): + for bve_view in self: + serialized_data = [] + for line in bve_view.line_ids.sorted(key=lambda r: r.sequence): + serialized_data_dict = { + 'sequence': line.sequence, + 'model_id': line.model_id.id, + 'id': line.field_id.id, + 'name': line.name, + 'model_name': line.model_id.name, + 'model': line.model_id.model, + 'type': line.ttype, + 'table_alias': line.table_alias, + 'description': line.description, + 'row': line.row, + 'column': line.column, + 'measure': line.measure, + 'list': line.in_list, + } + if line.join_node: + serialized_data_dict.update({ + 'join_node': line.join_node, + 'relation': line.relation, + }) + serialized_data += [serialized_data_dict] + bve_view.data = json.dumps(serialized_data) + + def _inverse_serialized_data(self): + for bve_view in self: + line_ids = self._sync_lines_and_data(bve_view.data) + bve_view.write({'line_ids': line_ids}) + name = fields.Char(required=True, copy=False) model_name = fields.Char(compute='_compute_model_name', store=True) note = fields.Text(string='Notes') - state = fields.Selection( - [('draft', 'Draft'), - ('created', 'Created')], - default='draft', - copy=False) - data = Serialized( + state = fields.Selection([ + ('draft', 'Draft'), + ('created', 'Created') + ], default='draft', copy=False) + data = fields.Char( + compute='_compute_serialized_data', + inverse='_inverse_serialized_data', help="Use the special query builder to define the query " "to generate your report dataset. " "NOTE: To be edited, the query should be in 'Draft' status.") + line_ids = fields.One2many( + 'bve.view.line', + 'bve_view_id', + string='Lines') action_id = fields.Many2one('ir.actions.act_window', string='Action') view_id = fields.Many2one('ir.ui.view', string='View') group_ids = fields.Many2many( @@ -57,6 +89,7 @@ class BveView(models.Model): string='Users', compute='_compute_users', store=True) + query = fields.Text() _sql_constraints = [ ('name_uniq', @@ -68,31 +101,20 @@ class BveView(models.Model): def _create_view_arch(self): self.ensure_one() - def _get_field_def(name, def_type=''): - if not def_type: - return '' - return """""".format( - 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' + def _get_field_def(name, def_type): + return """""".format(name, def_type) + + def _get_field_type(line): + row = line.row and 'row' + column = line.column and 'col' + measure = line.measure and 'measure' return row or column or measure - def _get_field_list(fields_info): - 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 - - fields_info = json.loads(self.data) - view_fields = _get_field_list(fields_info) + view_fields = [] + for line in self.line_ids: + def_type = _get_field_type(line) + if def_type: + view_fields.append(_get_field_def(line.name, def_type)) return view_fields @api.multi @@ -100,22 +122,12 @@ class BveView(models.Model): self.ensure_one() def _get_field_def(name): - return """""".format( - name - ) - - def _get_field_list(fields_info): - view_fields = [] - for field_info in fields_info: - field_name = field_info['name'] - if field_info['list'] and 'join_node' not in field_info: - field_def = _get_field_def(field_name) - view_fields.append(field_def) - return view_fields - - fields_info = json.loads(self.data) - - view_fields = _get_field_list(fields_info) + return """""".format(name) + + view_fields = [] + for line in self.line_ids: + if line.in_list and not line.join_node: + view_fields.append(_get_field_def(line.name)) return view_fields @api.multi @@ -160,8 +172,7 @@ class BveView(models.Model): """.format("".join(self._create_view_arch())) }] - for vals in view_vals: - View.sudo().create(vals) + View.sudo().create(view_vals) # create Tree view tree_view = View.sudo().create({ @@ -199,132 +210,105 @@ class BveView(models.Model): def _build_access_rules(self, model): self.ensure_one() - def group_ids_with_access(model_name, access_mode): - # pylint: disable=sql-injection - self.env.cr.execute('''SELECT - g.id - FROM - ir_model_access a - JOIN ir_model m ON (a.model_id=m.id) - JOIN res_groups g ON (a.group_id=g.id) - WHERE - m.model=%s AND - a.active = true AND - a.perm_''' + access_mode, (model_name,)) - res = self.env.cr.fetchall() - return [x[0] for x in res] - - info = json.loads(self.data) - model_names = list(set([f['model'] for f in info])) - read_groups = set.intersection(*[set( - group_ids_with_access(model_name, 'read') - ) for model_name in model_names]) - - if not read_groups and not self.group_ids: - raise UserError(_('Please select at least one group' - ' on the security tab.')) - - # read access - for group in read_groups: + if not self.group_ids: self.env['ir.model.access'].sudo().create({ 'name': 'read access to ' + self.model_name, 'model_id': model.id, - 'group_id': group, 'perm_read': True, }) + else: + # read access only to model + access_vals = [] + for group in self.group_ids: + access_vals += [{ + 'name': 'read access to ' + self.model_name, + 'model_id': model.id, + 'group_id': group.id, + 'perm_read': True + }] + self.env['ir.model.access'].sudo().create(access_vals) - # read and write access - for group in self.group_ids: - self.env['ir.model.access'].sudo().create({ - 'name': 'read-write access to ' + self.model_name, - 'model_id': model.id, - 'group_id': group.id, - 'perm_read': True, - 'perm_write': True, - }) - - @api.model + @api.multi def _create_sql_view(self): + self.ensure_one() - def get_fields_info(fields_data): + def get_fields_info(lines): fields_info = [] - for field_data in fields_data: - field = self.env['ir.model.fields'].browse(field_data['id']) + for line in lines: 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[line.field_id.model_id.model]._table, + 'table_alias': line.table_alias, + 'select_field': line.field_id.name, + 'as_field': line.name, + 'join': line.join_node, } - if field_data.get('join_node'): - vals.update({'join': field_data['join_node']}) fields_info.append(vals) return fields_info def get_join_nodes(info): - join_nodes = [ - (f['table_alias'], - f['join'], - f['select_field']) for f in info if f['join'] is not False] - return join_nodes + return [( + f['table_alias'], + f['join'], + f['select_field'] + ) for f in info if f['join']] def get_tables(info): - tables = set([(f['table'], f['table_alias']) for f in info]) - return tables - - def get_fields(info): - return [("{}.{}".format(f['table_alias'], - f['select_field']), - f['as_field']) for f in info if 'join_node' not in f] + return set([(f['table'], f['table_alias']) for f in info]) - def check_empty_data(data): - if not data or data == '[]': - raise UserError(_('No data to process.')) + def get_select_fields(info): + first_field = [(info[0]['table_alias'] + ".id", "id")] + next_fields = [ + ("{}.{}".format(f['table_alias'], f['select_field']), + f['as_field']) for f in info if 'join_node' not in f + ] + return first_field + next_fields - check_empty_data(self.data) + if not self.line_ids: + raise UserError(_('No data to process.')) - formatted_data = json.loads(self.data) - info = get_fields_info(formatted_data) - select_fields = get_fields(info) + info = get_fields_info(self.line_ids) + select_fields = get_select_fields(info) tables = get_tables(info) join_nodes = get_join_nodes(info) - table_name = self.model_name.replace('.', '_') + view_name = self.model_name.replace('.', '_') + select_str = ', '.join(["{} AS {}".format(f[0], f[1]) + for f in select_fields]) + from_str = ', '.join(["{} AS {}".format(t[0], t[1]) + for t in list(tables)]) + where_str = " AND ".join(["{}.{} = {}.id".format(j[0], j[2], j[1]) + for j in join_nodes]) # robustness in case something went wrong - # pylint: disable=sql-injection - self._cr.execute('DROP TABLE IF EXISTS "%s"' % table_name) - - basic_fields = [ - ("t0.id", "id") - ] - # pylint: disable=sql-injection - q = """CREATE or REPLACE VIEW %s as ( + self._cr.execute('DROP TABLE IF EXISTS %s', (AsIs(view_name), )) + + self.query = """ SELECT %s + FROM %s + """ % (AsIs(select_str), AsIs(from_str), ) + if where_str: + self.query += """ WHERE %s - )""" % (table_name, ','.join( - ["{} AS {}".format(f[0], f[1]) - for f in basic_fields + select_fields]), ','.join( - ["{} AS {}".format(t[0], t[1]) - for t in list(tables)]), " AND ".join( - ["{}.{} = {}.id".format(j[0], j[2], j[1]) - for j in join_nodes] + ["TRUE"])) + """ % (AsIs(where_str), ) - self.env.cr.execute(q) + self.env.cr.execute( + """CREATE or REPLACE VIEW %s as ( + %s + )""", (AsIs(view_name), AsIs(self.query), )) @api.multi def action_translations(self): self.ensure_one() + if self.state != 'created': + return model = self.env['ir.model'].sudo().search([ ('model', '=', self.model_name) ]) - translation_obj = self.env['ir.translation'].sudo() - translation_obj.translate_fields('ir.model', model.id) + IrTranslation = self.env['ir.translation'].sudo() + IrTranslation.translate_fields('ir.model', model.id) for field_id in model.field_id.ids: - translation_obj.translate_fields('ir.model.fields', field_id) + IrTranslation.translate_fields('ir.model.fields', field_id) return { 'name': 'Translations', 'res_model': 'ir.translation', @@ -348,53 +332,51 @@ class BveView(models.Model): def action_create(self): self.ensure_one() - def _prepare_field(field_data): - 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, - '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', - 'readonly': True - } - if vals['ttype'] == 'monetary': - vals.update({'ttype': 'float'}) - if field.ttype == 'selection' and not field.selection: - model_obj = self.env[field.model_id.model] - selection = model_obj._fields[field.name].selection - if callable(selection): - selection_domain = selection(model_obj) - else: - selection_domain = selection - vals.update({'selection': str(selection_domain)}) - return vals - - # clean dirty view (in case something went wrong) - self.action_reset() + def _prepare_field(line): + field = line.field_id + vals = { + 'name': line.name, + 'complete_name': field.complete_name, + 'model': self.model_name, + 'relation': field.relation, + 'field_description': line.description, + 'ttype': field.ttype, + 'selection': field.selection, + 'size': field.size, + 'state': 'manual', + 'readonly': True, + 'groups': [(6, 0, field.groups.ids)], + } + if vals['ttype'] == 'monetary': + vals.update({'ttype': 'float'}) + if field.ttype == 'selection' and not field.selection: + model_obj = self.env[field.model_id.model] + selection = model_obj._fields[field.name].selection + if callable(selection): + selection_domain = selection(model_obj) + else: + selection_domain = selection + vals.update({'selection': str(selection_domain)}) + return vals + + self._check_invalid_lines() + self._check_groups_consistency() + + # force removal of dirty views in case something went wrong + self.sudo().action_reset() # create sql view self._create_sql_view() # create model and fields - data = json.loads(self.data) - model_vals = { + fields_data = self.line_ids.filtered(lambda l: not l.join_node) + field_ids = [(0, 0, _prepare_field(f)) for f in fields_data] + model = self.env['ir.model'].sudo().with_context(bve=True).create({ 'name': self.name, 'model': self.model_name, 'state': 'manual', - 'field_id': [ - (0, 0, _prepare_field(field)) - for field in data - if 'join_node' not in field] - } - Model = self.env['ir.model'].sudo().with_context(bve=True) - model = Model.create(model_vals) + 'field_id': field_ids, + }) # give access rights self._build_access_rules(model) @@ -402,9 +384,59 @@ class BveView(models.Model): # create tree, graph and pivot views self._create_bve_view() + def _check_groups_consistency(self): + self.ensure_one() + + if not self.group_ids: + return + + for line_model in self.line_ids.mapped('model_id'): + res_count = self.env['ir.model.access'].sudo().search([ + ('model_id', '=', line_model.id), + ('perm_read', '=', True), + '|', + ('group_id', '=', False), + ('group_id', 'in', self.group_ids.ids), + ], limit=1) + if not res_count: + access_records = self.env['ir.model.access'].sudo().search([ + ('model_id', '=', line_model.id), + ('perm_read', '=', True), + ]) + group_list = '' + for group in access_records.mapped('group_id'): + group_list += ' * %s\n' % (group.full_name, ) + msg_title = _( + 'The model "%s" cannot be accessed by users with the ' + 'selected groups only.' % (line_model.name, )) + msg_details = _( + 'At least one of the following groups must be added:') + raise UserError(_( + '%s\n\n%s\n%s' % (msg_title, msg_details, group_list,) + )) + + def _check_invalid_lines(self): + self.ensure_one() + if any(not line.model_id for line in self.line_ids): + invalid_lines = self.line_ids.filtered(lambda l: not l.model_id) + missing_models = set(invalid_lines.mapped('model_name')) + missing_models = ', '.join(missing_models) + raise UserError(_( + 'Following models are missing: %s.\n' + 'Probably some modules were uninstalled.' % (missing_models,) + )) + if any(not line.field_id for line in self.line_ids): + invalid_lines = self.line_ids.filtered(lambda l: not l.field_id) + missing_fields = set(invalid_lines.mapped('field_name')) + missing_fields = ', '.join(missing_fields) + raise UserError(_( + 'Following fields are missing: %s.' % (missing_fields,) + )) + @api.multi def open_view(self): self.ensure_one() + self._check_invalid_lines() [action] = self.action_id.read() action['display_name'] = _('BI View') return action @@ -413,7 +445,7 @@ class BveView(models.Model): def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % self.name) - return super(BveView, self).copy(default=default) + return super().copy(default=default) @api.multi def action_reset(self): @@ -422,23 +454,22 @@ class BveView(models.Model): has_menus = False if self.action_id: action = 'ir.actions.act_window,%d' % (self.action_id.id,) - menus = self.env['ir.ui.menu'].sudo().search([ + menus = self.env['ir.ui.menu'].search([ ('action', '=', action) ]) has_menus = True if menus else False menus.unlink() if self.action_id.view_id: - self.action_id.view_id.sudo().unlink() - self.action_id.sudo().unlink() + self.sudo().action_id.view_id.unlink() + self.sudo().action_id.unlink() self.env['ir.ui.view'].sudo().search( [('model', '=', self.model_name)]).unlink() - ir_models = self.env['ir.model'].sudo().search([ - ('model', '=', self.model_name) - ]) - for model in ir_models: - model.unlink() + models_to_delete = self.env['ir.model'].sudo().search([ + ('model', '=', self.model_name)]) + if models_to_delete: + models_to_delete.unlink() table_name = self.model_name.replace('.', '_') tools.drop_view_if_exists(self.env.cr, table_name) @@ -450,9 +481,74 @@ class BveView(models.Model): @api.multi def unlink(self): + if self.filtered(lambda v: v.state == 'created'): + raise UserError( + _('You cannot delete a created view! ' + 'Reset the view to draft first.')) + return super().unlink() + + @api.model + def _sync_lines_and_data(self, data): + line_ids = [(5, 0, 0)] + fields_info = {} + if data: + fields_info = json.loads(data) + + table_model_map = {} + for item in fields_info: + if item.get('join_node', -1) == -1: + table_model_map[item['table_alias']] = item['model_id'] + + for sequence, field_info in enumerate(fields_info, start=1): + join_model_id = False + join_node = field_info.get('join_node', -1) + if join_node != -1 and table_model_map.get(join_node): + join_model_id = int(table_model_map[join_node]) + + line_ids += [(0, False, { + 'sequence': sequence, + 'model_id': field_info['model_id'], + 'table_alias': field_info['table_alias'], + 'description': field_info['description'], + 'field_id': field_info['id'], + 'ttype': field_info['type'], + 'row': field_info['row'], + 'column': field_info['column'], + 'measure': field_info['measure'], + 'in_list': field_info['list'], + 'relation': field_info.get('relation'), + 'join_node': field_info.get('join_node'), + 'join_model_id': join_model_id, + })] + return line_ids + + @api.constrains('line_ids') + def _constraint_line_ids(self): for view in self: - if view.state == 'created': - raise UserError( - _('You cannot delete a created view! ' - 'Reset the view to draft first.')) - return super(BveView, self).unlink() + nodes = view.line_ids.filtered(lambda n: n.join_node) + nodes_models = nodes.mapped('table_alias') + nodes_models += nodes.mapped('join_node') + not_nodes = view.line_ids.filtered(lambda n: not n.join_node) + not_nodes_models = not_nodes.mapped('table_alias') + err_msg = _('Inconsistent lines.') + if set(nodes_models) - set(not_nodes_models): + raise ValidationError(err_msg) + if len(set(not_nodes_models) - set(nodes_models)) > 1: + raise ValidationError(err_msg) + + @api.model + def get_clean_list(self, data_dict): + serialized_data = json.loads(data_dict) + table_alias_list = set() + for item in serialized_data: + if item.get('join_node', -1) == -1: + table_alias_list.add(item['table_alias']) + + for item in serialized_data: + if item.get('join_node', -1) != -1: + if item['table_alias'] not in table_alias_list: + serialized_data.remove(item) + elif item['join_node'] not in table_alias_list: + serialized_data.remove(item) + + return json.dumps(serialized_data) diff --git a/bi_view_editor/models/bve_view_line.py b/bi_view_editor/models/bve_view_line.py new file mode 100644 index 00000000..ab30e885 --- /dev/null +++ b/bi_view_editor/models/bve_view_line.py @@ -0,0 +1,63 @@ +# Copyright 2015-2019 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class BveViewLine(models.Model): + _name = 'bve.view.line' + _description = 'BI View Editor Lines' + + name = fields.Char(compute='_compute_name') + sequence = fields.Integer(default=1) + bve_view_id = fields.Many2one('bve.view', ondelete='cascade') + model_id = fields.Many2one('ir.model', string='Model') + model_name = fields.Char(compute='_compute_model_name', store=True) + table_alias = fields.Char() + join_model_id = fields.Many2one('ir.model', string='Join Model') + field_id = fields.Many2one('ir.model.fields', string='Field') + field_name = fields.Char(compute='_compute_model_field_name', store=True) + ttype = fields.Char(string='Type') + description = fields.Char(translate=True) + relation = fields.Char() + join_node = fields.Char() + + row = fields.Boolean() + column = fields.Boolean() + measure = fields.Boolean() + in_list = fields.Boolean() + + @api.constrains('row', 'column', 'measure') + def _constrains_options_check(self): + measure_types = ['float', 'integer', 'monetary'] + for line in self: + if line.row or line.column: + if line.join_model_id or line.ttype in measure_types: + err_msg = _('This field cannot be a row or a column.') + raise ValidationError(err_msg) + if line.measure: + if line.join_model_id or line.ttype not in measure_types: + err_msg = _('This field cannot be a measure.') + raise ValidationError(err_msg) + + @api.depends('field_id', 'sequence') + def _compute_name(self): + for line in self: + if line.field_id: + field_name = line.field_id.name + line.name = 'x_bve_%s_%s' % (line.sequence, field_name,) + + @api.depends('model_id') + def _compute_model_name(self): + for line in self: + if line.model_id: + line.model_name = line.model_id.model + + @api.depends('field_id') + def _compute_model_field_name(self): + for line in self: + if line.field_id: + field_name = line.description + model_name = line.model_name + line.field_name = '%s (%s)' % (field_name, model_name, ) diff --git a/bi_view_editor/models/ir_model.py b/bi_view_editor/models/ir_model.py index 2003b209..b3ce86cc 100644 --- a/bi_view_editor/models/ir_model.py +++ b/bi_view_editor/models/ir_model.py @@ -1,6 +1,8 @@ -# Copyright 2015-2018 Onestein () +# Copyright 2015-2019 Onestein () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from collections import defaultdict + from odoo import api, models, registry NO_BI_MODELS = [ @@ -9,14 +11,6 @@ NO_BI_MODELS = [ 'fetchmail.server' ] -NO_BI_FIELDS = [ - 'id', - 'create_uid', - 'create_date', - 'write_uid', - 'write_date' -] - NO_BI_TTYPES = [ 'many2many', 'one2many', @@ -73,8 +67,8 @@ class IrModel(models.Model): return 1 return 0 - def _check_unknow(model_name): - if model_name == 'Unknow' or '.' in model_name: + def _check_unknown(model_name): + if model_name == 'Unknown' or '.' in model_name: return 1 return 0 @@ -84,7 +78,7 @@ class IrModel(models.Model): count_check += _check_name(model_model) count_check += _check_startswith(model_model) count_check += _check_contains(model_model) - count_check += _check_unknow(model_name) + count_check += _check_unknown(model_name) if not count_check: return self.env['ir.model.access'].check( model['model'], 'read', False) @@ -97,100 +91,72 @@ class IrModel(models.Model): key=lambda x: x['name']) return res - @api.model - def _search_fields(self, domain): - Fields = self.env['ir.model.fields'] - fields = Fields.sudo().search(domain) - return fields - - @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 - """ - - def get_model_list(model_ids): - model_list = [] - domain = [('model_id', 'in', list(model_ids.values())), - ('store', '=', True), - ('ttype', 'in', ['many2one'])] - filtered_fields = self._search_fields(domain) - for model in model_ids.items(): - for field in filtered_fields: - if model[1] == field.model_id.id: - model_list.append( - dict(dict_for_field(field), - join_node=-1, - table_alias=model[0]) - ) - return model_list - - def get_relation_list(model_ids, model_names): - relation_list = [] - domain = [('relation', 'in', list(model_names.values())), - ('store', '=', True), - ('ttype', 'in', ['many2one'])] - filtered_fields = self._search_fields(domain) - for model in model_ids.items(): - for field in filtered_fields: - if model_names[model[1]] == field['relation']: - relation_list.append( - dict(dict_for_field(field), - join_node=model[0], - table_alias=-1) - ) - return relation_list - - models = self.sudo().browse(model_ids.values()) + def get_model_list(self, model_table_map): + if not model_table_map: + return [] + domain = [('model_id', 'in', list(model_table_map.keys())), + ('store', '=', True), + ('ttype', '=', 'many2one')] + fields = self.env['ir.model.fields'].sudo().search(domain) + model_list = [] + for field in fields: + for table_alias in model_table_map[field.model_id.id]: + model_list.append(dict( + dict_for_field(field), + table_alias=table_alias, + join_node=-1, + )) + return model_list + + def get_relation_list(self, model_table_map): + if not model_table_map: + return [] model_names = {} - for model in models: - model_names.update({model.id: model.model}) - - model_list = get_model_list(model_ids) - relation_list = get_relation_list(model_ids, model_names) - - return relation_list + model_list + for model in self.sudo().browse(model_table_map.keys()): + model_names.update({model.model: model.id}) + + domain = [('relation', 'in', list(model_names.keys())), + ('store', '=', True), + ('ttype', '=', 'many2one')] + fields = self.env['ir.model.fields'].sudo().search(domain) + relation_list = [] + for field in fields: + model_id = model_names[field.relation] + for join_node in model_table_map[model_id]: + relation_list.append(dict( + dict_for_field(field), + join_node=join_node, + table_alias=-1 + )) + return relation_list @api.model - def get_related_models(self, model_ids): + def get_related_models(self, model_table_map): """ Return list of model dicts for all models that can be joined with the already selected models. """ - - 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 = list(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) - 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)] - for model in self.sudo().search(domain): - models_list.append(dict_for_model(model)) - return self.sort_filter_models(models_list) + domain = [('transient', '=', False)] + if model_table_map: + model_list = self.get_model_list(model_table_map) + relation_list = self.get_relation_list(model_table_map) + model_ids = [f['model_id'] for f in relation_list + model_list] + model_ids += list(model_table_map.keys()) + relations = [f['relation'] for f in model_list] + domain += [ + '|', ('id', 'in', model_ids), ('model', 'in', relations)] + return self.sudo().search(domain) @api.model - def get_models(self): + def get_models(self, table_model_map=None): """ Return list of model dicts for all available models. """ + self = self.with_context(lang=self.env.user.lang) + model_table_map = defaultdict(list) + for k, v in (table_model_map or {}).items(): + model_table_map[v].append(k) models_list = [] - for model in self.search([('transient', '=', False)]): + for model in self.get_related_models(model_table_map): models_list.append(dict_for_model(model)) return self.sort_filter_models(models_list) @@ -201,21 +167,23 @@ class IrModel(models.Model): Return all possible join nodes to add new_field to the query containing model_ids. """ - 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_model_table_map(field_data): + table_map = defaultdict(list) + for data in field_data: + table_map[data['model_id']].append(data['table_alias']) + return table_map - def _get_join_nodes_dict(model_ids, new_field): + def _get_join_nodes_dict(model_table_map, 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 field in self.get_related_fields(model_ids): - c = [field['join_node'] == -1, field['table_alias'] == -1] - a = (new_field['model'] == field['relation']) - b = (new_field['model_id'] == field['model_id']) - if (a and c[0]) or (b and c[1]): + for alias in model_table_map[new_field['model_id']]: + join_nodes.append({'table_alias': alias}) + + for field in self.get_model_list(model_table_map): + if new_field['model'] == field['relation']: + join_nodes.append(field) + + for field in self.get_relation_list(model_table_map): + if new_field['model_id'] == field['model_id']: join_nodes.append(field) return join_nodes @@ -229,10 +197,11 @@ class IrModel(models.Model): nodes_list.append(node) return nodes_list - model_ids = _get_model_ids(field_data) + self = self.with_context(lang=self.env.user.lang) + model_table_map = _get_model_table_map(field_data) keys = [(field['table_alias'], field['id']) for field in field_data if field.get('join_node', -1) != -1] - join_nodes = _get_join_nodes_dict(model_ids, new_field) + join_nodes = _get_join_nodes_dict(model_table_map, new_field) join_nodes = remove_duplicate_nodes(join_nodes) return list(filter( @@ -241,38 +210,34 @@ class IrModel(models.Model): @api.model def get_fields(self, model_id): + self = self.with_context(lang=self.env.user.lang) domain = [ ('model_id', '=', model_id), ('store', '=', True), - ('name', 'not in', NO_BI_FIELDS), + ('name', 'not in', models.MAGIC_COLUMNS), ('ttype', 'not in', NO_BI_TTYPES) ] fields_dict = [] - filtered_fields = self._search_fields(domain) - for field in filtered_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( + for field in self.env['ir.model.fields'].sudo().search(domain): + fields_dict.append({ + 'id': field.id, + 'model_id': model_id, + 'name': field.name, + 'description': field.field_description, + 'type': field.ttype, + 'model': field.model, + }) + return sorted( fields_dict, key=lambda x: x['description'], reverse=True ) - return sorted_fields @api.model def create(self, vals): if self.env.context and self.env.context.get('bve'): vals['state'] = 'base' - res = super(IrModel, self).create(vals) + res = super().create(vals) # this sql update is necessary since a write method here would # be not working (an orm constraint is restricting the modification diff --git a/bi_view_editor/models/models.py b/bi_view_editor/models/models.py index 546b44b5..ae95bf5b 100644 --- a/bi_view_editor/models/models.py +++ b/bi_view_editor/models/models.py @@ -1,4 +1,4 @@ -# Copyright 2017-2018 Onestein () +# Copyright 2017-2019 Onestein () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging @@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__) @api.model def _bi_view(_name): - return _name[0:6] == 'x_bve.' + return _name.startswith('x_bve.') _auto_init_orig = models.BaseModel._auto_init @@ -41,23 +41,23 @@ class Base(models.AbstractModel): @api.model def _setup_complete(self): if not _bi_view(self._name): - super(Base, self)._setup_complete() + super()._setup_complete() else: self.pool.models[self._name]._log_access = False @api.model def _read_group_process_groupby(self, gb, query): if not _bi_view(self._name): - return super(Base, self)._read_group_process_groupby(gb, query) + return super()._read_group_process_groupby(gb, query) split = gb.split(':') if split[0] not in self._fields: raise UserError( _('No data to be displayed.')) - return super(Base, self)._read_group_process_groupby(gb, query) + return super()._read_group_process_groupby(gb, query) @api.model def _add_magic_fields(self): if _bi_view(self._name): self._log_access = False - return super(Base, self)._add_magic_fields() + return super()._add_magic_fields() diff --git a/bi_view_editor/readme/ROADMAP.rst b/bi_view_editor/readme/ROADMAP.rst index 4d261c61..0a98a82b 100644 --- a/bi_view_editor/readme/ROADMAP.rst +++ b/bi_view_editor/readme/ROADMAP.rst @@ -6,5 +6,4 @@ * Find better ways to extend the *_auto_init()* without override * Possibly avoid the monkey patches * Data the user has no access to (e.g. in a multi company situation) can be viewed by making a view -* Store the JSON data structure in ORM * Would be nice if models available to select when creating a view are limited to the ones that have intersecting groups (for non technical users) diff --git a/bi_view_editor/security/ir.model.access.csv b/bi_view_editor/security/ir.model.access.csv index 81d91dc4..dd6cc2da 100644 --- a/bi_view_editor/security/ir.model.access.csv +++ b/bi_view_editor/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_bve_view_everyone,bve.view,bi_view_editor.model_bve_view,,1,1,1,1 +access_bve_view_line,access_bve_view_line,model_bve_view_line,,1,1,1,1 diff --git a/bi_view_editor/static/src/js/bi_view_editor.FieldList.js b/bi_view_editor/static/src/js/bi_view_editor.FieldList.js index 087db302..2525e223 100644 --- a/bi_view_editor/static/src/js/bi_view_editor.FieldList.js +++ b/bi_view_editor/static/src/js/bi_view_editor.FieldList.js @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 Onestein () +/* Copyright 2015-2019 Onestein () * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ odoo.define('bi_view_editor.FieldList', function (require) { @@ -12,18 +12,18 @@ odoo.define('bi_view_editor.FieldList', function (require) { start: function () { var res = this._super.apply(this, arguments); this.$el.mouseleave(function () { - $(this).addClass('hidden'); + $(this).addClass('d-none'); }); return res; }, open: function (x, y) { this.$el.css({ 'left': x + 'px', - 'top': y + 'px' + 'top': y + 'px', }); - this.$el.removeClass('hidden'); + this.$el.removeClass('d-none'); return _.extend({}, window.Backbone.Events); - } + }, }); var FieldListFieldContextMenu = FieldListContextMenu.extend({ @@ -55,7 +55,7 @@ odoo.define('bi_view_editor.FieldList', function (require) { }); return events; - } + }, }); var FieldListJoinContextMenu = FieldListContextMenu.extend({ @@ -72,14 +72,14 @@ odoo.define('bi_view_editor.FieldList', function (require) { events.trigger('change', node); }); return events; - } + }, }); var FieldList = Widget.extend({ template: 'bi_view_editor.FieldList', events: { 'click .delete-button': 'removeClicked', - 'keyup input[name="description"]': 'keyupDescription' + 'keyup input[name="description"]': 'keyupDescription', }, start: function () { var res = this._super.apply(this, arguments); @@ -94,10 +94,10 @@ odoo.define('bi_view_editor.FieldList', function (require) { setMode: function (mode) { if (mode === 'readonly') { this.$el.find('input[type="text"]').attr('disabled', true); - this.$el.find(".delete-button:last").addClass('hidden'); + this.$el.find(".delete-button").addClass('d-none'); } else { this.$el.find('input[type="text"]').removeAttr('disabled'); - this.$el.find(".delete-button:last").removeClass('hidden'); + this.$el.find(".delete-button").removeClass('d-none'); } this.mode = mode; }, @@ -122,7 +122,7 @@ odoo.define('bi_view_editor.FieldList', function (require) { var data = $(this).data('field'); model_data[data.table_alias] = { model_id: data.model_id, - model_name: data.model_name + model_name: data.model_name, }; }); return model_data; @@ -149,7 +149,7 @@ odoo.define('bi_view_editor.FieldList', function (require) { // Render table row var $html = $(qweb.render(field.join_node ? 'bi_view_editor.JoinListItem' : 'bi_view_editor.FieldListItem', { - 'field': field + 'field': field, })).data('field', field).contextmenu(function (e) { var $item = $(this); if (self.mode === 'readonly') { @@ -160,17 +160,10 @@ odoo.define('bi_view_editor.FieldList', function (require) { }); this.$el.find('tbody').append($html); - - this.$el.find(".delete-button").addClass('hidden'); - this.$el.find(".delete-button:last").removeClass('hidden'); - this.order(); }, remove: function (id) { var $item = this.$el.find('tr[data-id="' + id + '"]'); $item.remove(); - this.cleanJoinNodes(); - this.$el.find(".delete-button").addClass('hidden'); - this.$el.find(".delete-button:last").removeClass('hidden'); this.trigger('removed', id); }, set: function (fields) { @@ -182,8 +175,6 @@ odoo.define('bi_view_editor.FieldList', function (require) { for (var i = 0; i < set_fields.length; i++) { this.add(set_fields[i]); } - this.$el.find(".delete-button").addClass('hidden'); - this.$el.find(".delete-button:last").removeClass('hidden'); }, openContextMenu: function ($item, x, y) { var field = $item.data('field'); @@ -205,9 +196,9 @@ odoo.define('bi_view_editor.FieldList', function (require) { var $attribute = $(this); var value = data[$attribute.attr('data-for')]; if (value) { - $attribute.removeClass('hidden'); + $attribute.removeClass('d-none'); } else { - $attribute.addClass('hidden'); + $attribute.addClass('d-none'); } }); }, @@ -219,87 +210,12 @@ odoo.define('bi_view_editor.FieldList', function (require) { keyupDescription: function () { this.trigger('updated'); }, - cleanJoinNodes: function () { - var aliases = $.makeArray(this.$el.find("tbody tr").map(function () { - var data = $(this).data('field'); - return data.table_alias.localeCompare(data.join_node) > 0 ? data.join_node : data.table_alias; - })); - - this.$el.find("tbody tr").each(function () { - var data = $(this).data('field'); - if (typeof data.join_node === 'undefined') { - return; - } - var no_alias = data.table_alias.localeCompare(data.join_node) > 0 && - aliases.indexOf(data.table_alias) === -1; - if (no_alias || - aliases.indexOf(data.join_node) === -1) { - $(this).remove(); - } - }); - }, - getOrder: function () { - var items = this.get(); - var ordered = items.sort(function (a, b) { - var res = a.table_alias.localeCompare(b.table_alias); - if (res === 0) { - var both_join_node = a.join_node && b.join_node; - var both_not_join_node = !a.join_node && !b.join_node; - if (both_join_node || both_not_join_node) { - return 0; - } else if (!a.join_node && b.join_node) { - if (b.table_alias.localeCompare(b.join_node) > 0) { - return 1; - } - return -1; - } else if (a.join_node && !b.join_node) { - if (a.table_alias.localeCompare(a.join_node) > 0) { - return -1; - } - return 1; - } - } - return res; - }); - - var res = []; - _.each(ordered, function (item) { - var already_exists = _.findIndex(res, function (f) { - return f._id === item._id; - }) !== -1; - if (already_exists) { - return; - } - res.push(item); - if (item.join_node) { - var join_node_fields = _.filter(ordered, function (f) { - return f.table_alias === item.join_node && !f.join_node; - }); - res = _.union(res, join_node_fields); - } - }); - return res; - }, - order: function () { - var order = this.getOrder(); - var $rows = this.$el.find("tbody tr"); - - $rows.sort(function (a, b) { - var a_index = _.findIndex(order, function (item) { - return item._id === $(a).data('field')._id; - }); - var b_index = _.findIndex(order, function (item) { - return item._id === $(b).data('field')._id; - }); - return a_index - b_index; - }).appendTo(this.$el.find("tbody")); - } }); return { 'FieldList': FieldList, 'FieldListContextMenu': FieldListContextMenu, 'FieldListFieldContextMenu': FieldListFieldContextMenu, - 'FieldListJoinContextMenu': FieldListJoinContextMenu + 'FieldListJoinContextMenu': FieldListJoinContextMenu, }; }); diff --git a/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js b/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js index e0434329..bbb1ec8f 100644 --- a/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js +++ b/bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 Onestein () +/* Copyright 2015-2019 Onestein () * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ odoo.define('bi_view_editor.JoinNodeDialog', function (require) { @@ -11,10 +11,10 @@ odoo.define('bi_view_editor.JoinNodeDialog', function (require) { var JoinNodeDialog = Dialog.extend({ xmlDependencies: Dialog.prototype.xmlDependencies.concat([ - '/bi_view_editor/static/src/xml/bi_view_editor.xml' + '/bi_view_editor/static/src/xml/bi_view_editor.xml', ]), events: { - "click li": "choiceClicked" + "click li": "choiceClicked", }, init: function (parent, options, choices, model_data) { this.choices = choices; @@ -30,22 +30,22 @@ odoo.define('bi_view_editor.JoinNodeDialog', function (require) { title: _t("Join..."), dialogClass: 'oe_act_window', $content: qweb.render('bi_view_editor.JoinNodeDialog', { - 'choices': choices + 'choices': choices, }), buttons: [{ text: _t("Cancel"), classes: "btn-default o_form_button_cancel", - close: true - }] + close: true, + }], }); this._super(parent, defaults); }, choiceClicked: function (e) { this.trigger('chosen', { - choice: this.choices[$(e.currentTarget).attr('data-index')] + choice: this.choices[$(e.currentTarget).attr('data-index')], }); this.close(); - } + }, }); return JoinNodeDialog; diff --git a/bi_view_editor/static/src/js/bi_view_editor.ModelList.js b/bi_view_editor/static/src/js/bi_view_editor.ModelList.js index 96bca43c..588cea8d 100644 --- a/bi_view_editor/static/src/js/bi_view_editor.ModelList.js +++ b/bi_view_editor/static/src/js/bi_view_editor.ModelList.js @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 Onestein () +/* Copyright 2015-2019 Onestein () * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ odoo.define('bi_view_editor.ModelList', function (require) { @@ -6,13 +6,12 @@ odoo.define('bi_view_editor.ModelList', function (require) { var Widget = require('web.Widget'); var core = require('web.core'); - var session = require('web.session'); var qweb = core.qweb; var ModelList = Widget.extend({ template: 'bi_view_editor.ModelList', events: { - 'keyup .search-bar > input': 'filterChanged' + 'keyup .search-bar > input': 'filterChanged', }, init: function (parent) { var res = this._super(parent); @@ -43,22 +42,10 @@ odoo.define('bi_view_editor.ModelList', function (require) { this.active_models.push(id); }, loadModels: function (model_ids) { - if (model_ids) { - return this._rpc({ - model: 'ir.model', - method: 'get_related_models', - args: [model_ids], - context: { - lang: session.user_context.lang - } - }); - } return this._rpc({ model: 'ir.model', method: 'get_models', - context: { - lang: session.user_context.lang - } + args: model_ids ? [model_ids] : [], }); }, loadFields: function (model_id) { @@ -67,9 +54,6 @@ odoo.define('bi_view_editor.ModelList', function (require) { model: 'ir.model', method: 'get_fields', args: [model_id], - context: { - lang: session.user_context.lang - } }); this.cache_fields[model_id] = deferred; } @@ -83,7 +67,7 @@ odoo.define('bi_view_editor.ModelList', function (require) { var $html = $(qweb.render('bi_view_editor.ModelListItem', { 'id': model.id, 'model': model.model, - 'name': model.name + 'name': model.name, })); $html.find('.class').data('model', model).click(function () { self.modelClicked($(this)); @@ -110,7 +94,7 @@ odoo.define('bi_view_editor.ModelList', function (require) { _.each(fields, function (field) { var $field = $(qweb.render('bi_view_editor.ModelListFieldItem', { name: field.name, - description: field.description + description: field.description, })).data('field', field).click(function () { self.fieldClicked($(this)); }).draggable({ @@ -118,7 +102,7 @@ odoo.define('bi_view_editor.ModelList', function (require) { 'scroll': false, 'helper': 'clone', 'appendTo': 'body', - 'containment': 'window' + 'containment': 'window', }); $model_item.after($field); @@ -157,13 +141,13 @@ odoo.define('bi_view_editor.ModelList', function (require) { var data = $(this).data('model'); if (data.name.toLowerCase().indexOf(val) === -1 && data.model.toLowerCase().indexOf(val) === -1) { - $(this).addClass('hidden'); + $(this).addClass('d-none'); } else { - $(this).removeClass('hidden'); + $(this).removeClass('d-none'); } }); this.current_filter = val; - } + }, }); return ModelList; diff --git a/bi_view_editor/static/src/js/bi_view_editor.js b/bi_view_editor/static/src/js/bi_view_editor.js index ed3a231d..2b8abba3 100644 --- a/bi_view_editor/static/src/js/bi_view_editor.js +++ b/bi_view_editor/static/src/js/bi_view_editor.js @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 Onestein () +/* Copyright 2015-2019 Onestein () * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ odoo.define('bi_view_editor', function (require) { @@ -15,7 +15,7 @@ odoo.define('bi_view_editor', function (require) { var BiViewEditor = AbstractField.extend({ template: "bi_view_editor.Frame", events: { - "click .clear-btn": "clear" + "click .clear-btn": "clear", }, start: function () { var self = this; @@ -39,7 +39,7 @@ odoo.define('bi_view_editor', function (require) { drop: function (event, ui) { self.addField(_.extend({}, ui.draggable.data('field'))); ui.draggable.draggable('option', 'revert', false); - } + }, }); this.on("change:effective_readonly", this, function () { @@ -62,18 +62,23 @@ odoo.define('bi_view_editor', function (require) { }, fieldListRemoved: function () { console.log(this.field_list.get()); - this.loadAndPopulateModelList(); this._setValue(this.field_list.get()); + var model = new Data.DataSet(this, "bve.view"); + model.call('get_clean_list', [this.value]).then(function (result) { + this.field_list.set(JSON.parse(result)); + this._setValue(this.field_list.get()); + }.bind(this)); + this.loadAndPopulateModelList(); }, renderValue: function () { this.field_list.set(JSON.parse(this.value)); }, updateMode: function () { if (this.mode === 'readonly') { - this.$el.find('.clear-btn').addClass('hidden'); + this.$el.find('.clear-btn').addClass('d-none'); this.$el.find(".body .right").droppable("option", "disabled", true); } else { - this.$el.find('.clear-btn').removeClass('hidden'); + this.$el.find('.clear-btn').removeClass('d-none'); this.$el.find('.body .right').droppable('option', 'disabled', false); } this.field_list.setMode(this.mode); @@ -129,8 +134,7 @@ odoo.define('bi_view_editor', function (require) { this.addFieldAndJoinNode(data, e.choice); }); } else { - var table_alias = this.getTableAlias(data); - data.table_alias = table_alias; + data.table_alias = this.getTableAlias(data); this.field_list.add(data); this.loadAndPopulateModelList(); this._setValue(this.field_list.get()); @@ -139,7 +143,7 @@ odoo.define('bi_view_editor', function (require) { }, _parseValue: function (value) { return JSON.stringify(value); - } + }, }); field_registry.add('BVEEditor', BiViewEditor); diff --git a/bi_view_editor/static/src/xml/bi_view_editor.xml b/bi_view_editor/static/src/xml/bi_view_editor.xml index fb82e796..a5e6aaec 100644 --- a/bi_view_editor/static/src/xml/bi_view_editor.xml +++ b/bi_view_editor/static/src/xml/bi_view_editor.xml @@ -15,7 +15,7 @@ @@ -96,7 +96,7 @@ -