diff --git a/bi_sql_editor/README.rst b/bi_sql_editor/README.rst new file mode 100644 index 00000000..a08f05b2 --- /dev/null +++ b/bi_sql_editor/README.rst @@ -0,0 +1,184 @@ +.. 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 Views builder, based on Materialized or Normal SQL Views +=========================================================== + +This module extends the functionality of reporting, to support creation +of extra custom reports. +It allows user to write a custom SQL request. (Generally, admin users) + +Once written, a new model is generated, and user can map the selected field +with odoo fields. +Then user ends the process, creating new menu, action and graph view. + +Technically, the module create SQL View (or materialized view, if option is +checked). Materialized view duplicates datas, but request are fastest. If +materialized view is enabled, this module will create a cron task to refresh +the data). + +By default, users member of 'SQL Request / User' can see all the views. +You can specify extra groups that have the right to access to a specific view. + +Warning +------- +This module is intended for technician people in a company and for Odoo integrators. + +It requires the user to know SQL syntax and Odoo models. + +If you don't have such skills, do not try to use this module specially on a production +environment. + +Use Cases +--------- + +this module is interesting for the following use cases + +* You want to realize technical SQL requests, that Odoo framework doesn't allow + (For exemple, UNION with many SELECT) A typical use case is if you want to have + Sale Orders and PoS Orders datas in a same table + +* You want to customize an Odoo report, removing some useless fields and adding + some custom ones. In that case, you can simply select the fields of the original + report (sale.report model for exemple), and add your custom fields + +* You have a lot of data, and classical SQL Views have very bad performance. + In that case, MATERIALIZED VIEW will be a good solution to reduce display duration + +Configuration +============= + +To configure this module, you need to: + +* Go to Settings / Technical / Database Structure / SQL Views + +* tip your SQL request + + .. figure:: /bi_sql_editor/static/description/01_sql_request.png + :width: 800 px + +* Select the group(s) that could have access to the view + + .. figure:: /bi_sql_editor/static/description/02_security_access.png + :width: 800 px + +* Click on the button 'Clean and Check Request' + +* Once the sql request checked, the module analyses the column of the view, + and propose field mapping. For each field, you can decide to create an index + and set if it will be displayed on the pivot graph as a column, a row or a + measure. + + .. figure:: /bi_sql_editor/static/description/03_field_mapping.png + :width: 800 px + +* Click on the button 'Create SQL View, Indexes and Models'. (this step could + take a while, if view is materialized) + +* If it's a MATERIALIZED view: + * a cron task is created to refresh + the view. You can so define the frequency of the refresh. + * the size of view (and the indexes is displayed) + + .. figure:: /bi_sql_editor/static/description/04_materialized_view_setting.png + :width: 800 px + +* Finally, click on 'Create UI', to create new menu, action, graph view and + search view. + +Usage +===== + +To use this module, you need to: + +* Go to 'Reporting' / 'Custom Reports' + +* select the desired report + + .. figure:: /bi_sql_editor/static/description/05_reporting_pivot.png + :width: 800 px + +* You can switch to 'Pie' chart or 'Line Chart' as any report, + + .. figure:: /bi_sql_editor/static/description/05_reporting_pie.png + :width: 800 px + +.. 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 +====================== + +* Add 'interval', after type (row/col/measure) field for date(time) fields. + +* Dynamically change displayed action name to mention the last refresh of the + materialized view. + +* Create ir.rule to limit access. (for company_id for exemple) + +Note +==== + +The syntax of the sql request has the following constrains: + +* the name of the selectable columns should be prefixed by `x_` + +Sample: + +.. code-block:: sql + + SELECT name as x_name + FROM res_partner + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Sylvain LE GAL (https://twitter.com/legalsylvain) + +* This module is highly inspired by the work of + * Onestein: (http://www.onestein.nl/) + Module: OCA/server-tools/bi_view_editor. + Link: https://github.com/OCA/reporting-engine/tree/8.0/bi_view_editor + * Anybox: (https://anybox.fr/) + Module : OCA/server-tools/materialized_sql_view + link: https://github.com/OCA/server-tools/pull/110 + * GRAP, Groupement Régional Alimentaire de Proximité: (http://www.grap.coop/) + Module: grap/odoo-addons-misc/pos_sale_reporting + link: https://github.com/grap/odoo-addons-misc/tree/7.0/pos_sale_reporting + + +Funders +------- + +The development of this module has been financially supported by: + +* GRAP, Groupement Régional Alimentaire de Proximité (http://www.grap.coop) + +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_sql_editor/__init__.py b/bi_sql_editor/__init__.py new file mode 100644 index 00000000..cde864ba --- /dev/null +++ b/bi_sql_editor/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/bi_sql_editor/__openerp__.py b/bi_sql_editor/__openerp__.py new file mode 100644 index 00000000..0a131a26 --- /dev/null +++ b/bi_sql_editor/__openerp__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'BI SQL Editor', + 'summary': "BI Views builder, based on Materialized or Normal SQL Views", + 'version': '8.0.1.0.0', + 'license': 'AGPL-3', + 'category': 'Reporting', + 'author': 'GRAP,Odoo Community Association (OCA)', + 'website': 'https://www.odoo-community.org', + 'depends': [ + 'sql_request_abstract', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/view_bi_sql_view.xml', + 'views/action.xml', + 'views/menu.xml', + ], + 'demo': [ + 'demo/res_groups.xml', + 'demo/bi_sql_view.xml', + ], + 'installable': True, +} diff --git a/bi_sql_editor/demo/bi_sql_view.xml b/bi_sql_editor/demo/bi_sql_view.xml new file mode 100644 index 00000000..75231916 --- /dev/null +++ b/bi_sql_editor/demo/bi_sql_view.xml @@ -0,0 +1,59 @@ + + + + + + + Draft Incorrect SQL View + incorrect_view + + + + + + Partners View + partners_view + + + + + + + + Modules by Authors + modules_view + + + + + + + + + + + + diff --git a/bi_sql_editor/demo/res_groups.xml b/bi_sql_editor/demo/res_groups.xml new file mode 100644 index 00000000..2e9eadeb --- /dev/null +++ b/bi_sql_editor/demo/res_groups.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/bi_sql_editor/models/__init__.py b/bi_sql_editor/models/__init__.py new file mode 100644 index 00000000..77e272b3 --- /dev/null +++ b/bi_sql_editor/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import bi_sql_view +from . import bi_sql_view_field diff --git a/bi_sql_editor/models/bi_sql_view.py b/bi_sql_editor/models/bi_sql_view.py new file mode 100644 index 00000000..4c282102 --- /dev/null +++ b/bi_sql_editor/models/bi_sql_view.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging +from psycopg2 import ProgrammingError + +from openerp import _, api, fields, models, SUPERUSER_ID +from openerp.exceptions import Warning as UserError + +_logger = logging.getLogger(__name__) + + +class BiSQLView(models.Model): + _name = 'bi.sql.view' + _inherit = ['sql.request.mixin'] + + _sql_prefix = 'x_bi_sql_view_' + + _model_prefix = 'x_bi_sql_view.' + + _sql_request_groups_relation = 'bi_sql_view_groups_rel' + + _sql_request_users_relation = 'bi_sql_view_users_rel' + + _STATE_SQL_EDITOR = [ + ('model_valid', 'SQL View and Model Created'), + ('ui_valid', 'Graph, action and Menu Created'), + ] + + technical_name = fields.Char( + string='Technical Name', required=True, + help="Suffix of the SQL view. (SQL full name will be computed and" + " prefixed by 'x_bi_sql_view_'. Should have correct" + "syntax. For more information, see https://www.postgresql.org/" + "docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS") + + view_name = fields.Char( + string='View Name', compute='_compute_view_name', readonly=True, + store=True, help="Full name of the SQL view") + + model_name = fields.Char( + string='Model Name', compute='_compute_model_name', readonly=True, + store=True, help="Full Qualified Name of the transient model that will" + " be created.") + + is_materialized = fields.Boolean( + string='Is Materialized View', default=True, readonly=True, + states={'draft': [('readonly', False)]}) + + materialized_text = fields.Char( + compute='_compute_materialized_text', store=True) + + size = fields.Char( + string='Database Size', readonly=True, + help="Size of the materialized view and its indexes") + + state = fields.Selection(selection_add=_STATE_SQL_EDITOR) + + query = fields.Text( + help="SQL Request that will be inserted as the view. Take care to :\n" + " * set a name for all your selected fields, specially if you use" + " SQL function (like EXTRACT, ...);\n" + " * Do not use 'SELECT *' or 'SELECT table.*';\n" + " * prefix the name of the selectable columns by 'x_';", + default="SELECT\n" + " my_field as x_my_field\n" + "FROM my_table") + + domain_force = fields.Text( + string='Extra Rule Definition', default="[]", help="Define here" + " access restriction to data.\n" + " Take care to use field name prefixed by 'x_'." + " A global 'ir.rule' will be created." + " A typical Multi Company rule is for exemple \n" + " ['|', ('x_company_id','child_of', [user.company_id.id])," + "('x_company_id','=',False)].") + + has_group_changed = fields.Boolean(copy=False) + + bi_sql_view_field_ids = fields.One2many( + string='SQL Fields', comodel_name='bi.sql.view.field', + inverse_name='bi_sql_view_id') + + model_id = fields.Many2one( + string='Odoo Model', comodel_name='ir.model', readonly=True) + + graph_view_id = fields.Many2one( + string='Odoo Graph View', comodel_name='ir.ui.view', readonly=True) + + search_view_id = fields.Many2one( + string='Odoo Search View', comodel_name='ir.ui.view', readonly=True) + + action_id = fields.Many2one( + string='Odoo Action', comodel_name='ir.actions.act_window', + readonly=True) + + menu_id = fields.Many2one( + string='Odoo Menu', comodel_name='ir.ui.menu', readonly=True) + + cron_id = fields.Many2one( + string='Odoo Cron', comodel_name='ir.cron', readonly=True, + help="Cron Task that will refresh the materialized view") + + rule_id = fields.Many2one( + string='Odoo Rule', comodel_name='ir.rule', readonly=True) + + # Compute Section + @api.depends('is_materialized') + @api.multi + def _compute_materialized_text(self): + for sql_view in self: + sql_view.materialized_text =\ + sql_view.is_materialized and 'MATERIALIZED' or '' + + @api.depends('technical_name') + @api.multi + def _compute_view_name(self): + for sql_view in self: + sql_view.view_name = '%s%s' % ( + sql_view._sql_prefix, sql_view.technical_name) + + @api.depends('technical_name') + @api.multi + def _compute_model_name(self): + for sql_view in self: + sql_view.model_name = '%s%s' % ( + sql_view._model_prefix, sql_view.technical_name) + + @api.onchange('group_ids') + def onchange_group_ids(self): + if self.state not in ('draft', 'sql_valid'): + self.has_group_changed = True + + # Overload Section + @api.multi + def unlink(self): + non_draft_views = self.search([ + ('id', 'in', self.ids), + ('state', 'not in', ('draft', 'sql_valid'))]) + if non_draft_views: + raise UserError(_("You can only unlink draft views")) + return self.unlink() + + @api.multi + def copy(self, default=None): + self.ensure_one() + default = dict(default or {}) + default.update({ + 'name': _('%s (Copy)') % (self.name), + 'technical_name': '%s_copy' % (self.technical_name), + }) + return super(BiSQLView, self).copy(default=default) + + # Action Section + @api.multi + def button_create_sql_view_and_model(self): + for sql_view in self: + if sql_view.state != 'sql_valid': + raise UserError(_( + "You can only process this action on SQL Valid items")) + # Create ORM and acess + sql_view._create_model_and_fields() + sql_view._create_model_access() + + # Create SQL View and indexes + sql_view._create_view() + sql_view._create_index() + + if sql_view.is_materialized: + sql_view.cron_id = self.env['ir.cron'].create( + sql_view._prepare_cron()).id + sql_view.state = 'model_valid' + + @api.multi + def button_set_draft(self): + for sql_view in self: + if sql_view.state in ('model_valid', 'ui_valid'): + # Drop SQL View (and indexes by cascade) + sql_view._drop_view() + + # Drop ORM + sql_view._drop_model_and_fields() + + sql_view.graph_view_id.unlink() + sql_view.action_id.unlink() + sql_view.menu_id.unlink() + sql_view.rule_id.unlink() + if sql_view.cron_id: + sql_view.cron_id.unlink() + sql_view.write({'state': 'draft', 'has_group_changed': False}) + + @api.multi + def button_create_ui(self): + self.graph_view_id = self.env['ir.ui.view'].create( + self._prepare_graph_view()).id + self.search_view_id = self.env['ir.ui.view'].create( + self._prepare_search_view()).id + self.action_id = self.env['ir.actions.act_window'].create( + self._prepare_action()).id + self.menu_id = self.env['ir.ui.menu'].create( + self._prepare_menu()).id + self.write({'state': 'ui_valid'}) + + @api.multi + def button_update_model_access(self): + self._drop_model_access() + self._create_model_access() + self.write({'has_group_changed': False}) + + @api.multi + def button_refresh_materialized_view(self): + self._refresh_materialized_view() + + @api.multi + def button_open_view(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': self.model_id.model, + 'view_id': self.graph_view_id.id, + 'search_view_id': self.search_view_id.id, + 'view_type': 'graph', + 'view_mode': 'graph', + } + + # Prepare Function + @api.multi + def _prepare_model(self): + self.ensure_one() + field_id = [] + for field in self.bi_sql_view_field_ids.filtered( + lambda x: x.field_description is not False): + field_id.append([0, False, field._prepare_model_field()]) + return { + 'name': self.name, + 'model': self.model_name, + 'access_ids': [], + 'field_id': field_id, + } + + @api.multi + def _prepare_model_access(self): + self.ensure_one() + res = [] + for group in self.group_ids: + res.append({ + 'name': _('%s Access %s') % ( + self.model_name, group.full_name), + 'model_id': self.model_id.id, + 'group_id': group.id, + 'perm_read': True, + 'perm_create': False, + 'perm_write': False, + 'perm_unlink': False, + }) + return res + + @api.multi + def _prepare_cron(self): + self.ensure_one() + return { + 'name': _('Refresh Materialized View %s') % (self.view_name), + 'user_id': SUPERUSER_ID, + 'model': 'bi.sql.view', + 'function': 'button_refresh_materialized_view', + 'args': repr(([self.id],)) + } + + @api.multi + def _prepare_rule(self): + self.ensure_one() + return { + 'name': _('Access %s') % (self.name), + 'model_id': self.model_id.id, + 'domain_force': self.domain_force, + 'global': True, + } + + @api.multi + def _prepare_graph_view(self): + self.ensure_one() + return { + 'name': self.name, + 'type': 'graph', + 'model': self.model_id.model, + 'arch': + """""" + """{}""" + """""".format("".join( + [x._prepare_graph_field() + for x in self.bi_sql_view_field_ids])) + } + + @api.multi + def _prepare_search_view(self): + self.ensure_one() + return { + 'name': self.name, + 'type': 'search', + 'model': self.model_id.model, + 'arch': + """""" + """{}""" + """{}""" + """""".format( + "".join( + [x._prepare_search_field() + for x in self.bi_sql_view_field_ids]), + "".join( + [x._prepare_search_filter_field() + for x in self.bi_sql_view_field_ids])) + } + + @api.multi + def _prepare_action(self): + self.ensure_one() + return { + 'name': self.name, + 'res_model': self.model_id.model, + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'graph', + 'view_id': self.graph_view_id.id, + 'search_view_id': self.search_view_id.id, + } + + @api.multi + def _prepare_menu(self): + self.ensure_one() + return { + 'name': self.name, + 'parent_id': self.env.ref('bi_sql_editor.menu_bi_sql_editor').id, + 'action': 'ir.actions.act_window,%s' % (self.action_id.id), + } + + # Custom Section + def _log_execute(self, req): + _logger.info("Executing SQL Request %s ..." % (req)) + self.env.cr.execute(req) + + @api.multi + def _drop_view(self): + for sql_view in self: + self._log_execute( + "DROP %s VIEW IF EXISTS %s" % ( + sql_view.materialized_text, sql_view.view_name)) + sql_view.size = False + + @api.multi + def _create_view(self): + for sql_view in self: + sql_view._drop_view() + try: + self._log_execute(sql_view._prepare_request_for_execution()) + sql_view._refresh_size() + except ProgrammingError as e: + raise UserError(_( + "SQL Error while creating %s VIEW %s :\n %s") % ( + sql_view.materialized_text, sql_view.view_name, + e.message)) + + @api.multi + def _create_index(self): + for sql_view in self: + for sql_field in sql_view.bi_sql_view_field_ids.filtered( + lambda x: x.is_index is True): + self._log_execute( + "CREATE INDEX %s ON %s (%s);" % ( + sql_field.index_name, sql_view.view_name, + sql_field.name)) + + @api.multi + def _create_model_and_fields(self): + for sql_view in self: + # Create model + sql_view.model_id = self.env['ir.model'].create( + self._prepare_model()).id + sql_view.rule_id = self.env['ir.rule'].create( + self._prepare_rule()).id + # Drop table, created by the ORM + req = "DROP TABLE %s" % (sql_view.view_name) + self.env.cr.execute(req) + + @api.multi + def _create_model_access(self): + for sql_view in self: + for item in sql_view._prepare_model_access(): + self.env['ir.model.access'].create(item) + + @api.multi + def _drop_model_access(self): + for sql_view in self: + self.env['ir.model.access'].search( + [('model_id', '=', sql_view.model_name)]).unlink() + + @api.multi + def _drop_model_and_fields(self): + for sql_view in self: + sql_view.model_id.unlink() + + @api.multi + def _hook_executed_request(self): + self.ensure_one() + req = """ + SELECT attnum, + attname AS column, + format_type(atttypid, atttypmod) AS type + FROM pg_attribute + WHERE attrelid = '%s'::regclass + AND NOT attisdropped + AND attnum > 0 + ORDER BY attnum;""" % (self.view_name) + self.env.cr.execute(req) + return self.env.cr.fetchall() + + @api.multi + def _prepare_request_check_execution(self): + self.ensure_one() + return "CREATE VIEW %s AS (%s);" % (self.view_name, self.query) + + @api.multi + def _prepare_request_for_execution(self): + self.ensure_one() + query = """ + SELECT + CAST(row_number() OVER () as integer) AS id, + CAST(Null as timestamp without time zone) as create_date, + CAST(Null as integer) as create_uid, + CAST(Null as timestamp without time zone) as write_date, + CAST(Null as integer) as write_uid, + my_query.* + FROM + (%s) as my_query + """ % (self.query) + return "CREATE %s VIEW %s AS (%s);" % ( + self.materialized_text, self.view_name, query) + + @api.multi + def _check_execution(self): + """Ensure that the query is valid, trying to execute it. + a non materialized view is created for this check. + A rollback is done at the end. + After the execution, and before the rollback, an analysis of + the database structure is done, to know fields type.""" + self.ensure_one() + sql_view_field_obj = self.env['bi.sql.view.field'] + columns = super(BiSQLView, self)._check_execution() + field_ids = [] + for column in columns: + existing_field = self.bi_sql_view_field_ids.filtered( + lambda x: x.name == column[1]) + if existing_field: + # Update existing field + field_ids.append(existing_field.id) + existing_field.write({ + 'sequence': column[0], + 'sql_type': column[2], + }) + else: + # Create a new one if name is prefixed by x_ + if column[1][:2] == 'x_': + field_ids.append(sql_view_field_obj.create({ + 'sequence': column[0], + 'name': column[1], + 'sql_type': column[2], + 'bi_sql_view_id': self.id, + }).id) + + # Drop obsolete view field + self.bi_sql_view_field_ids.filtered( + lambda x: x.id not in field_ids).unlink() + + if not self.bi_sql_view_field_ids: + raise UserError(_( + "No Column was found.\n" + "Columns name should be prefixed by 'x_'.")) + + return columns + + @api.multi + def _refresh_materialized_view(self): + for sql_view in self: + req = "REFRESH %s VIEW %s" % ( + sql_view.materialized_text, sql_view.view_name) + self._log_execute(req) + sql_view._refresh_size() + + @api.multi + def _refresh_size(self): + for sql_view in self: + req = "SELECT pg_size_pretty(pg_total_relation_size('%s'));" % ( + sql_view.view_name) + self.env.cr.execute(req) + sql_view.size = self.env.cr.fetchone()[0] diff --git a/bi_sql_editor/models/bi_sql_view_field.py b/bi_sql_editor/models/bi_sql_view_field.py new file mode 100644 index 00000000..76f20657 --- /dev/null +++ b/bi_sql_editor/models/bi_sql_view_field.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import re + +from openerp import api, fields, models + + +class BiSQLViewField(models.Model): + _name = 'bi.sql.view.field' + _order = 'sequence' + + _TTYPE_SELECTION = [ + ('boolean', 'boolean'), + ('char', 'char'), + ('date', 'date'), + ('datetime', 'datetime'), + ('float', 'float'), + ('integer', 'integer'), + ('many2one', 'many2one'), + ('selection', 'selection'), + ] + + _GRAPH_TYPE_SELECTION = [ + ('col', 'Column'), + ('row', 'Row'), + ('measure', 'Measure'), + ] + + # Mapping to guess Odoo field type, from SQL column type + _SQL_MAPPING = { + 'boolean': 'boolean', + 'bigint': 'integer', + 'integer': 'integer', + 'double precision': 'float', + 'numeric': 'float', + 'text': 'char', + 'character varying': 'char', + 'date': 'datetime', + 'timestamp without time zone': 'datetime', + } + + name = fields.Char(string='Name', required=True, readonly=True) + + sql_type = fields.Char( + string='SQL Type', required=True, readonly=True, + help="SQL Type in the database") + + sequence = fields.Integer(string='sequence', required=True, readonly=True) + + bi_sql_view_id = fields.Many2one( + string='SQL View', comodel_name='bi.sql.view', ondelete='cascade') + + is_index = fields.Boolean( + string='Is Index', help="Check this box if you want to create" + " an index on that field. This is recommended for searchable and" + " groupable fields, to reduce duration") + + is_group_by = fields.Boolean( + string='Is Group by', help="Check this box if you want to create" + " a 'group by' option in the search view") + + index_name = fields.Char( + string='Index Name', compute='_compute_index_name') + + graph_type = fields.Selection( + string='Graph Type', selection=_GRAPH_TYPE_SELECTION) + + field_description = fields.Char( + string='Field Description', help="This will be used as the name" + " of the Odoo field, displayed for users") + + ttype = fields.Selection( + string='Field Type', selection=_TTYPE_SELECTION, help="Type of the" + " Odoo field that will be created. Let empty if you don't want to" + " create a new field. If empty, this field will not be displayed" + " neither available for search or group by function") + + selection = fields.Text( + string='Selection Options', default='[]', + help="For 'Selection' Odoo field.\n" + " List of options, specified as a Python expression defining a list of" + " (key, label) pairs. For example:" + " [('blue','Blue'), ('yellow','Yellow')]") + + many2one_model_id = fields.Many2one( + comodel_name='ir.model', string='Model', + help="For 'Many2one' Odoo field.\n" + " Co Model of the field.") + + # Compute Section + @api.multi + def _compute_index_name(self): + for sql_field in self: + sql_field.index_name = '%s_%s' % ( + sql_field.bi_sql_view_id.view_name, sql_field.name) + + # Overload Section + @api.multi + def create(self, vals): + field_without_prefix = vals['name'][2:] + # guess field description + field_description = re.sub( + r'\w+', lambda m: m.group(0).capitalize(), + field_without_prefix.replace('_id', '').replace('_', ' ')) + + # Guess ttype + # Don't execute as simple .get() in the dict to manage + # correctly the type 'character varying(x)' + ttype = False + for k, v in self._SQL_MAPPING.iteritems(): + if k in vals['sql_type']: + ttype = v + + # Guess many2one_model_id + many2one_model_id = False + if vals['sql_type'] == 'integer' and( + vals['name'][-3:] == '_id'): + ttype = 'many2one' + model_name = self._model_mapping().get(field_without_prefix, '') + many2one_model_id = self.env['ir.model'].search( + [('model', '=', model_name)]).id + + vals.update({ + 'ttype': ttype, + 'field_description': field_description, + 'many2one_model_id': many2one_model_id, + }) + return super(BiSQLViewField, self).create(vals) + + # Custom Section + @api.model + def _model_mapping(self): + """Return dict of key value, to try to guess the model based on a + field name. Sample : + {'account_id': 'account.account'; 'product_id': 'product.product'} + """ + relation_fields = self.env['ir.model.fields'].search([ + ('ttype', '=', 'many2one')]) + res = {} + keys_to_pop = [] + for field in relation_fields: + if field.name in res and res.get(field.name) != field.relation: + # The field name is not predictive + keys_to_pop.append(field.name) + else: + res.update({field.name: field.relation}) + + for key in list(set(keys_to_pop)): + res.pop(key) + + return res + + @api.multi + def _prepare_model_field(self): + self.ensure_one() + return { + 'name': self.name, + 'field_description': self.field_description, + 'model_id': self.bi_sql_view_id.model_id.id, + 'ttype': self.ttype, + 'selection': self.ttype == 'selection' and self.selection or False, + 'relation': self.ttype == 'many2one' and + self.many2one_model_id.model or False, + } + + @api.multi + def _prepare_graph_field(self): + self.ensure_one() + res = '' + if self.graph_type and self.field_description: + res = """""".format( + self.name, self.graph_type) + return res + + @api.multi + def _prepare_search_field(self): + self.ensure_one() + res = '' + if self.field_description: + res = """""".format(self.name) + return res + + @api.multi + def _prepare_search_filter_field(self): + self.ensure_one() + res = '' + if self.field_description and self.is_group_by: + res =\ + """""" % ( + self.field_description, self.name) + return res diff --git a/bi_sql_editor/security/ir.model.access.csv b/bi_sql_editor/security/ir.model.access.csv new file mode 100644 index 00000000..ca78a1ed --- /dev/null +++ b/bi_sql_editor/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_bi_sql_view_all,access_bi_sql_view_all,model_bi_sql_view,,0,0,0,0 +access_bi_sql_view_manager,access_bi_sql_view_manager,model_bi_sql_view,sql_request_abstract.group_sql_request_manager,1,1,1,1 +,,,,,,, +access_bi_sql_view_field_all,access_bi_sql_view_field_all,model_bi_sql_view_field,,0,0,0,0 +access_bi_sql_view_field_manager,access_bi_sql_view_field_manager,model_bi_sql_view_field,sql_request_abstract.group_sql_request_manager,1,1,1,1 diff --git a/bi_sql_editor/static/description/01_sql_request.png b/bi_sql_editor/static/description/01_sql_request.png new file mode 100644 index 00000000..118150e3 Binary files /dev/null and b/bi_sql_editor/static/description/01_sql_request.png differ diff --git a/bi_sql_editor/static/description/02_security_access.png b/bi_sql_editor/static/description/02_security_access.png new file mode 100644 index 00000000..fb717b91 Binary files /dev/null and b/bi_sql_editor/static/description/02_security_access.png differ diff --git a/bi_sql_editor/static/description/03_field_mapping.png b/bi_sql_editor/static/description/03_field_mapping.png new file mode 100644 index 00000000..08a8770f Binary files /dev/null and b/bi_sql_editor/static/description/03_field_mapping.png differ diff --git a/bi_sql_editor/static/description/04_materialized_view_setting.png b/bi_sql_editor/static/description/04_materialized_view_setting.png new file mode 100644 index 00000000..bcb25c01 Binary files /dev/null and b/bi_sql_editor/static/description/04_materialized_view_setting.png differ diff --git a/bi_sql_editor/static/description/05_reporting_pie.png b/bi_sql_editor/static/description/05_reporting_pie.png new file mode 100644 index 00000000..1ec4ac0c Binary files /dev/null and b/bi_sql_editor/static/description/05_reporting_pie.png differ diff --git a/bi_sql_editor/static/description/05_reporting_pivot.png b/bi_sql_editor/static/description/05_reporting_pivot.png new file mode 100644 index 00000000..c4c0a36c Binary files /dev/null and b/bi_sql_editor/static/description/05_reporting_pivot.png differ diff --git a/bi_sql_editor/static/description/icon.png b/bi_sql_editor/static/description/icon.png new file mode 100644 index 00000000..c72ba5ca Binary files /dev/null and b/bi_sql_editor/static/description/icon.png differ diff --git a/bi_sql_editor/static/description/main_screenshot.png b/bi_sql_editor/static/description/main_screenshot.png new file mode 100644 index 00000000..71c2984f Binary files /dev/null and b/bi_sql_editor/static/description/main_screenshot.png differ diff --git a/bi_sql_editor/views/action.xml b/bi_sql_editor/views/action.xml new file mode 100644 index 00000000..31c1c0be --- /dev/null +++ b/bi_sql_editor/views/action.xml @@ -0,0 +1,18 @@ + + + + + + + SQL Views + ir.actions.act_window + bi.sql.view + form + tree,form + + + diff --git a/bi_sql_editor/views/menu.xml b/bi_sql_editor/views/menu.xml new file mode 100644 index 00000000..5fe24bab --- /dev/null +++ b/bi_sql_editor/views/menu.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/bi_sql_editor/views/view_bi_sql_view.xml b/bi_sql_editor/views/view_bi_sql_view.xml new file mode 100644 index 00000000..5ab2034b --- /dev/null +++ b/bi_sql_editor/views/view_bi_sql_view.xml @@ -0,0 +1,126 @@ + + + + + + + bi.sql.view + + + + + + + + + + + + bi.sql.view + +
+
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +