Browse Source

[12.0][MIG] bi_view_editor

myc-14.0-py3o
Andrea 6 years ago
parent
commit
cd6ba396d5
  1. 7
      bi_view_editor/__manifest__.py
  2. 4
      bi_view_editor/hooks.py
  3. 51
      bi_view_editor/migrations/10.0.1.0.2/post-migrate.py
  4. 1
      bi_view_editor/models/__init__.py
  5. 434
      bi_view_editor/models/bve_view.py
  6. 63
      bi_view_editor/models/bve_view_line.py
  7. 197
      bi_view_editor/models/ir_model.py
  8. 12
      bi_view_editor/models/models.py
  9. 1
      bi_view_editor/readme/ROADMAP.rst
  10. 1
      bi_view_editor/security/ir.model.access.csv
  11. 114
      bi_view_editor/static/src/js/bi_view_editor.FieldList.js
  12. 16
      bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js
  13. 34
      bi_view_editor/static/src/js/bi_view_editor.ModelList.js
  14. 22
      bi_view_editor/static/src/js/bi_view_editor.js
  15. 18
      bi_view_editor/static/src/xml/bi_view_editor.xml
  16. 6
      bi_view_editor/templates/assets_template.xml
  17. 256
      bi_view_editor/tests/test_bi_view.py
  18. 24
      bi_view_editor/views/bve_view.xml
  19. 6
      bi_view_editor/wizard/wizard_ir_model_menu_create.py

7
bi_view_editor/__manifest__.py

@ -1,4 +1,4 @@
# Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{ {
@ -9,11 +9,10 @@
'license': 'AGPL-3', 'license': 'AGPL-3',
'website': 'https://github.com/OCA/reporting-engine', 'website': 'https://github.com/OCA/reporting-engine',
'category': 'Reporting', 'category': 'Reporting',
'version': '11.0.1.0.0',
'version': '12.0.1.0.0',
'development_status': 'Beta',
'depends': [ 'depends': [
'base',
'web', 'web',
'base_sparse_field'
], ],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',

4
bi_view_editor/hooks.py

@ -1,4 +1,4 @@
# Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging import logging
@ -12,7 +12,7 @@ _logger = logging.getLogger(__name__)
def _bi_view(_name): def _bi_view(_name):
return _name[0:6] == 'x_bve.'
return _name.startswith('x_bve.')
def post_load(): def post_load():

51
bi_view_editor/migrations/10.0.1.0.2/post-migrate.py

@ -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])
})

1
bi_view_editor/models/__init__.py

@ -2,4 +2,5 @@
from . import models from . import models
from . import bve_view from . import bve_view
from . import bve_view_line
from . import ir_model from . import ir_model

434
bi_view_editor/models/bve_view.py

