diff --git a/bi_view_editor/README.rst b/bi_view_editor/README.rst new file mode 100644 index 00000000..70199c13 --- /dev/null +++ b/bi_view_editor/README.rst @@ -0,0 +1,61 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============== +BI View Editor +============== + +This module creates views for Odoo 8. You can build relevant queries including security. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/143/8.0 + +Known issues / Roadmap +====================== + +* Not stored fields are not supported yet + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed `feedback +`_. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Diego Luis Neto +* Kevin Graveman +* Richard Dijkstra +* Andrea Stirpe + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/bi_view_editor/__init__.py b/bi_view_editor/__init__.py new file mode 100644 index 00000000..1be685b8 --- /dev/null +++ b/bi_view_editor/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 ONESTEiN BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/bi_view_editor/__openerp__.py b/bi_view_editor/__openerp__.py new file mode 100644 index 00000000..bfa485a1 --- /dev/null +++ b/bi_view_editor/__openerp__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 ONESTEiN BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'BI View Editor', + 'summary': '''Graphical BI views builder for Odoo 8''', + 'images': ['static/description/main_screenshot.png'], + 'author': 'ONESTEiN BV,Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'http://www.onestein.eu', + 'category': 'Reporting', + 'version': '8.0.1.0.0', + 'depends': [ + 'base', + 'web', + ], + 'data': [ + 'security/ir.model.access.csv', + 'security/rules.xml', + 'templates/assets_template.xml', + 'views/bve_view.xml', + ], + 'qweb': [ + 'templates/qweb_template.xml', + ], + 'js': [ + 'static/src/js/bve.js' + ], + 'demo': [], + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/bi_view_editor/models/__init__.py b/bi_view_editor/models/__init__.py new file mode 100644 index 00000000..319d0dfb --- /dev/null +++ b/bi_view_editor/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 ONESTEiN BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import bve_view +from . import ir_model diff --git a/bi_view_editor/models/bve_view.py b/bi_view_editor/models/bve_view.py new file mode 100644 index 00000000..4f287a17 --- /dev/null +++ b/bi_view_editor/models/bve_view.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 ONESTEiN BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import json + +from openerp import tools +from openerp import SUPERUSER_ID +from openerp import models, fields, api +from openerp.exceptions import Warning +from openerp.tools.translate import _ + + +class BveView(models.Model): + _name = 'bve.view' + + @api.depends('group_ids') + @api.one + def _compute_users(self): + if self.sudo().group_ids: + self.user_ids = self.env['res.users'].sudo().browse( + list(set([u.id for group in self.sudo().group_ids + for u in group.users]))) + else: + self.user_ids = self.env['res.users'].sudo().search([]) + + name = fields.Char(size=128, string="Name", required=True) + model_name = fields.Char(size=128, string="Model Name") + + note = fields.Text(string="Notes") + + state = fields.Selection( + [('draft', 'Draft'), + ('created', 'Created')], + string="State", + default="draft") + data = fields.Text( + string="Data", + help="Use the special Onestein query builder to define the query " + "to generate your report dataset. " + "NOTE: Te be edited, the query should be in 'Draft' status.") + + action_id = fields.Many2one('ir.actions.act_window', string="Action") + view_id = fields.Many2one('ir.ui.view', string="View") + + group_ids = fields.Many2many( + 'res.groups', + string="Groups", + help="User groups allowed to see the generated report; " + "if NO groups are specified the report will be public " + "for everyone.") + + user_ids = fields.Many2many( + 'res.users', + string="Users", + compute=_compute_users, + store=True) + + _sql_constraints = [ + ('name_uniq', + 'unique(name)', + 'Custom BI View names must be unique!'), + ] + + @api.multi + def unlink(self): + for view in self: + if view.state == 'created': + raise Warning( + _('Error'), + _('You cannot delete a created view! ' + 'Reset the view to draft first.')) + + super(BveView, self).unlink() + + @api.multi + def action_edit_query(self): + return { + 'type': 'ir.actions.client', + 'tag': 'bi_view_editor.open', + 'target': 'new', + 'params': {'bve_view_id': self.id} + } + + @api.multi + def action_reset(self): + if self.action_id: + if self.action_id.view_id: + self.action_id.view_id.sudo().unlink() + self.action_id.sudo().unlink() + + self.env['ir.model'].sudo().search( + [('model', '=', self.model_name)]).unlink() + + table_name = self.model_name.replace(".", "_") + tools.drop_view_if_exists(self.env.cr, table_name) + + self.write({ + 'state': 'draft' + }) + return True + + def _create_graph_view(self): + fields_info = json.loads(self.data) + return ["""""".format( + field_info['name'], + (field_info['row'] and 'row') or + (field_info['column'] and 'col') or + (field_info['measure'] and 'measure')) + for field_info in fields_info if field_info['row'] or + field_info['column'] or field_info['measure']] + + @api.multi + def action_create(self): + + def _get_fields_info(fields_data): + fields_info = [] + for field_data in fields_data: + field = self.env['ir.model.fields'].browse(field_data["id"]) + vals = { + "table": self.env[field.model_id.model]._table, + "table_alias": field_data["table_alias"], + "select_field": field.name, + "as_field": "x_" + field_data["name"], + "join": False, + "model": field.model_id.model + } + if field_data.get("join_node"): + vals.update({"join": field_data["join_node"]}) + fields_info.append(vals) + return fields_info + + def _build_query(): + + info = _get_fields_info(json.loads(self.data)) + fields = [("{}.{}".format(f["table_alias"], + f["select_field"]), + f["as_field"]) for f in info if 'join_node' not in f] + tables = set([(f["table"], f["table_alias"]) for f in info]) + join_nodes = [ + (f["table_alias"], + f["join"], + f["select_field"]) for f in info if f["join"] is not False] + + table_name = self.model_name.replace(".", "_") + tools.drop_view_if_exists(self.env.cr, table_name) + + basic_fields = [ + ("t0.id", "id"), + ("t0.write_uid", "write_uid"), + ("t0.write_date", "write_date"), + ("t0.create_uid", "create_uid"), + ("t0.create_date", "create_date") + ] + + q = """CREATE or REPLACE VIEW %s as ( + SELECT %s + FROM %s + WHERE %s + )""" % (table_name, ','.join( + ["{} AS {}".format(f[0], f[1]) + for f in basic_fields + 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"])) + + self.env.cr.execute(q) + + def _prepare_field(field_data): + if not field_data["custom"]: + field = self.env['ir.model.fields'].browse(field_data["id"]) + 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" + } + if field.ttype == 'selection' and not field.selection: + model_obj = self.env[field.model_id.model] + selection = model_obj._columns[field.name].selection + selection_domain = str(selection) + vals.update({"selection": selection_domain}) + return vals + + def _prepare_object(): + return { + 'name': self.name, + 'model': self.model_name, + 'field_id': [ + (0, 0, _prepare_field(field)) + for field in json.loads(self.data) + if 'join_node' not in field] + } + + def _build_object(): + res_id = self.env['ir.model'].sudo().create(_prepare_object()) + return res_id + + # read access + def group_ids_with_access(model_name, access_mode): + 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) + LEFT JOIN ir_module_category c ON (c.id=g.category_id) + WHERE + m.model=%s AND + a.active IS True AND + a.perm_''' + access_mode, (model_name,)) + return [x[0] for x in self.env.cr.fetchall()] + + def _build_access_rules(obj): + info = json.loads(self.data) + models = list(set([f["model"] for f in info])) + read_groups = set.intersection(*[set( + group_ids_with_access(model, 'read')) for model in models]) + + for group in read_groups: + self.env['ir.model.access'].sudo().create({ + 'name': 'read access to ' + self.model_name, + 'model_id': obj.id, + 'group_id': group, + 'perm_read': True, + }) + + # edit access + for group in self.group_ids: + self.env['ir.model.access'].sudo().create({ + 'name': 'read access to ' + self.model_name, + 'model_id': obj.id, + 'group_id': group.id, + 'perm_read': True, + 'perm_write': True, + }) + + return + + self.model_name = "x_bve." + ''.join( + [x for x in self.name.lower() + if x.isalnum()]).replace("_", ".").replace(" ", ".") + + _build_query() + obj = _build_object() + _build_access_rules(obj) + self.env.cr.commit() + + from openerp.modules.registry import RegistryManager + self.env.registry = RegistryManager.new(self.env.cr.dbname) + self.pool = self.env.registry + + view_id = self.pool.get('ir.ui.view').create( + self.env.cr, SUPERUSER_ID, + {'name': "Analysis", + 'type': 'graph', + 'model': self.model_name, + 'priority': 16, + 'arch': """ + {} + """.format("".join(self._create_graph_view())) + }, context={}) + view_ids = [view_id] + + action_vals = {'name': self.name, + 'res_model': self.model_name, + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'graph', + 'view_id': view_ids and view_ids[0] or 0, + 'context': "{'service_name': '%s'}" % self.name, + } + act_window = self.env['ir.actions.act_window'] + action_id = act_window.sudo().create(action_vals) + + self.write({ + 'action_id': action_id.id, + 'view_id': view_id, + 'state': 'created' + }) + + return True + + @api.multi + def open_view(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': self.model_name, + 'view_type': 'graph', + 'view_mode': 'graph', + } diff --git a/bi_view_editor/models/ir_model.py b/bi_view_editor/models/ir_model.py new file mode 100644 index 00000000..7433918c --- /dev/null +++ b/bi_view_editor/models/ir_model.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 ONESTEiN BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, api +from openerp.modules.registry import RegistryManager + +NO_BI_MODELS = [ + "temp.range", + "account.statement.operation.template", + "fetchmail.server" +] + +NO_BI_FIELDS = [ + "id", + "create_uid", + "create_date", + "write_uid", + "write_date" +] + + +def dict_for_field(field): + return { + "id": field.id, + "name": field.name, + "description": field.field_description, + "type": field.ttype, + "relation": field.relation, + "custom": False, + + "model_id": field.model_id.id, + "model": field.model_id.model, + "model_name": field.model_id.name + } + + +def dict_for_model(model): + return { + "id": model.id, + "name": model.name, + "model": model.model + } + + +class IrModel(models.Model): + _inherit = 'ir.model' + + def _filter_bi_fields(self, ir_model_field_obj): + name = ir_model_field_obj.name + model = ir_model_field_obj.model_id + model_name = model.model + if name in self.env[model_name]._columns: + f = self.env[model_name]._columns[name] + return f._classic_write + return False + + @api.model + def _filter_bi_models(self, model): + model_name = model["model"] + if model_name in NO_BI_MODELS or \ + model_name.startswith("workflow") or \ + model_name.startswith("ir.") or \ + model["name"] == "Unknow" or \ + "report" in model_name or \ + model_name.startswith("base_") or \ + "_" in model_name or \ + "." in model["name"] or \ + "mail" in model_name or \ + "edi." in model_name: + return False + return self.env['ir.model.access'].check( + model["model"], 'read', False) + + @api.model + def get_related_fields(self, model_ids): + """ Return list of field dicts for all fields that can be + joined with models in model_ids + """ + model_names = dict([(model.id, model.model) + for model in self.env['ir.model'].sudo().search( + [('id', 'in', model_ids.values())])]) + filter_bi_fields = self._filter_bi_fields + if filter_bi_fields: + rfields = [ + dict(dict_for_field(field), + join_node=-1, + table_alias=model[0]) + for field in filter( + filter_bi_fields, + self.env['ir.model.fields'].sudo().search( + [('model_id', 'in', model_ids.values()), + ('ttype', 'in', ['many2one'])])) + for model in model_ids.items() + if model[1] == field.model_id.id + ] + + lfields = [ + dict(dict_for_field(field), + join_node=model[0], + table_alias=-1) + for field in filter( + filter_bi_fields, + self.env['ir.model.fields'].sudo().search( + [('relation', 'in', model_names.values()), + ('ttype', 'in', ['many2one'])])) + for model in model_ids.items() + if model_names[model[1]] == field['relation'] + ] + + return [dict(field, join_node=model[0]) + for field in lfields + if model_names[model[1]] == field['relation']] + [ + dict(field, table_alias=model[0]) + for model in model_ids.items() + for field in rfields if model[1] == field['model_id']] + + @api.model + def get_related_models(self, model_ids): + """ Return list of model dicts for all models that can be + joined with models in model_ids + """ + related_fields = self.get_related_fields(model_ids) + return sorted(filter( + self._filter_bi_models, + [{"id": model.id, "name": model.name, "model": model.model} + for model in self.env['ir.model'].sudo().search( + ['|', + ('id', 'in', model_ids.values() + [ + f['model_id'] + for f in related_fields if f['table_alias'] == -1]), + ('model', 'in', [ + f['relation'] + for f in related_fields if f['join_node'] == -1])])]), + key=lambda x: x['name']) + + @api.model + def get_models(self): + """ Return list of model dicts for all available models. + """ + models_domain = [('osv_memory', '=', False)] + return sorted(filter( + self._filter_bi_models, + [dict_for_model(model) + for model in self.search(models_domain)]), + key=lambda x: x['name']) + + @api.model + def get_join_nodes(self, field_data, new_field): + """ Return list of field dicts of join nodes + + Return all possible join nodes to add new_field to the query + containing model_ids. + """ + model_ids = dict([(field['table_alias'], + field['model_id']) for field in field_data]) + keys = [(field['table_alias'], field['id']) + for field in field_data if field.get('join_node', -1) != -1] + join_nodes = ([{'table_alias': alias} + for alias, model_id in model_ids.items() + if model_id == new_field['model_id']] + [ + d for d in self.get_related_fields(model_ids) + if (d['relation'] == new_field['model'] and + d['join_node'] == -1) or + (d['model_id'] == new_field['model_id'] and + d['table_alias'] == -1)]) + return filter( + lambda x: 'id' not in x or + (x['table_alias'], x['id']) not in keys, join_nodes) + + @api.model + def get_fields(self, model_id): + bi_field_domain = [ + ('model_id', '=', model_id), + ("name", "not in", NO_BI_FIELDS), + ('ttype', 'not in', [ + 'many2many', "one2many", "html", "binary", "reference"]) + ] + filter_bi_fields = self._filter_bi_fields + fields_obj = self.env['ir.model.fields'] + fields = filter(filter_bi_fields, + fields_obj.sudo().search(bi_field_domain)) + return sorted([{"id": field.id, + "model_id": model_id, + "name": field.name, + "description": field.field_description, + "type": field.ttype, + "custom": False, + "model": field.model_id.model, + "model_name": field.model_id.name} + for field in fields], key=lambda x: x['description'], + reverse=True) + + def create(self, cr, user, vals, context=None): + if context is None: + context = {} + if context and context.get('bve'): + vals['state'] = 'base' + res = super(IrModel, self).create(cr, user, vals, context) + if vals.get('state', 'base') == 'bve': + vals['state'] = 'manual' + + # add model in registry + self.instanciate(cr, user, vals['model'], context) + self.pool.setup_models(cr, partial=(not self.pool.ready)) + + # update database schema + # model = self.pool[vals['model']] + # ctx = dict( + # context, + # field_name=vals['name'], + # field_state='manual', + # select=vals.get('select_level', '0'), + # update_custom_fields=True) + RegistryManager.signal_registry_change(cr.dbname) + + self.write(cr, user, [res], {'state': 'manual'}) + + return res diff --git a/bi_view_editor/security/ir.model.access.csv b/bi_view_editor/security/ir.model.access.csv new file mode 100644 index 00000000..2547fb5a --- /dev/null +++ b/bi_view_editor/security/ir.model.access.csv @@ -0,0 +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_technical_settings,bve.view,bi_view_editor.model_bve_view,base.group_no_one,1,1,1,1 diff --git a/bi_view_editor/security/rules.xml b/bi_view_editor/security/rules.xml new file mode 100644 index 00000000..d68db5db --- /dev/null +++ b/bi_view_editor/security/rules.xml @@ -0,0 +1,13 @@ + + + + + + bve_view read access + + + ['|',('user_ids','=',False),('user_ids','in',user.id)] + + + + diff --git a/bi_view_editor/static/description/icon.png b/bi_view_editor/static/description/icon.png new file mode 100644 index 00000000..bd6f94bc Binary files /dev/null and b/bi_view_editor/static/description/icon.png differ diff --git a/bi_view_editor/static/description/index.html b/bi_view_editor/static/description/index.html new file mode 100644 index 00000000..57f1c8ac --- /dev/null +++ b/bi_view_editor/static/description/index.html @@ -0,0 +1,60 @@ +
+
+