@ -1,49 +1,81 @@
# Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json 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): class BveView(models.Model):
_name = 'bve.view' _name = 'bve.view'
_description = 'BI View Editor' _description = 'BI View Editor'
@api.depends('group_ids')
@api.multi
@api.depends('group_ids', 'group_ids.users')
def _compute_users(self): 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: else:
bve_view.user_ids = self.env['res.users'].sudo().search([]) bve_view.user_ids = self.env['res.users'].sudo().search([])
@api.depends('name') @api.depends('name')
@api.multi
def _compute_model_name(self): def _compute_model_name(self):
for bve_view in self: for bve_view in self:
name = [x for x in bve_view.name.lower() if x.isalnum()] name = [x for x in bve_view.name.lower() if x.isalnum()]
model_name = ''.join(name).replace('_', '.').replace(' ', '.') model_name = ''.join(name).replace('_', '.').replace(' ', '.')
bve_view.model_name = 'x_bve.' + model_name 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) name = fields.Char(required=True, copy=False)
model_name = fields.Char(compute='_compute_model_name', store=True) model_name = fields.Char(compute='_compute_model_name', store=True)
note = fields.Text(string='Notes') 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 " help="Use the special query builder to define the query "
"to generate your report dataset. " "to generate your report dataset. "
"NOTE: To be edited, the query should be in 'Draft' status.") "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') action_id = fields.Many2one('ir.actions.act_window', string='Action')
view_id = fields.Many2one('ir.ui.view', string='View') view_id = fields.Many2one('ir.ui.view', string='View')
group_ids = fields.Many2many( group_ids = fields.Many2many(
@ -57,6 +89,7 @@ class BveView(models.Model):
string='Users', string='Users',
compute='_compute_users', compute='_compute_users',
store=True) store=True)
query = fields.Text()
_sql_constraints = [ _sql_constraints = [
('name_uniq', ('name_uniq',
@ -68,31 +101,20 @@ class BveView(models.Model):
def _create_view_arch(self): def _create_view_arch(self):
self.ensure_one() self.ensure_one()
def _get_field_def(name, def_type=''):
if not def_type:
return ''
return """<field name="x_{}" type="{}" />""".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 """<field name="{}" type="{}" />""".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 return row or column or measure
def _get_field_list(fields_info):
view_fields = [] view_fields = []
for field_info in fields_info:
field_name = field_info['name']
def_type = _get_field_type(field_info)
for line in self.line_ids:
def_type = _get_field_type(line)
if def_type: 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.append(_get_field_def(line.name, def_type))
return view_fields return view_fields
@api.multi @api.multi
@ -100,22 +122,12 @@ class BveView(models.Model):
self.ensure_one() self.ensure_one()
def _get_field_def(name): def _get_field_def(name):
return """<field name="x_{}" />""".format(
name
)
return """<field name="{}" />""".format(name)
def _get_field_list(fields_info):
view_fields = [] 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)
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 return view_fields
@api.multi @api.multi
@ -160,8 +172,7 @@ class BveView(models.Model):
""".format("".join(self._create_view_arch())) """.format("".join(self._create_view_arch()))
}] }]
for vals in view_vals:
View.sudo().create(vals)
View.sudo().create(view_vals)
# create Tree view # create Tree view
tree_view = View.sudo().create({ tree_view = View.sudo().create({
@ -199,132 +210,105 @@ class BveView(models.Model):
def _build_access_rules(self, model): def _build_access_rules(self, model):
self.ensure_one() 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({ self.env['ir.model.access'].sudo().create({
'name': 'read access to ' + self.model_name, 'name': 'read access to ' + self.model_name,
'model_id': model.id, 'model_id': model.id,
'group_id': group,
'perm_read': True, 'perm_read': True,
}) })
# read and write access
else:
# read access only to model
access_vals = []
for group in self.group_ids: for group in self.group_ids:
self.env['ir.model.access'].sudo().create({
'name': 'read-write access to ' + self.model_name,
access_vals += [{
'name': 'read access to ' + self.model_name,
'model_id': model.id, 'model_id': model.id,
'group_id': group.id, 'group_id': group.id,
'perm_read': True,
'perm_write': True,
})
'perm_read': True
}]
self.env['ir.model.access'].sudo().create(access_vals)
@api.model
@api.multi
def _create_sql_view(self): def _create_sql_view(self):
self.ensure_one()
def get_fields_info(fields_data):
def get_fields_info(lines):
fields_info = [] fields_info = []
for field_data in fields_data:
field = self.env['ir.model.fields'].browse(field_data['id'])
for line in lines:
vals = { 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) fields_info.append(vals)
return fields_info return fields_info
def get_join_nodes(info): def get_join_nodes(info):
join_nodes = [
(f['table_alias'],
return [(
f['table_alias'],
f['join'], f['join'],
f['select_field']) for f in info if f['join'] is not False]
return join_nodes
f['select_field']
) for f in info if f['join']]
def get_tables(info): def get_tables(info):
tables = set([(f['table'], f['table_alias']) for f in info])
return tables
return set([(f['table'], f['table_alias']) for f in info])
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]
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
def check_empty_data(data):
if not data or data == '[]':
if not self.line_ids:
raise UserError(_('No data to process.')) raise UserError(_('No data to process.'))
check_empty_data(self.data)
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) tables = get_tables(info)
join_nodes = get_join_nodes(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 # robustness in case something went wrong
# pylint: disable=sql-injection
self._cr.execute('DROP TABLE IF EXISTS "%s"' % table_name)
self._cr.execute('DROP TABLE IF EXISTS %s', (AsIs(view_name), ))
basic_fields = [
("t0.id", "id")
]
# pylint: disable=sql-injection
q = """CREATE or REPLACE VIEW %s as (
self.query = """
SELECT %s SELECT %s
FROM %s FROM %s
""" % (AsIs(select_str), AsIs(from_str), )
if where_str:
self.query += """
WHERE %s 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 @api.multi
def action_translations(self): def action_translations(self):
self.ensure_one() self.ensure_one()
if self.state != 'created':
return
model = self.env['ir.model'].sudo().search([ model = self.env['ir.model'].sudo().search([
('model', '=', self.model_name) ('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: 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 { return {
'name': 'Translations', 'name': 'Translations',
'res_model': 'ir.translation', 'res_model': 'ir.translation',
@ -348,21 +332,20 @@ class BveView(models.Model):
def action_create(self): def action_create(self):
self.ensure_one() self.ensure_one()
def _prepare_field(field_data):
if not field_data['custom']:
field = self.env['ir.model.fields'].browse(field_data['id'])
def _prepare_field(line):
field = line.field_id
vals = { vals = {
'name': 'x_' + field_data['name'],
'name': line.name,
'complete_name': field.complete_name, 'complete_name': field.complete_name,
'model': self.model_name, 'model': self.model_name,
'relation': field.relation, 'relation': field.relation,
'field_description': field_data.get(
'description', field.field_description),
'field_description': line.description,
'ttype': field.ttype, 'ttype': field.ttype,
'selection': field.selection, 'selection': field.selection,
'size': field.size, 'size': field.size,
'state': 'manual', 'state': 'manual',
'readonly': True
'readonly': True,
'groups': [(6, 0, field.groups.ids)],
} }
if vals['ttype'] == 'monetary': if vals['ttype'] == 'monetary':
vals.update({'ttype': 'float'}) vals.update({'ttype': 'float'})
@ -376,25 +359,24 @@ class BveView(models.Model):
vals.update({'selection': str(selection_domain)}) vals.update({'selection': str(selection_domain)})
return vals return vals
# clean dirty view (in case something went wrong)
self.action_reset()
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 # create sql view
self._create_sql_view() self._create_sql_view()
# create model and fields # 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, 'name': self.name,
'model': self.model_name, 'model': self.model_name,
'state': 'manual', '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 # give access rights
self._build_access_rules(model) self._build_access_rules(model)
@ -402,9 +384,59 @@ class BveView(models.Model):
# create tree, graph and pivot views # create tree, graph and pivot views
self._create_bve_view() 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 @api.multi
def open_view(self): def open_view(self):
self.ensure_one() self.ensure_one()
self._check_invalid_lines()
[action] = self.action_id.read() [action] = self.action_id.read()
action['display_name'] = _('BI View') action['display_name'] = _('BI View')
return action return action
@ -413,7 +445,7 @@ class BveView(models.Model):
def copy(self, default=None): def copy(self, default=None):
self.ensure_one() self.ensure_one()
default = dict(default or {}, name=_("%s (copy)") % self.name) default = dict(default or {}, name=_("%s (copy)") % self.name)
return super(BveView, self).copy(default=default)
return super().copy(default=default)
@api.multi @api.multi
def action_reset(self): def action_reset(self):
@ -422,23 +454,22 @@ class BveView(models.Model):
has_menus = False has_menus = False
if self.action_id: if self.action_id:
action = 'ir.actions.act_window,%d' % (self.action_id.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) ('action', '=', action)
]) ])
has_menus = True if menus else False has_menus = True if menus else False
menus.unlink() menus.unlink()
if self.action_id.view_id: 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( self.env['ir.ui.view'].sudo().search(
[('model', '=', self.model_name)]).unlink() [('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('.', '_') table_name = self.model_name.replace('.', '_')
tools.drop_view_if_exists(self.env.cr, table_name) tools.drop_view_if_exists(self.env.cr, table_name)
@ -450,9 +481,74 @@ class BveView(models.Model):
@api.multi @api.multi
def unlink(self): def unlink(self):
for view in self:
if view.state == 'created':
if self.filtered(lambda v: v.state == 'created'):
raise UserError( raise UserError(
_('You cannot delete a created view! ' _('You cannot delete a created view! '
'Reset the view to draft first.')) 'Reset the view to draft first.'))
return super(BveView, self).unlink()
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:
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)

63
bi_view_editor/models/bve_view_line.py

@ -0,0 +1,63 @@
# Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
# 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, )

197
bi_view_editor/models/ir_model.py

@ -1,6 +1,8 @@
# Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict
from odoo import api, models, registry from odoo import api, models, registry
NO_BI_MODELS = [ NO_BI_MODELS = [
@ -9,14 +11,6 @@ NO_BI_MODELS = [
'fetchmail.server' 'fetchmail.server'
] ]
NO_BI_FIELDS = [
'id',
'create_uid',
'create_date',
'write_uid',
'write_date'
]
NO_BI_TTYPES = [ NO_BI_TTYPES = [
'many2many', 'many2many',
'one2many', 'one2many',
@ -73,8 +67,8 @@ class IrModel(models.Model):
return 1 return 1
return 0 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 1
return 0 return 0
@ -84,7 +78,7 @@ class IrModel(models.Model):
count_check += _check_name(model_model) count_check += _check_name(model_model)
count_check += _check_startswith(model_model) count_check += _check_startswith(model_model)
count_check += _check_contains(model_model) count_check += _check_contains(model_model)
count_check += _check_unknow(model_name)
count_check += _check_unknown(model_name)
if not count_check: if not count_check:
return self.env['ir.model.access'].check( return self.env['ir.model.access'].check(
model['model'], 'read', False) model['model'], 'read', False)
@ -97,100 +91,72 @@ class IrModel(models.Model):
key=lambda x: x['name']) key=lambda x: x['name'])
return res 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())),
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), ('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),
('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, join_node=-1,
table_alias=model[0])
)
))
return model_list 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_relation_list(self, model_table_map):
if not model_table_map:
return []
model_names = {} model_names = {}
for model in models:
model_names.update({model.id: model.model})
for model in self.sudo().browse(model_table_map.keys()):
model_names.update({model.model: model.id})
model_list = get_model_list(model_ids)
relation_list = get_relation_list(model_ids, model_names)
return relation_list + model_list
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 @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 """ Return list of model dicts for all models that can be
joined with the already selected models. 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 @api.model
def get_models(self):
def get_models(self, table_model_map=None):
""" Return list of model dicts for all available models. """ 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 = [] 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)) models_list.append(dict_for_model(model))
return self.sort_filter_models(models_list) 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 Return all possible join nodes to add new_field to the query
containing model_ids. 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 = [] join_nodes = []
for alias, model_id in model_ids.items():
if model_id == new_field['model_id']:
for alias in model_table_map[new_field['model_id']]:
join_nodes.append({'table_alias': alias}) 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 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) join_nodes.append(field)
return join_nodes return join_nodes
@ -229,10 +197,11 @@ class IrModel(models.Model):
nodes_list.append(node) nodes_list.append(node)
return nodes_list 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']) keys = [(field['table_alias'], field['id'])
for field in field_data if field.get('join_node', -1) != -1] 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) join_nodes = remove_duplicate_nodes(join_nodes)
return list(filter( return list(filter(
@ -241,38 +210,34 @@ class IrModel(models.Model):
@api.model @api.model
def get_fields(self, model_id): def get_fields(self, model_id):
self = self.with_context(lang=self.env.user.lang)
domain = [ domain = [
('model_id', '=', model_id), ('model_id', '=', model_id),
('store', '=', True), ('store', '=', True),
('name', 'not in', NO_BI_FIELDS),
('name', 'not in', models.MAGIC_COLUMNS),
('ttype', 'not in', NO_BI_TTYPES) ('ttype', 'not in', NO_BI_TTYPES)
] ]
fields_dict = [] fields_dict = []
filtered_fields = self._search_fields(domain)
for field in filtered_fields:
fields_dict.append(
{'id': field.id,
for field in self.env['ir.model.fields'].sudo().search(domain):
fields_dict.append({
'id': field.id,
'model_id': model_id, 'model_id': model_id,
'name': field.name, 'name': field.name,
'description': field.field_description, 'description': field.field_description,
'type': field.ttype, 'type': field.ttype,
'custom': False,
'model': field.model_id.model,
'model_name': field.model_id.name
}
)
sorted_fields = sorted(
'model': field.model,
})
return sorted(
fields_dict, fields_dict,
key=lambda x: x['description'], key=lambda x: x['description'],
reverse=True reverse=True
) )
return sorted_fields
@api.model @api.model
def create(self, vals): def create(self, vals):
if self.env.context and self.env.context.get('bve'): if self.env.context and self.env.context.get('bve'):
vals['state'] = 'base' vals['state'] = 'base'
res = super(IrModel, self).create(vals)
res = super().create(vals)
# this sql update is necessary since a write method here would # this sql update is necessary since a write method here would
# be not working (an orm constraint is restricting the modification # be not working (an orm constraint is restricting the modification

12
bi_view_editor/models/models.py

@ -1,4 +1,4 @@
# Copyright 2017-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2017-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging import logging
@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__)
@api.model @api.model
def _bi_view(_name): def _bi_view(_name):
return _name[0:6] == 'x_bve.'
return _name.startswith('x_bve.')
_auto_init_orig = models.BaseModel._auto_init _auto_init_orig = models.BaseModel._auto_init
@ -41,23 +41,23 @@ class Base(models.AbstractModel):
@api.model @api.model
def _setup_complete(self): def _setup_complete(self):
if not _bi_view(self._name): if not _bi_view(self._name):
super(Base, self)._setup_complete()
super()._setup_complete()
else: else:
self.pool.models[self._name]._log_access = False self.pool.models[self._name]._log_access = False
@api.model @api.model
def _read_group_process_groupby(self, gb, query): def _read_group_process_groupby(self, gb, query):
if not _bi_view(self._name): 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(':') split = gb.split(':')
if split[0] not in self._fields: if split[0] not in self._fields:
raise UserError( raise UserError(
_('No data to be displayed.')) _('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 @api.model
def _add_magic_fields(self): def _add_magic_fields(self):
if _bi_view(self._name): if _bi_view(self._name):
self._log_access = False self._log_access = False
return super(Base, self)._add_magic_fields()
return super()._add_magic_fields()

1
bi_view_editor/readme/ROADMAP.rst

@ -6,5 +6,4 @@
* Find better ways to extend the *_auto_init()* without override * Find better ways to extend the *_auto_init()* without override
* Possibly avoid the monkey patches * 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 * 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) * 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)

1
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 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_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

114
bi_view_editor/static/src/js/bi_view_editor.FieldList.js

@ -1,4 +1,4 @@
/* Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
/* Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define('bi_view_editor.FieldList', function (require) { odoo.define('bi_view_editor.FieldList', function (require) {
@ -12,18 +12,18 @@ odoo.define('bi_view_editor.FieldList', function (require) {
start: function () { start: function () {
var res = this._super.apply(this, arguments); var res = this._super.apply(this, arguments);
this.$el.mouseleave(function () { this.$el.mouseleave(function () {
$(this).addClass('hidden');
$(this).addClass('d-none');
}); });
return res; return res;
}, },
open: function (x, y) { open: function (x, y) {
this.$el.css({ this.$el.css({
'left': x + 'px', 'left': x + 'px',
'top': y + 'px'
'top': y + 'px',
}); });
this.$el.removeClass('hidden');
this.$el.removeClass('d-none');
return _.extend({}, window.Backbone.Events); return _.extend({}, window.Backbone.Events);
}
},
}); });
var FieldListFieldContextMenu = FieldListContextMenu.extend({ var FieldListFieldContextMenu = FieldListContextMenu.extend({
@ -55,7 +55,7 @@ odoo.define('bi_view_editor.FieldList', function (require) {
}); });
return events; return events;
}
},
}); });
var FieldListJoinContextMenu = FieldListContextMenu.extend({ var FieldListJoinContextMenu = FieldListContextMenu.extend({
@ -72,14 +72,14 @@ odoo.define('bi_view_editor.FieldList', function (require) {
events.trigger('change', node); events.trigger('change', node);
}); });
return events; return events;
}
},
}); });
var FieldList = Widget.extend({ var FieldList = Widget.extend({
template: 'bi_view_editor.FieldList', template: 'bi_view_editor.FieldList',
events: { events: {
'click .delete-button': 'removeClicked', 'click .delete-button': 'removeClicked',
'keyup input[name="description"]': 'keyupDescription'
'keyup input[name="description"]': 'keyupDescription',
}, },
start: function () { start: function () {
var res = this._super.apply(this, arguments); var res = this._super.apply(this, arguments);
@ -94,10 +94,10 @@ odoo.define('bi_view_editor.FieldList', function (require) {
setMode: function (mode) { setMode: function (mode) {
if (mode === 'readonly') { if (mode === 'readonly') {
this.$el.find('input[type="text"]').attr('disabled', true); 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 { } else {
this.$el.find('input[type="text"]').removeAttr('disabled'); 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; this.mode = mode;
}, },
@ -122,7 +122,7 @@ odoo.define('bi_view_editor.FieldList', function (require) {
var data = $(this).data('field'); var data = $(this).data('field');
model_data[data.table_alias] = { model_data[data.table_alias] = {
model_id: data.model_id, model_id: data.model_id,
model_name: data.model_name
model_name: data.model_name,
}; };
}); });
return model_data; return model_data;
@ -149,7 +149,7 @@ odoo.define('bi_view_editor.FieldList', function (require) {
// Render table row // Render table row
var $html = $(qweb.render(field.join_node ? 'bi_view_editor.JoinListItem' : 'bi_view_editor.FieldListItem', { 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) { })).data('field', field).contextmenu(function (e) {
var $item = $(this); var $item = $(this);
if (self.mode === 'readonly') { 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('tbody').append($html);
this.$el.find(".delete-button").addClass('hidden');
this.$el.find(".delete-button:last").removeClass('hidden');
this.order();
}, },
remove: function (id) { remove: function (id) {
var $item = this.$el.find('tr[data-id="' + id + '"]'); var $item = this.$el.find('tr[data-id="' + id + '"]');
$item.remove(); $item.remove();
this.cleanJoinNodes();
this.$el.find(".delete-button").addClass('hidden');
this.$el.find(".delete-button:last").removeClass('hidden');
this.trigger('removed', id); this.trigger('removed', id);
}, },
set: function (fields) { set: function (fields) {
@ -182,8 +175,6 @@ odoo.define('bi_view_editor.FieldList', function (require) {
for (var i = 0; i < set_fields.length; i++) { for (var i = 0; i < set_fields.length; i++) {
this.add(set_fields[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) { openContextMenu: function ($item, x, y) {
var field = $item.data('field'); var field = $item.data('field');
@ -205,9 +196,9 @@ odoo.define('bi_view_editor.FieldList', function (require) {
var $attribute = $(this); var $attribute = $(this);
var value = data[$attribute.attr('data-for')]; var value = data[$attribute.attr('data-for')];
if (value) { if (value) {
$attribute.removeClass('hidden');
$attribute.removeClass('d-none');
} else { } else {
$attribute.addClass('hidden');
$attribute.addClass('d-none');
} }
}); });
}, },
@ -219,87 +210,12 @@ odoo.define('bi_view_editor.FieldList', function (require) {
keyupDescription: function () { keyupDescription: function () {
this.trigger('updated'); 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 { return {
'FieldList': FieldList, 'FieldList': FieldList,
'FieldListContextMenu': FieldListContextMenu, 'FieldListContextMenu': FieldListContextMenu,
'FieldListFieldContextMenu': FieldListFieldContextMenu, 'FieldListFieldContextMenu': FieldListFieldContextMenu,
'FieldListJoinContextMenu': FieldListJoinContextMenu
'FieldListJoinContextMenu': FieldListJoinContextMenu,
}; };
}); });

16
bi_view_editor/static/src/js/bi_view_editor.JoinNodeDialog.js

@ -1,4 +1,4 @@
/* Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
/* Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define('bi_view_editor.JoinNodeDialog', function (require) { odoo.define('bi_view_editor.JoinNodeDialog', function (require) {
@ -11,10 +11,10 @@ odoo.define('bi_view_editor.JoinNodeDialog', function (require) {
var JoinNodeDialog = Dialog.extend({ var JoinNodeDialog = Dialog.extend({
xmlDependencies: Dialog.prototype.xmlDependencies.concat([ 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: { events: {
"click li": "choiceClicked"
"click li": "choiceClicked",
}, },
init: function (parent, options, choices, model_data) { init: function (parent, options, choices, model_data) {
this.choices = choices; this.choices = choices;
@ -30,22 +30,22 @@ odoo.define('bi_view_editor.JoinNodeDialog', function (require) {
title: _t("Join..."), title: _t("Join..."),
dialogClass: 'oe_act_window', dialogClass: 'oe_act_window',
$content: qweb.render('bi_view_editor.JoinNodeDialog', { $content: qweb.render('bi_view_editor.JoinNodeDialog', {
'choices': choices
'choices': choices,
}), }),
buttons: [{ buttons: [{
text: _t("Cancel"), text: _t("Cancel"),
classes: "btn-default o_form_button_cancel", classes: "btn-default o_form_button_cancel",
close: true
}]
close: true,
}],
}); });
this._super(parent, defaults); this._super(parent, defaults);
}, },
choiceClicked: function (e) { choiceClicked: function (e) {
this.trigger('chosen', { this.trigger('chosen', {
choice: this.choices[$(e.currentTarget).attr('data-index')]
choice: this.choices[$(e.currentTarget).attr('data-index')],
}); });
this.close(); this.close();
}
},
}); });
return JoinNodeDialog; return JoinNodeDialog;

34
bi_view_editor/static/src/js/bi_view_editor.ModelList.js

@ -1,4 +1,4 @@
/* Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
/* Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define('bi_view_editor.ModelList', function (require) { 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 Widget = require('web.Widget');
var core = require('web.core'); var core = require('web.core');
var session = require('web.session');
var qweb = core.qweb; var qweb = core.qweb;
var ModelList = Widget.extend({ var ModelList = Widget.extend({
template: 'bi_view_editor.ModelList', template: 'bi_view_editor.ModelList',
events: { events: {
'keyup .search-bar > input': 'filterChanged'
'keyup .search-bar > input': 'filterChanged',
}, },
init: function (parent) { init: function (parent) {
var res = this._super(parent); var res = this._super(parent);
@ -43,22 +42,10 @@ odoo.define('bi_view_editor.ModelList', function (require) {
this.active_models.push(id); this.active_models.push(id);
}, },
loadModels: function (model_ids) { 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({ return this._rpc({
model: 'ir.model', model: 'ir.model',
method: 'get_models', method: 'get_models',
context: {
lang: session.user_context.lang
}
args: model_ids ? [model_ids] : [],
}); });
}, },
loadFields: function (model_id) { loadFields: function (model_id) {
@ -67,9 +54,6 @@ odoo.define('bi_view_editor.ModelList', function (require) {
model: 'ir.model', model: 'ir.model',
method: 'get_fields', method: 'get_fields',
args: [model_id], args: [model_id],
context: {
lang: session.user_context.lang
}
}); });
this.cache_fields[model_id] = deferred; 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', { var $html = $(qweb.render('bi_view_editor.ModelListItem', {
'id': model.id, 'id': model.id,
'model': model.model, 'model': model.model,
'name': model.name
'name': model.name,
})); }));
$html.find('.class').data('model', model).click(function () { $html.find('.class').data('model', model).click(function () {
self.modelClicked($(this)); self.modelClicked($(this));
@ -110,7 +94,7 @@ odoo.define('bi_view_editor.ModelList', function (require) {
_.each(fields, function (field) { _.each(fields, function (field) {
var $field = $(qweb.render('bi_view_editor.ModelListFieldItem', { var $field = $(qweb.render('bi_view_editor.ModelListFieldItem', {
name: field.name, name: field.name,
description: field.description
description: field.description,
})).data('field', field).click(function () { })).data('field', field).click(function () {
self.fieldClicked($(this)); self.fieldClicked($(this));
}).draggable({ }).draggable({
@ -118,7 +102,7 @@ odoo.define('bi_view_editor.ModelList', function (require) {
'scroll': false, 'scroll': false,
'helper': 'clone', 'helper': 'clone',
'appendTo': 'body', 'appendTo': 'body',
'containment': 'window'
'containment': 'window',
}); });
$model_item.after($field); $model_item.after($field);
@ -157,13 +141,13 @@ odoo.define('bi_view_editor.ModelList', function (require) {
var data = $(this).data('model'); var data = $(this).data('model');
if (data.name.toLowerCase().indexOf(val) === -1 && if (data.name.toLowerCase().indexOf(val) === -1 &&
data.model.toLowerCase().indexOf(val) === -1) { data.model.toLowerCase().indexOf(val) === -1) {
$(this).addClass('hidden');
$(this).addClass('d-none');
} else { } else {
$(this).removeClass('hidden');
$(this).removeClass('d-none');
} }
}); });
this.current_filter = val; this.current_filter = val;
}
},
}); });
return ModelList; return ModelList;

22
bi_view_editor/static/src/js/bi_view_editor.js

@ -1,4 +1,4 @@
/* Copyright 2015-2018 Onestein (<http://www.onestein.eu>)
/* Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define('bi_view_editor', function (require) { odoo.define('bi_view_editor', function (require) {
@ -15,7 +15,7 @@ odoo.define('bi_view_editor', function (require) {
var BiViewEditor = AbstractField.extend({ var BiViewEditor = AbstractField.extend({
template: "bi_view_editor.Frame", template: "bi_view_editor.Frame",
events: { events: {
"click .clear-btn": "clear"
"click .clear-btn": "clear",
}, },
start: function () { start: function () {
var self = this; var self = this;
@ -39,7 +39,7 @@ odoo.define('bi_view_editor', function (require) {
drop: function (event, ui) { drop: function (event, ui) {
self.addField(_.extend({}, ui.draggable.data('field'))); self.addField(_.extend({}, ui.draggable.data('field')));
ui.draggable.draggable('option', 'revert', false); ui.draggable.draggable('option', 'revert', false);
}
},
}); });
this.on("change:effective_readonly", this, function () { this.on("change:effective_readonly", this, function () {
@ -62,18 +62,23 @@ odoo.define('bi_view_editor', function (require) {
}, },
fieldListRemoved: function () { fieldListRemoved: function () {
console.log(this.field_list.get()); console.log(this.field_list.get());
this.loadAndPopulateModelList();
this._setValue(this.field_list.get()); 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 () { renderValue: function () {
this.field_list.set(JSON.parse(this.value)); this.field_list.set(JSON.parse(this.value));
}, },
updateMode: function () { updateMode: function () {
if (this.mode === 'readonly') { 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); this.$el.find(".body .right").droppable("option", "disabled", true);
} else { } 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.$el.find('.body .right').droppable('option', 'disabled', false);
} }
this.field_list.setMode(this.mode); this.field_list.setMode(this.mode);
@ -129,8 +134,7 @@ odoo.define('bi_view_editor', function (require) {
this.addFieldAndJoinNode(data, e.choice); this.addFieldAndJoinNode(data, e.choice);
}); });
} else { } else {
var table_alias = this.getTableAlias(data);
data.table_alias = table_alias;
data.table_alias = this.getTableAlias(data);
this.field_list.add(data); this.field_list.add(data);
this.loadAndPopulateModelList(); this.loadAndPopulateModelList();
this._setValue(this.field_list.get()); this._setValue(this.field_list.get());
@ -139,7 +143,7 @@ odoo.define('bi_view_editor', function (require) {
}, },
_parseValue: function (value) { _parseValue: function (value) {
return JSON.stringify(value); return JSON.stringify(value);
}
},
}); });
field_registry.add('BVEEditor', BiViewEditor); field_registry.add('BVEEditor', BiViewEditor);

18
bi_view_editor/static/src/xml/bi_view_editor.xml

@ -15,7 +15,7 @@
</div> </div>
<div class="footer"> <div class="footer">
<div class="left"></div> <div class="left"></div>
<div class="right"><button class="clear-btn hidden"><span class="fa fa-eraser"></span> Clear</button></div>
<div class="right"><button class="clear-btn d-none"><span class="fa fa-eraser"></span> Clear</button></div>
</div> </div>
</div> </div>
</t> </t>
@ -96,7 +96,7 @@
<!-- FieldContextMenu --> <!-- FieldContextMenu -->
<t t-name="bi_view_editor.FieldList.FieldContextMenu"> <t t-name="bi_view_editor.FieldList.FieldContextMenu">
<ul class="context-menu hidden">
<ul class="context-menu d-none">
<li> <li>
<div class="checkbox"> <div class="checkbox">
<label> <label>
@ -130,7 +130,7 @@
<!-- JoinContextMenu --> <!-- JoinContextMenu -->
<t t-name="bi_view_editor.FieldList.JoinContextMenu"> <t t-name="bi_view_editor.FieldList.JoinContextMenu">
<ul class="context-menu hidden">
<ul class="context-menu d-none">
<li> <li>
<div class="checkbox"> <div class="checkbox">
<label> <label>
@ -151,10 +151,10 @@
<t t-esc="field.model_name" /> <t t-esc="field.model_name" />
</td> </td>
<td> <td>
<span data-for="column" t-attf-class="#{field.column and 'fa fa-columns' or 'fa fa-columns hidden'}" title='Column'></span>
<span data-for="row" t-attf-class="#{field.row and 'fa fa-bars' or 'fa fa-bars hidden'}" title='Row'></span>
<span data-for="measure" t-attf-class="#{field.measure and 'fa fa-bar-chart-o' or 'fa fa-bar-chart-o hidden'}" title='Measure'></span>
<span data-for="list" t-attf-class="#{field.list and 'fa fa-list' or 'fa fa-list hidden'}" title='List'></span>
<span data-for="column" t-attf-class="#{field.column and 'fa fa-columns' or 'fa fa-columns d-none'}" title='Column'></span>
<span data-for="row" t-attf-class="#{field.row and 'fa fa-bars' or 'fa fa-bars d-none'}" title='Row'></span>
<span data-for="measure" t-attf-class="#{field.measure and 'fa fa-bar-chart-o' or 'fa fa-bar-chart-o d-none'}" title='Measure'></span>
<span data-for="list" t-attf-class="#{field.list and 'fa fa-list' or 'fa fa-list d-none'}" title='List'></span>
</td> </td>
<td> <td>
<span t-attf-data-id="#{field._id}" class="delete-button fa fa-trash-o"/> <span t-attf-data-id="#{field._id}" class="delete-button fa fa-trash-o"/>
@ -165,7 +165,7 @@
<t t-name="bi_view_editor.JoinListItem"> <t t-name="bi_view_editor.JoinListItem">
<tr t-attf-data-id="#{field._id}" class="join-node"> <tr t-attf-data-id="#{field._id}" class="join-node">
<td colspan="4"> <td colspan="4">
<input class="hidden" type="text" name="description" t-attf-value="#{field.description}"/>
<input class="d-none" type="text" name="description" t-attf-value="#{field.description}"/>
<t t-if="field.join_node > field.table_alias"> <t t-if="field.join_node > field.table_alias">
<b><t t-esc="field.model_name" /></b> <b><t t-esc="field.model_name" /></b>
<i class="fa fa-caret-right"/> <i class="fa fa-caret-right"/>
@ -176,7 +176,7 @@
<i class="fa fa-caret-left"/> <i class="fa fa-caret-left"/>
<b><t t-esc="field.model_name" /></b> <b><t t-esc="field.model_name" /></b>
</t> </t>
<span t-attf-class="#{!field.join_left and 'hidden' or ''}" data-for="join_left"><i>(join left)</i></span>
<span t-attf-class="#{!field.join_left and 'd-none' or ''}" data-for="join_left"><i>(join left)</i></span>
</td> </td>
</tr> </tr>
</t> </t>

6
bi_view_editor/templates/assets_template.xml

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<template id="assets_backend" name="bi_view_editor assets" inherit_id="web.assets_backend"> <template id="assets_backend" name="bi_view_editor assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <xpath expr="." position="inside">
@ -13,5 +12,4 @@
</xpath> </xpath>
</template> </template>
</data>
</openerp>
</odoo>

256
bi_view_editor/tests/test_bi_view.py

@ -1,10 +1,12 @@
# Copyright 2017-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2017-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json import json
from odoo.tests.common import TransactionCase, at_install, post_install
import odoo
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError from odoo.exceptions import UserError
from ..hooks import post_load, uninstall_hook
class TestBiViewEditor(TransactionCase): class TestBiViewEditor(TransactionCase):
@ -12,31 +14,28 @@ class TestBiViewEditor(TransactionCase):
def setUp(self): def setUp(self):
def _get_models(model_name_list): def _get_models(model_name_list):
Model = self.env['ir.model']
return (Model.search(
[('model', '=', name)]) for name in model_name_list)
return (self.env['ir.model'].search([
('model', '=', name)
]) for name in model_name_list)
def _get_fields(model_field_list): def _get_fields(model_field_list):
ModelFields = self.env['ir.model.fields']
return (ModelFields.search(
[('model', '=', model_field[0]),
('name', '=', model_field[1])],
limit=1) for model_field in model_field_list)
return (self.env['ir.model.fields'].search([
('model', '=', model_field[0]),
('name', '=', model_field[1])
], limit=1) for model_field in model_field_list)
def get_new_field(self): def get_new_field(self):
new_field = {
return {
'model_id': self.partner_model.id, 'model_id': self.partner_model.id,
'name': self.partner_field_name, 'name': self.partner_field_name,
'custom': False,
'id': self.partner_field.id, 'id': self.partner_field.id,
'model': self.partner_model_name, 'model': self.partner_model_name,
'type': self.partner_field.ttype, 'type': self.partner_field.ttype,
'model_name': self.partner_model.name, 'model_name': self.partner_model.name,
'description': self.partner_field.field_description 'description': self.partner_field.field_description
} }
return new_field
super(TestBiViewEditor, self).setUp()
super().setUp()
self.partner_model_name = 'res.partner' self.partner_model_name = 'res.partner'
self.partner_field_name = 'name' self.partner_field_name = 'name'
self.partner_company_field_name = 'company_id' self.partner_company_field_name = 'company_id'
@ -55,12 +54,10 @@ class TestBiViewEditor(TransactionCase):
(self.partner_model_name, self.partner_company_field_name), (self.partner_model_name, self.partner_company_field_name),
(self.company_model_name, self.company_field_name)]) (self.company_model_name, self.company_field_name)])
data = [
{'model_id': self.partner_model.id,
'name': self.partner_field_name,
self.data = [{
'model_id': self.partner_model.id,
'model_name': self.partner_model.name, 'model_name': self.partner_model.name,
'model': self.partner_model_name, 'model': self.partner_model_name,
'custom': 0,
'type': self.partner_field.ttype, 'type': self.partner_field.ttype,
'id': self.partner_field.id, 'id': self.partner_field.id,
'description': self.partner_field.field_description, 'description': self.partner_field.field_description,
@ -69,11 +66,9 @@ class TestBiViewEditor(TransactionCase):
'column': 1, 'column': 1,
'list': 1, 'list': 1,
'measure': 0 'measure': 0
},
{'model_id': self.partner_model.id,
'name': self.partner_company_field_name,
}, {
'model_id': self.partner_model.id,
'table_alias': 't0', 'table_alias': 't0',
'custom': 0,
'relation': self.company_model_name, 'relation': self.company_model_name,
'model': self.partner_model_name, 'model': self.partner_model_name,
'model_name': self.partner_model.name, 'model_name': self.partner_model.name,
@ -85,12 +80,10 @@ class TestBiViewEditor(TransactionCase):
'column': 0, 'column': 0,
'list': 1, 'list': 1,
'measure': 0 'measure': 0
},
{'model_id': self.company_model.id,
'name': 'name_1',
}, {
'model_id': self.company_model.id,
'model_name': self.company_model.name, 'model_name': self.company_model.name,
'model': self.company_model_name, 'model': self.company_model_name,
'custom': 0,
'type': self.company_field.ttype, 'type': self.company_field.ttype,
'id': self.company_field.id, 'id': self.company_field.id,
'description': self.company_field.field_description, 'description': self.company_field.field_description,
@ -99,26 +92,21 @@ class TestBiViewEditor(TransactionCase):
'column': 0, 'column': 0,
'list': 0, 'list': 0,
'measure': 0 'measure': 0
}
]
format_data = json.dumps(data)
}]
self.bi_view1_vals = { self.bi_view1_vals = {
'state': 'draft', 'state': 'draft',
'data': format_data
'data': json.dumps(self.data)
} }
self.new_field = get_new_field(self) self.new_field = get_new_field(self)
def test_01_get_fields(self): def test_01_get_fields(self):
Model = self.env['ir.model']
fields = Model.get_fields(self.partner_model.id)
fields = self.env['ir.model'].get_fields(self.partner_model.id)
self.assertIsInstance(fields, list) self.assertIsInstance(fields, list)
self.assertGreater(len(fields), 0) self.assertGreater(len(fields), 0)
def test_02_get_join_nodes(self): def test_02_get_join_nodes(self):
Fields = self.env['ir.model.fields']
field_res_users = Fields.search([
field_res_users = self.env['ir.model.fields'].search([
('name', '=', 'login'), ('name', '=', 'login'),
('model', '=', 'res.users') ('model', '=', 'res.users')
], limit=1) ], limit=1)
@ -127,7 +115,6 @@ class TestBiViewEditor(TransactionCase):
'name': 'login', 'name': 'login',
'column': False, 'column': False,
'table_alias': 't0', 'table_alias': 't0',
'custom': False,
'measure': False, 'measure': False,
'id': field_res_users.id, 'id': field_res_users.id,
'model': 'res.users', 'model': 'res.users',
@ -137,21 +124,22 @@ class TestBiViewEditor(TransactionCase):
'description': 'Login' 'description': 'Login'
}] }]
new_field = self.new_field new_field = self.new_field
Model = self.env['ir.model']
nodes = Model.get_join_nodes(field_data, new_field)
nodes = self.env['ir.model'].get_join_nodes(field_data, new_field)
self.assertIsInstance(nodes, list) self.assertIsInstance(nodes, list)
self.assertGreater(len(nodes), 0) self.assertGreater(len(nodes), 0)
def test_03_get_join_nodes(self): def test_03_get_join_nodes(self):
new_field = self.new_field new_field = self.new_field
Model = self.env['ir.model']
nodes = Model.get_join_nodes([], new_field)
nodes = self.env['ir.model'].get_join_nodes([], new_field)
self.assertIsInstance(nodes, list) self.assertIsInstance(nodes, list)
self.assertEqual(len(nodes), 0) self.assertEqual(len(nodes), 0)
def test_04_get_related_models(self): def test_04_get_related_models(self):
Model = self.env['ir.model']
related_models = Model.get_related_models({
all_models = self.env['ir.model'].get_models()
self.assertIsInstance(all_models, list)
self.assertGreater(len(all_models), 0)
related_models = self.env['ir.model'].get_models({
't0': self.partner_model.id, 't0': self.partner_model.id,
't1': self.company_model.id 't1': self.company_model.id
}) })
@ -197,13 +185,11 @@ class TestBiViewEditor(TransactionCase):
bi_view4.action_create() bi_view4.action_create()
def test_08_get_models(self): def test_08_get_models(self):
Model = self.env['ir.model']
models = Model.get_models()
models = self.env['ir.model'].get_models()
self.assertIsInstance(models, list) self.assertIsInstance(models, list)
self.assertGreater(len(models), 0) self.assertGreater(len(models), 0)
@at_install(False)
@post_install(True)
@odoo.tests.tagged('post_install', '-at_install')
def test_09_create_open_bve_object(self): def test_09_create_open_bve_object(self):
vals = self.bi_view1_vals vals = self.bi_view1_vals
employees_group = self.env.ref('base.group_user') employees_group = self.env.ref('base.group_user')
@ -213,6 +199,24 @@ class TestBiViewEditor(TransactionCase):
}) })
bi_view = self.env['bve.view'].create(vals) bi_view = self.env['bve.view'].create(vals)
self.assertEqual(len(bi_view), 1) self.assertEqual(len(bi_view), 1)
self.assertEqual(len(bi_view.line_ids), 3)
# check lines
line1 = bi_view.line_ids[0]
line2 = bi_view.line_ids[1]
line3 = bi_view.line_ids[2]
self.assertTrue(line1.in_list)
self.assertTrue(line2.in_list)
self.assertFalse(line3.in_list)
self.assertFalse(line1.row)
self.assertTrue(line1.column)
self.assertFalse(line1.measure)
self.assertFalse(line2.row)
self.assertFalse(line2.column)
self.assertFalse(line2.measure)
self.assertTrue(line3.row)
self.assertFalse(line3.column)
self.assertFalse(line3.measure)
# create bve object # create bve object
bi_view.action_create() bi_view.action_create()
@ -225,19 +229,24 @@ class TestBiViewEditor(TransactionCase):
# open view # open view
open_action = bi_view.open_view() open_action = bi_view.open_view()
self.assertEqual(isinstance(open_action, dict), True) self.assertEqual(isinstance(open_action, dict), True)
self.assertEqual(bi_view.state, 'created')
# try to remove view # try to remove view
with self.assertRaises(UserError): with self.assertRaises(UserError):
bi_view.unlink() bi_view.unlink()
@at_install(False)
@post_install(True)
# reset to draft
bi_view.action_reset()
self.assertEqual(bi_view.state, 'draft')
# remove view
bi_view.unlink()
@odoo.tests.tagged('post_install', '-at_install')
def test_10_create_open_bve_object_apostrophe(self): def test_10_create_open_bve_object_apostrophe(self):
vals = self.bi_view1_vals vals = self.bi_view1_vals
employees_group = self.env.ref('base.group_user')
vals.update({ vals.update({
'name': "Test View5", 'name': "Test View5",
'group_ids': [(6, 0, [employees_group.id])],
}) })
data_list = list() data_list = list()
for r in json.loads(vals['data']): for r in json.loads(vals['data']):
@ -249,3 +258,150 @@ class TestBiViewEditor(TransactionCase):
self.assertEqual(len(bi_view), 1) self.assertEqual(len(bi_view), 1)
# create bve object # create bve object
bi_view.action_create() bi_view.action_create()
def test_11_clean_nodes(self):
data_dict1 = {
'sequence': 1,
'model_id': 74,
'id': 858,
'name': 'name',
'model_name': 'Contact',
'model': 'res.partner',
'type': 'char',
'table_alias': 't74',
'description': 'Name',
'row': False,
'column': False,
'measure': False,
'list': True,
}
data_dict2 = {
'sequence': 2,
'model_id': 74,
'id': 896,
'name': 'company_id',
'model_name': 'Contact',
'model': 'res.partner',
'type': 'many2one',
'table_alias': 't74',
'description': 'Company',
'row': False,
'column': False,
'measure': False,
'list': True,
'join_node': 't83',
'relation': 'res.company',
'join_left': False
}
old_data = json.dumps([data_dict1, data_dict2])
new_data = self.env['bve.view'].get_clean_list(old_data)
new_data_dict = json.loads(new_data)
self.assertEqual(len(new_data_dict), 1)
for key in data_dict1.keys():
self.assertEqual(new_data_dict[0][key], data_dict1[key])
def test_12_check_groups(self):
vals = self.bi_view1_vals
group_system = self.env.ref('base.group_system')
vals.update({
'name': 'Test View1',
'group_ids': [(6, 0, [group_system.id])],
})
bi_view1 = self.env['bve.view'].create(vals)
with self.assertRaises(UserError):
bi_view1.action_create()
def test_13_check_lines_missing_model(self):
vals = self.bi_view1_vals
group_user = self.env.ref('base.group_user')
vals.update({
'name': 'Test View1',
'group_ids': [(6, 0, [group_user.id])],
})
bi_view1 = self.env['bve.view'].create(vals)
for line in bi_view1.line_ids:
self.assertTrue(line.model_id)
self.assertTrue(line.model_name)
self.env.cr.execute('UPDATE bve_view_line SET model_id = null')
bi_view1.invalidate_cache()
for line in bi_view1.line_ids:
self.assertFalse(line.model_id)
self.assertTrue(line.model_name)
with self.assertRaises(UserError):
bi_view1.action_create()
def test_14_check_lines_missing_fieldl(self):
vals = self.bi_view1_vals
group_user = self.env.ref('base.group_user')
vals.update({
'name': 'Test View1',
'group_ids': [(6, 0, [group_user.id])],
})
bi_view1 = self.env['bve.view'].create(vals)
for line in bi_view1.line_ids:
self.assertTrue(line.field_id)
self.assertTrue(line.field_name)
self.env.cr.execute('UPDATE bve_view_line SET field_id = null')
bi_view1.invalidate_cache()
for line in bi_view1.line_ids:
self.assertFalse(line.field_id)
self.assertTrue(line.field_name)
with self.assertRaises(UserError):
bi_view1.action_create()
def test_15_create_lines(self):
vals = self.bi_view1_vals
vals.update({'name': 'Test View1'})
bi_view1 = self.env['bve.view'].create(vals)
bi_view1._compute_serialized_data()
data = json.loads(bi_view1.data)
self.assertTrue(data)
self.assertTrue(isinstance(data, list))
def test_16_post_load(self):
post_load()
def test_17_uninstall_hook(self):
uninstall_hook(self.cr, self.env)
def test_18_action_translations(self):
self.env['res.lang'].load_lang('it_IT')
vals = self.bi_view1_vals
vals.update({'name': 'Test View1'})
bi_view1 = self.env['bve.view'].create(vals)
res = bi_view1.action_translations()
self.assertFalse(res)
bi_view1.action_create()
res = bi_view1.action_translations()
self.assertTrue(res)
@odoo.tests.tagged('post_install', '-at_install')
def test_19_field_selection(self):
field = self.env['ir.model.fields'].search([
('model', '=', self.company_model_name),
('name', '=', 'base_onboarding_company_state')
], limit=1)
selection_data = [{
'model_id': self.company_model.id,
'model_name': self.company_model.name,
'model': self.company_model_name,
'type': field.ttype,
'id': field.id,
'description': 'State of the onboarding company step',
'table_alias': 't1',
'row': 0,
'column': 0,
'list': 1,
'measure': 0
}]
vals = {
'state': 'draft',
'data': json.dumps(self.data + selection_data)
}
vals.update({'name': 'Test View6'})
bi_view1 = self.env['bve.view'].create(vals)
bi_view1.action_create()
self.assertEqual(len(bi_view1.line_ids), 4)

24
bi_view_editor/views/bve_view.xml

@ -48,6 +48,30 @@
<field name="data" widget="BVEEditor" nolabel="1" attrs="{'readonly': [('state','=','created')]}"/> <field name="data" widget="BVEEditor" nolabel="1" attrs="{'readonly': [('state','=','created')]}"/>
</group> </group>
</page> </page>
<page string="Lines" groups="base.group_no_one">
<group>
<field name="line_ids" nolabel="1" attrs="{'readonly': [('state','=','created')]}">
<tree decoration-info="join_model_id" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="description" string="Field"/>
<field name="model_id"/>
<field name="table_alias"/>
<field name="join_model_id"/>
<field name="join_node"/>
<field name="ttype" invisible="1"/>
<field name="row" widget="toggle_button" attrs="{'invisible': ['|', ('join_model_id','!=',False), ('ttype','in',('float', 'integer', 'monetary'))]}"/>
<field name="column" widget="toggle_button" attrs="{'invisible': ['|', ('join_model_id','!=',False), ('ttype','in',('float', 'integer', 'monetary'))]}"/>
<field name="measure" widget="toggle_button" attrs="{'invisible': ['|', ('join_model_id','!=',False), ('ttype','not in',('float', 'integer', 'monetary'))]}"/>
<field name="in_list" widget="boolean_toggle" attrs="{'invisible': [('join_model_id','!=',False)]}"/>
</tree>
</field>
</group>
</page>
<page string="SQL" groups="base.group_no_one" attrs="{'invisible': [('state','!=','created')]}">
<group>
<field name="query" nolabel="1" readonly="1"/>
</group>
</page>
<page string="Security"> <page string="Security">
<field nolabel="1" name="group_ids" /> <field nolabel="1" name="group_ids" />
</page> </page>

6
bi_view_editor/wizard/wizard_ir_model_menu_create.py

@ -1,4 +1,4 @@
# Copyright 2017-2018 Onestein (<http://www.onestein.eu>)
# Copyright 2017-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, models from odoo import api, models
@ -26,11 +26,11 @@ class WizardModelMenuCreate(models.TransientModel):
'res_id': menu.id, 'res_id': menu.id,
}) })
return {'type': 'ir.actions.client', 'tag': 'reload'} return {'type': 'ir.actions.client', 'tag': 'reload'}
return super(WizardModelMenuCreate, self).menu_create()
return super().menu_create()
@api.model @api.model
def default_get(self, fields_list): def default_get(self, fields_list):
defaults = super(WizardModelMenuCreate, self).default_get(fields_list)
defaults = super().default_get(fields_list)
if self.env.context.get('active_model') == 'bve.view': if self.env.context.get('active_model') == 'bve.view':
active_id = self.env.context.get('active_id') active_id = self.env.context.get('active_id')
bve_view = self.env['bve.view'].browse(active_id) bve_view = self.env['bve.view'].browse(active_id)

Loading…
Cancel
Save