BI reporting concept

+

easy and effective.

+
+

+ This module is a beta release of a smart and effective Odoo reporting tool. +

+

+ Feel free to use and fix issues that you encounter. + Please let us know when you have difficuties doing so.. +

+
+ +
+
+ BI view editor +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bi_view_editor/static/description/main_screenshot.png b/bi_view_editor/static/description/main_screenshot.png new file mode 100644 index 00000000..66789fac Binary files /dev/null and b/bi_view_editor/static/description/main_screenshot.png differ diff --git a/bi_view_editor/static/description/screen1.png b/bi_view_editor/static/description/screen1.png new file mode 100644 index 00000000..bcdf31a1 Binary files /dev/null and b/bi_view_editor/static/description/screen1.png differ diff --git a/bi_view_editor/static/src/css/bve.css b/bi_view_editor/static/src/css/bve.css new file mode 100644 index 00000000..e4bb40be --- /dev/null +++ b/bi_view_editor/static/src/css/bve.css @@ -0,0 +1,175 @@ +.oe_form_field_bi_editor { + /*box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);*/ + border: 1px solid #DDDDDD; +} + +.oe_form_field_bi_editor .header, .oe_form_field_bi_editor .footer { + width: 100%; + height: 50px; + background-color: #7c7bad; + color: #fff; +} + +.oe_form_field_bi_editor .footer { + background-color: #FFF; + border-top: 1px solid #DDDDDD; +} + +.oe_form_field_bi_editor .header .left, .oe_form_field_bi_editor .footer .left { + width: 75%; + float: left; + line-height: 50px; + padding-left: 10px; + padding-top: 13px; +} + +.oe_form_field_bi_editor .header .right, .oe_form_field_bi_editor .footer .right { + width: 25%; + float: right; + padding-top: 13px; + padding-right: 15px; + text-align: right; +} + +.oe_form_field_bi_editor .body { + padding-bottom: 0px; +} + +.oe_form_field_bi_editor .body .left { + float: left; + width: 30%; + box-sizing: border-box; + border-right: 1px solid #DDDDDD; +} + +.oe_form_field_bi_editor .body .left .search-bar { + height: 23px; + width: 100%; + position: relative; +} + +.oe_form_field_bi_editor .body .left .search-bar input { + width: 100%; + border-radius: 0px; + border-left: 0px; + border-right: 0px; + border-top: 0px; + padding-left: 18px; + padding-top: 4px; + position: absolute; + left: 0px; + top: 0px; + z-index: 1; +} + +.oe_form_field_bi_editor .body .left .search-bar span { + position: absolute; + left: 3px; + top: 5px; + z-index: 2; +} + +.oe_form_field_bi_editor .body .left .class-list { + height: 400px; /* FIXME */ + overflow-y: scroll; + overflow-x: hidden; +} + +.oe_form_field_bi_editor .body .left .class-list .class { + font-weight: bold; + padding-bottom: 5px; + padding-top: 5px; + padding-left: 10px; + cursor: pointer; +} + +.oe_form_field_bi_editor .body .left .class-list .class:hover { + background-color: #7C7BAD; + color: #FFF; +} + +.oe_form_field_bi_editor .body .left .class-list .field { + font-weight: normal; + padding-left: 20px; + padding-top: 3px; + padding-bottom: 3px; + cursor: pointer; +} + +.oe_form_field_bi_editor .body .right { + width: 70%; + float: left; + box-sizing: border-box; + height: 423px; /* FIXME */ + overflow-y: scroll; + overflow-x: hidden; +} + +.oe_form_field_bi_editor .body .right .field-list { + width: 100%; +} + +.oe_form_field_bi_editor .body .right .field-list th, +.oe_form_field_bi_editor .body .right .field-list td { + padding-left: 10px; + padding-top: 6px; + padding-bottom: 6px; + vertical-align: middle; +} + +.oe_form_field_bi_editor .body .right .field-list tbody tr button.delete-button, +.oe_form_field_bi_editor .body .right .field-list tbody tr button.delete-button:hover { + background-color: transparent; + border: none; + background-image: none; + padding: 0; +} + +.oe_form_field_bi_editor .body .right .field-list tbody tr:hover { + background-color: #DDD; +} + +.oe_form_field_bi_editor .context-menu, .oe_form_field_bi_editor .context-menu ul { + display: none; + z-index: 1000; + position: fixed; + background-color: #fff; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + border: 1px solid #DDDDDD; + list-style-type: none; + padding: 0px; + width: 175px; + +} + +.oe_form_field_bi_editor .context-menu li { + padding: 5px; + height: 28px; + cursor: pointer; +} + +.oe_form_field_bi_editor .context-menu li:hover { + background-color: #7C7BAD; + color: #FFF; +} + +.oe_form_field_bi_editor .context-menu ul { + display: none; + margin-left: 165px; + margin-top: -24px; +} + +.oe_form_field_bi_editor .context-menu ul:hover { + display: none; + margin-left: 165px; + margin-top: -24px; +} + + +.clear { + clear: both; +} + +.displaynone { + display: none; +} diff --git a/bi_view_editor/static/src/js/bve.js b/bi_view_editor/static/src/js/bve.js new file mode 100644 index 00000000..0e48ffbf --- /dev/null +++ b/bi_view_editor/static/src/js/bve.js @@ -0,0 +1,447 @@ + +openerp.bi_view_editor = function (instance, local) { + + instance.bi_view_editor.BVEEditor = instance.web.form.AbstractField.extend({ + template: "BVEEditor", + activeModelMenus: [], + currentFilter: "", + init: function(parent, action) { + this._super.apply(this, arguments); + }, + start: function() { + var self = this; + this._super(); + this.on("change:effective_readonly", this, function() { + this.display_field(); + this.render_value(); + }); + this.display_field(); + this.render_value(); + }, + display_field: function () { + var self = this; + this.$el.find(".body .right").droppable({ + accept: "div.class-list div.field", + drop: function (event, ui) { + self.add_field(ui.draggable); + ui.draggable.draggable('option', 'revert', false ); + ui.draggable.remove(); + } + }); + if (!this.get("effective_readonly")) { + this.$el.find('.search-bar').attr('disabled', false); + this.$el.find('.class-list').css('opacity', '1'); + this.$el.find('.class-list .class').css('cursor', 'pointer'); + this.$el.find(".body .right").droppable("option", "disabled", false); + this.$el.find('#clear').css('display', 'inline-block').click(function () { + self.set_fields([]); + }); + this.$el.find('.search-bar input').keyup(function(e) { + //Local filter + self.filter($(this).val()); + }); + } else { + this.$el.find(".body .right").droppable("option", "disabled", true); + this.$el.find('#clear').css('display', 'none'); + this.$el.find('.search-bar').attr('disabled', true); + this.$el.find('.class-list').css('opacity', '.35'); + this.$el.find('.class-list .class').css('cursor', 'default'); + } + }, + filter: function(val) { + val = (typeof val != 'undefined') ? val.toLowerCase() : this.currentFilter; + this.currentFilter = val; + this.$el.find(".class-list .class-container").each(function() { + var modelData = $(this).find(".class").data('model-data'); + //TODO: filter on all model fields (name, technical name, etc) + + if(typeof modelData == 'undefined' || (modelData.name.toLowerCase().indexOf(val) == -1 && modelData.model.toLowerCase().indexOf(val) == -1)) + $(this).hide(); + else + $(this).show(); + }); + }, + get_field_icons: function(field) { + icons = ""; + if(field.column) + icons += " "; + if(field.row) + icons += " "; + if(field.measure) + icons += " "; + + return icons; + }, + update_field_view: function(row) { + row.find("td:nth-child(3)").html(this.get_field_icons(row.data('field-data'))); + }, + render_value: function() { + this.set_fields(JSON.parse(this.get('value'))); + }, + load_classes: function(scrollTo) { + scrollTo = (typeof scrollTo == 'undefined') ? false : scrollTo; + var self = this; + var model = new instance.web.Model("ir.model"); + if (this.$el.find(".field-list tbody tr").length > 0) { + model.call("get_related_models", [this.get_model_ids()], { context: new instance.web.CompoundContext() }).then(function(result) { + self.show_classes(result); + //if(scrollTo) self.$el.find('.class-list').scrollTo('#bve-class-' + scrollTo.model_id); + }); + } else { + model.call("get_models", { context: new instance.web.CompoundContext() }).then(function(result) { + self.show_classes(result); + //if(scrollTo) self.$el.find('.class-list').scrollTo('#bve-class-' + scrollTo.model_id); + }); + } + }, + show_classes: function (result) { + var self = this; + var model = new instance.web.Model("ir.model"); + self.$el.find(".class-list .class").remove(); + self.$el.find(".class-list .field").remove(); + var css = this.get('effective_readonly') ? 'cursor: default' : 'cursor: pointer' + + for (var i = 0; i < result.length; i++) { + var item = $("
" + result[i]["name"] + "
") + .data('model-data', result[i]) + .click(function (evt) { + if(self.get("effective_readonly")) return; + var classel = $(this); + + if (classel.data('bve-processed')) { + classel.parent().find('.field').remove(); + classel.data('bve-processed', false); + var index = self.activeModelMenus.indexOf(classel.data('model-data').id); + if(index != -1) self.activeModelMenus.splice(index, 1); + } else { + self.activeModelMenus.push(classel.data('model-data').id); + model.call("get_fields", [classel.data('model-data').id], { context: new instance.web.CompoundContext() }).then(function(result) { + for (var i = 0; i < result.length; i++) { + classel.find("#bve-field-" + result[i]["name"]).remove(); + if(self.$el.find(".field-list tbody [name=label-" + result[i].id + "]").length > 0) continue; + classel.after($("
" + result[i]["description"] + "
") + .data('field-data', result[i]) + .click(function () { + if (!self.get("effective_readonly")) { + self.add_field($(this)); + } + }) + .draggable({ + 'revert': 'invalid', + 'scroll': false, + 'helper': 'clone', + 'appendTo': 'body', + 'containment': 'window' + }) + ); + } + }); + + $(this).data('bve-processed', true); + } + }) + .wrap("
").parent(); + self.$el.find(".class-list").append(item); + + var index = self.activeModelMenus.indexOf(item.find(".class").data('model-data').id); + if(index != -1 && !self.get("effective_readonly")) { + model.call("get_fields", [self.activeModelMenus[index]], { context: new instance.web.CompoundContext() }).then(function(result) { + var item = self.$el.find(".class-list #bve-class-" + result[0].model_id); + for (var o = 0; o < result.length; o++) { + if(self.$el.find(".field-list tbody [name=label-" + result[o].id + "]").length > 0) continue; + item.after($("
" + result[o]["description"] + "
") + .data('field-data', result[o]) + .click(function () { + if (!self.get("effective_readonly")) { + self.add_field($(this)); + } + }) + .draggable({ + 'revert': 'invalid', + 'scroll': false, + 'helper': 'clone', + 'appendTo': 'body', + 'containment': 'window' + })); + } + item.data('bve-processed', true); + }); + } + self.filter(); + } + + }, + add_field_to_table: function(data, options) { + var self = this; + if (typeof data.row == 'undefined') { + data.row = false; + } + if (typeof data.column == 'undefined') { + data.column = false; + } + if (typeof data.measure == 'undefined') { + data.measure = false; + } + + var n = 1; + var name = data.name; + while ($.grep(self.get_fields(), function (el) { return el.name == data.name;}).length > 0) { + data.name = name + '_' + n; + n += 1; + } + var classes = ""; + if (typeof data.join_node != 'undefined') { + classes = "join-node displaynone"; + } + var delete_button = ""; + var disabled = " disabled=\"disabled\" "; + if (!this.get("effective_readonly")) { + delete_button = ""; + disabled = ""; + } + + self.$el.find(".field-list tbody") + .append($("" + data.model_name + "" + self.get_field_icons(data) + "" + delete_button + "") + .data('field-data', data) + .contextmenu(function(e) { + e.preventDefault(); + if (self.get("effective_readonly")) return; + var target = $(e.currentTarget); + var currentFieldData = target.data('field-data'); + + var contextMenu = self.$el.find(".context-menu"); + contextMenu.css("left", e.pageX + "px"); + contextMenu.css("top", e.pageY + "px"); + contextMenu.mouseleave(function() { + contextMenu.hide(); + }); + contextMenu.find("li").hover(function() { + $(this).find("ul").css("color", "#000"); + $(this).find("ul").show(); + }, function() { + $(this).find("ul").hide(); + }); + + //Set checkboxes + if(currentFieldData.column) + contextMenu.find('#column-checkbox').attr('checked', true); + else + contextMenu.find('#column-checkbox').attr('checked', false); + + if(currentFieldData.row) + contextMenu.find('#row-checkbox').attr('checked', true); + else + contextMenu.find('#row-checkbox').attr('checked', false); + + if(currentFieldData.measure) + contextMenu.find('#measure-checkbox').attr('checked', true); + else + contextMenu.find('#measure-checkbox').attr('checked', false); + + if(currentFieldData.type == "float" || currentFieldData.type == "integer") { + contextMenu.find('#column-checkbox').attr('disabled', true); + contextMenu.find('#row-checkbox').attr('disabled', true); + contextMenu.find('#measure-checkbox').attr('disabled', false); + } + else { + contextMenu.find('#column-checkbox').attr('disabled', false); + contextMenu.find('#row-checkbox').attr('disabled', false); + contextMenu.find('#measure-checkbox').attr('disabled', true); + } + + //Add change events + contextMenu.find('#column-checkbox').unbind("change"); + contextMenu.find('#column-checkbox').change(function() { + currentFieldData.column = $(this).is(":checked"); + target.data('field-data', currentFieldData); + self.update_field_view(target); + self.internal_set_value(JSON.stringify(self.get_fields())); + }); + contextMenu.find('#row-checkbox').unbind("change"); + contextMenu.find('#row-checkbox').change(function() { + currentFieldData.row = $(this).is(":checked"); + target.data('field-data', currentFieldData); + self.update_field_view(target); + self.internal_set_value(JSON.stringify(self.get_fields())); + }); + contextMenu.find('#measure-checkbox').unbind("change"); + contextMenu.find('#measure-checkbox').change(function() { + currentFieldData.measure = $(this).is(":checked"); + target.data('field-data', currentFieldData); + self.update_field_view(target); + self.internal_set_value(JSON.stringify(self.get_fields())); + }); + contextMenu.show(); + + $(document).mouseup(function (e) { + var container = $(".context-menu"); + + if (!container.is(e.target) // if the target of the click isn't the container... + && container.has(e.target).length === 0) // ... nor a descendant of the container + { + container.hide(); + } + }); + + }) + ); + + self.$el.find('.delete-button').unbind("click"); + self.$el.find('.delete-button').click(function() { + $(this).closest('tr').remove(); + self.clean_join_nodes(); + self.internal_set_value(JSON.stringify(self.get_fields())); + self.load_classes(); + return false; + }) + }, + clean_join_nodes: function () { + var aliases = $.makeArray(this.$el.find(".field-list tbody tr").map(function (idx, el) { + var d = $(this).data('field-data'); + return d.table_alias; + })); + + this.$el.find(".field-list tbody tr").each(function (idx, el) { + var d = $(this).data('field-data'); + if (typeof d.join_node != 'undefined' && aliases.indexOf(d.join_node) === -1) { + $(this).remove(); + } + }); + }, + get_model_ids: function () { + var model_ids = {}; + this.$el.find(".field-list tbody tr").each(function (idx, el) { + var d = $(this).data('field-data'); + model_ids[d.table_alias] = d.model_id; + }); + return model_ids; + }, + get_model_data: function () { + var model_data = {}; + this.$el.find(".field-list tbody tr").each(function (idx, el) { + var d = $(this).data('field-data'); + model_data[d.table_alias] = {model_id: d.model_id, model_name: d.model_name}; + }); + return model_data; + }, + get_table_alias: function(field) { + if (typeof field.table_alias != 'undefined') { + return field.table_alias; + } else { + var model_ids = this.get_model_ids(); + var n = 0; + while (typeof model_ids["t" + n] != 'undefined') n++; + return "t" + n; + } + }, + add_field_and_join_node: function(field, join_node) { + var self = this; + if (join_node.join_node == -1) { + field.table_alias = self.get_table_alias(field); + join_node.join_node = field.table_alias; + self.add_field_to_table(join_node); + } else if (join_node.table_alias == -1) { + field.table_alias = self.get_table_alias(field); + join_node.table_alias = field.table_alias; + self.add_field_to_table(join_node); + } else { + field.table_alias = join_node.table_alias; + } + self.add_field_to_table(field); + self.internal_set_value(JSON.stringify(self.get_fields())); + self.load_classes(field); + }, + add_field: function(field) { + var data = field.data('field-data'); + var model = new instance.web.Model("ir.model"); + var model_ids = this.get_model_ids(); + var field_data = this.get_fields(); + var self = this; + model.call('get_join_nodes', [field_data, data], {context: new instance.web.CompoundContext()}).then(function(result) { + //self.$el.find(".search-bar").val(""); + //self.filter(""); + if (result.length == 1) { + self.add_field_and_join_node(data, result[0]); + self.internal_set_value(JSON.stringify(self.get_fields())); + //self.load_classes(data); + } else if (result.length > 1) { + var pop = new instance.bi_view_editor.JoinNodePopup(self); + pop.display_popup(result, self.get_model_data(), self.add_field_and_join_node.bind(self), data); + } else { + // first field and table only. + table_alias = self.get_table_alias(data); + data.table_alias = table_alias; + self.add_field_to_table(data); + self.internal_set_value(JSON.stringify(self.get_fields())); + self.load_classes(data); + } + }); + }, + get_fields: function() { + return $.makeArray(this.$el.find(".field-list tbody tr").map(function (idx, el) { + var d = $(this).data('field-data'); + d.description = $("input[name='label-" + d.id + "']").val(); + return d; + })); + }, + set_fields: function(values) { + this.activeModelMenus = []; + if (!values) { + values = []; + } + this.$el.find('.field-list tbody tr').remove(); + for(var i = 0; i < values.length; i++) { + this.add_field_to_table(values[i]); + } + this.load_classes(); + } + }); + instance.web.form.widgets.add('BVEEditor', 'instance.bi_view_editor.BVEEditor'); + + + local.JoinNodePopup = instance.web.Widget.extend({ + template: "JoinNodePopup", + start: function() { + var self = this; + }, + + display_popup: function(choices, model_data, callback, callback_data) { + var self = this; + this.renderElement(); + var joinnodes = this.$el.find('#join-nodes'); + joinnodes.empty(); + for (var i=0; i" + description+ "") + .data('idx', i) + .wrap("

") + .parent()); + + } + var dialog = new instance.web.Dialog(this, { + dialogClass: 'oe_act_window', + title: "Choose Join Node", + }, this.$el).open(); + + joinnodes.find('a').click(function() { + callback(callback_data, choices[$(this).data('idx')]); + dialog.close(); + }) + + //dialog.on('closing', this, function (e){ + // self.check_exit(true); + //}); + //this.$buttonpane = dialog.$buttons; + this.start(); + } + }); +}; diff --git a/bi_view_editor/templates/assets_template.xml b/bi_view_editor/templates/assets_template.xml new file mode 100644 index 00000000..ad6014c5 --- /dev/null +++ b/bi_view_editor/templates/assets_template.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/bi_view_editor/templates/qweb_template.xml b/bi_view_editor/templates/qweb_template.xml new file mode 100644 index 00000000..a1859ac1 --- /dev/null +++ b/bi_view_editor/templates/qweb_template.xml @@ -0,0 +1,78 @@ + + + +
+

Please choose the join node

+
+
+
+
+ + + +
+ + +
+ +
+ +
+
+
+
+ + + + + + + + + + + +
NameModelOptions
+
+
+
+ +
    + +
  • Column
  • +
  • Row
  • +
  • Measure
  • + + +
+
+
+
diff --git a/bi_view_editor/views/bve_view.xml b/bi_view_editor/views/bve_view.xml new file mode 100644 index 00000000..57b3b25d --- /dev/null +++ b/bi_view_editor/views/bve_view.xml @@ -0,0 +1,70 @@ + + + + + + bve.view.tree + bve.view + + + + + + + + + bve.view.form + bve.view + +
+
+
+ +

+ +

+ + + + + + + + + + + +
+
+
+
+ + + Custom BI Views + ir.actions.act_window + bve.view + form + tree,form + +

+ Click to create a Custom Query Object. +

+ +

+
+
+ + + + +
+