Browse Source

[IMP] bi_sql_editor: black, isort, prettier

14.0-report-py3o-pr-506
HviorForgeFlow 5 years ago
committed by David James
parent
commit
f020f4e8a7
  1. 38
      bi_sql_editor/__manifest__.py
  2. 38
      bi_sql_editor/demo/bi_sql_view_demo.xml
  3. 2
      bi_sql_editor/demo/res_groups_demo.xml
  4. 2
      bi_sql_editor/hooks.py
  5. 565
      bi_sql_editor/models/bi_sql_view.py
  6. 211
      bi_sql_editor/models/bi_sql_view_field.py
  7. 95
      bi_sql_editor/tests/test_bi_sql_view.py
  8. 3
      bi_sql_editor/views/action.xml
  9. 16
      bi_sql_editor/views/menu.xml
  10. 192
      bi_sql_editor/views/view_bi_sql_view.xml
  11. 1
      setup/bi_sql_editor/odoo/addons/bi_sql_editor
  12. 6
      setup/bi_sql_editor/setup.py

38
bi_sql_editor/__manifest__.py

@ -3,27 +3,21 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # 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': '12.0.1.2.0',
'license': 'AGPL-3',
'category': 'Reporting',
'author': 'GRAP,Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/reporting-engine',
'depends': [
'base',
'sql_request_abstract',
"name": "BI SQL Editor",
"summary": "BI Views builder, based on Materialized or Normal SQL Views",
"version": "12.0.1.2.0",
"license": "AGPL-3",
"category": "Reporting",
"author": "GRAP,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/reporting-engine",
"depends": ["base", "sql_request_abstract"],
"data": [
"security/ir.model.access.csv",
"views/view_bi_sql_view.xml",
"views/action.xml",
"views/menu.xml",
], ],
'data': [
'security/ir.model.access.csv',
'views/view_bi_sql_view.xml',
'views/action.xml',
'views/menu.xml',
],
'demo': [
'demo/res_groups_demo.xml',
'demo/bi_sql_view_demo.xml',
],
'installable': True,
'uninstall_hook': 'uninstall_hook'
"demo": ["demo/res_groups_demo.xml", "demo/bi_sql_view_demo.xml"],
"installable": True,
"uninstall_hook": "uninstall_hook",
} }

38
bi_sql_editor/demo/bi_sql_view_demo.xml

@ -4,24 +4,25 @@ Copyright (C) 2014 - Today GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain) @author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
<odoo noupdate="1"> <odoo noupdate="1">
<record id="incorrect_sql_view" model="bi.sql.view"> <record id="incorrect_sql_view" model="bi.sql.view">
<field name="name">Draft Incorrect SQL View</field> <field name="name">Draft Incorrect SQL View</field>
<field name="technical_name">incorrect_view</field> <field name="technical_name">incorrect_view</field>
<field name="query"><![CDATA[
<field
name="query"
><![CDATA[
SELECT * SELECT *
FROM unexisting_table FROM unexisting_table
ORDER BY unexisting_field ORDER BY unexisting_field
]]> ]]>
</field> </field>
</record> </record>
<record id="partner_sql_view" model="bi.sql.view"> <record id="partner_sql_view" model="bi.sql.view">
<field name="name">Partners View</field> <field name="name">Partners View</field>
<field name="technical_name">partners_view</field> <field name="technical_name">partners_view</field>
<field name="query"><![CDATA[
<field
name="query"
><![CDATA[
SELECT SELECT
name as x_name, name as x_name,
street as x_street, street as x_street,
@ -31,12 +32,13 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
]]> ]]>
</field> </field>
</record> </record>
<record id="module_sql_view" model="bi.sql.view"> <record id="module_sql_view" model="bi.sql.view">
<field name="name">Modules by Authors</field> <field name="name">Modules by Authors</field>
<field name="technical_name">modules_view</field> <field name="technical_name">modules_view</field>
<field name="is_materialized" eval="0" /> <field name="is_materialized" eval="0" />
<field name="query"><![CDATA[
<field
name="query"
><![CDATA[
SELECT SELECT
name as x_name, name as x_name,
case case
@ -47,11 +49,19 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
]]> ]]>
</field> </field>
</record> </record>
<function model="bi.sql.view" name="button_validate_sql_expression" eval="([ref('module_sql_view')])"/>
<function model="bi.sql.view" name="button_create_sql_view_and_model" eval="([ref('module_sql_view')])"/>
<function model="bi.sql.view" name="button_create_ui" eval="([ref('module_sql_view')])"/>
<function
model="bi.sql.view"
name="button_validate_sql_expression"
eval="([ref('module_sql_view')])"
/>
<function
model="bi.sql.view"
name="button_create_sql_view_and_model"
eval="([ref('module_sql_view')])"
/>
<function
model="bi.sql.view"
name="button_create_ui"
eval="([ref('module_sql_view')])"
/>
</odoo> </odoo>

2
bi_sql_editor/demo/res_groups_demo.xml

@ -4,12 +4,10 @@ Copyright (C) 2014 - Today GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain) @author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
<odoo> <odoo>
<record id="base.group_no_one" model="res.groups"> <record id="base.group_no_one" model="res.groups">
<field name="users" eval="[(4, ref('base.user_admin'))]" /> <field name="users" eval="[(4, ref('base.user_admin'))]" />
</record> </record>
<record id="sql_request_abstract.group_sql_request_user" model="res.groups"> <record id="sql_request_abstract.group_sql_request_user" model="res.groups">
<field name="users" eval="[(4, ref('base.user_demo'))]" /> <field name="users" eval="[(4, ref('base.user_demo'))]" />
</record> </record>

2
bi_sql_editor/hooks.py

@ -6,6 +6,6 @@ from odoo.api import Environment
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
env = Environment(cr, SUPERUSER_ID, {}) env = Environment(cr, SUPERUSER_ID, {})
recs = env['bi.sql.view'].search([])
recs = env["bi.sql.view"].search([])
for rec in recs: for rec in recs:
rec.button_set_draft() rec.button_set_draft()

565
bi_sql_editor/models/bi_sql_view.py

@ -4,11 +4,13 @@
import logging import logging
from datetime import datetime from datetime import datetime
from psycopg2 import ProgrammingError from psycopg2 import ProgrammingError
from odoo import _, api, fields, models, SUPERUSER_ID
from odoo import SUPERUSER_ID, _, api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools import pycompat, safe_eval, sql from odoo.tools import pycompat, safe_eval, sql
from odoo.addons.base.models.ir_model import IrModel from odoo.addons.base.models.ir_model import IrModel
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -21,15 +23,15 @@ def _instanciate(self, model_data):
# This monkey patch is meant to avoid create/search tables for those # This monkey patch is meant to avoid create/search tables for those
# materialized views. Doing "super" doesn't work. # materialized views. Doing "super" doesn't work.
class CustomModel(models.Model): class CustomModel(models.Model):
_name = pycompat.to_native(model_data['model'])
_description = model_data['name']
_name = pycompat.to_native(model_data["model"])
_description = model_data["name"]
_module = False _module = False
_custom = True _custom = True
_transient = bool(model_data['transient'])
__doc__ = model_data['info']
_transient = bool(model_data["transient"])
__doc__ = model_data["info"]
# START OF patch # START OF patch
if model_data['model'].startswith(BiSQLView._model_prefix):
if model_data["model"].startswith(BiSQLView._model_prefix):
CustomModel._auto = False CustomModel._auto = False
CustomModel._abstract = True CustomModel._abstract = True
# END of patch # END of patch
@ -40,62 +42,73 @@ IrModel._instanciate = _instanciate
class BiSQLView(models.Model): class BiSQLView(models.Model):
_name = 'bi.sql.view'
_order = 'sequence'
_inherit = ['sql.request.mixin']
_name = "bi.sql.view"
_order = "sequence"
_inherit = ["sql.request.mixin"]
_sql_prefix = 'x_bi_sql_view_'
_sql_prefix = "x_bi_sql_view_"
_model_prefix = 'x_bi_sql_view.'
_model_prefix = "x_bi_sql_view."
_sql_request_groups_relation = 'bi_sql_view_groups_rel'
_sql_request_groups_relation = "bi_sql_view_groups_rel"
_sql_request_users_relation = 'bi_sql_view_users_rel'
_sql_request_users_relation = "bi_sql_view_users_rel"
_STATE_SQL_EDITOR = [ _STATE_SQL_EDITOR = [
('model_valid', 'SQL View and Model Created'),
('ui_valid', 'Views, Action and Menu Created'),
("model_valid", "SQL View and Model Created"),
("ui_valid", "Views, Action and Menu Created"),
] ]
technical_name = fields.Char( technical_name = fields.Char(
string='Technical Name', required=True,
string="Technical Name",
required=True,
help="Suffix of the SQL view. SQL full name will be computed and" help="Suffix of the SQL view. SQL full name will be computed and"
" prefixed by 'x_bi_sql_view_'. Syntax should follow: " " prefixed by 'x_bi_sql_view_'. Syntax should follow: "
"https://www.postgresql.org/" "https://www.postgresql.org/"
"docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS")
"docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS",
)
view_name = fields.Char( view_name = fields.Char(
string='View Name', compute='_compute_view_name', readonly=True,
store=True, help="Full name of the SQL view")
string="View Name",
compute="_compute_view_name",
readonly=True,
store=True,
help="Full name of the SQL view",
)
model_name = fields.Char( 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.")
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( is_materialized = fields.Boolean(
string='Is Materialized View', default=True, readonly=True,
states={
'draft': [('readonly', False)],
'sql_valid': [('readonly', False)],
})
string="Is Materialized View",
default=True,
readonly=True,
states={"draft": [("readonly", False)], "sql_valid": [("readonly", False)]},
)
materialized_text = fields.Char(
compute='_compute_materialized_text', store=True)
materialized_text = fields.Char(compute="_compute_materialized_text", store=True)
size = fields.Char( size = fields.Char(
string='Database Size', readonly=True,
help="Size of the materialized view and its indexes")
string="Database Size",
readonly=True,
help="Size of the materialized view and its indexes",
)
state = fields.Selection(selection_add=_STATE_SQL_EDITOR) state = fields.Selection(selection_add=_STATE_SQL_EDITOR)
view_order = fields.Char(string='View Order',
view_order = fields.Char(
string="View Order",
required=True, required=True,
readonly=False, readonly=False,
states={'ui_valid': [('readonly', True)]},
states={"ui_valid": [("readonly", True)]},
default="pivot,graph,tree", default="pivot,graph,tree",
help='Comma-separated text. Possible values:'
' "graph", "pivot" or "tree"')
help="Comma-separated text. Possible values:" ' "graph", "pivot" or "tree"',
)
query = fields.Text( query = fields.Text(
help="SQL Request that will be inserted as the view. Take care to :\n" help="SQL Request that will be inserted as the view. Take care to :\n"
@ -103,98 +116,112 @@ class BiSQLView(models.Model):
" SQL function (like EXTRACT, ...);\n" " SQL function (like EXTRACT, ...);\n"
" * Do not use 'SELECT *' or 'SELECT table.*';\n" " * Do not use 'SELECT *' or 'SELECT table.*';\n"
" * prefix the name of the selectable columns by 'x_';", " * prefix the name of the selectable columns by 'x_';",
default="SELECT\n"
" my_field as x_my_field\n"
"FROM my_table")
default="SELECT\n" " my_field as x_my_field\n" "FROM my_table",
)
domain_force = fields.Text( domain_force = fields.Text(
string='Extra Rule Definition', default="[]", readonly=True,
string="Extra Rule Definition",
default="[]",
readonly=True,
help="Define here access restriction to data.\n" help="Define here access restriction to data.\n"
" Take care to use field name prefixed by 'x_'." " Take care to use field name prefixed by 'x_'."
" A global 'ir.rule' will be created." " A global 'ir.rule' will be created."
" A typical Multi Company rule is for exemple \n" " A typical Multi Company rule is for exemple \n"
" ['|', ('x_company_id','child_of', [user.company_id.id])," " ['|', ('x_company_id','child_of', [user.company_id.id]),"
"('x_company_id','=',False)].", "('x_company_id','=',False)].",
states={
'draft': [('readonly', False)],
'sql_valid': [('readonly', False)],
})
states={"draft": [("readonly", False)], "sql_valid": [("readonly", False)]},
)
computed_action_context = fields.Text( computed_action_context = fields.Text(
compute="_compute_computed_action_context",
string="Computed Action Context")
compute="_compute_computed_action_context", string="Computed Action Context"
)
action_context = fields.Text( action_context = fields.Text(
string="Action Context", default="{}", readonly=True,
string="Action Context",
default="{}",
readonly=True,
help="Define here a context that will be used" help="Define here a context that will be used"
" by default, when creating the action.", " by default, when creating the action.",
states={ states={
'draft': [('readonly', False)],
'sql_valid': [('readonly', False)],
'model_valid': [('readonly', False)],
})
"draft": [("readonly", False)],
"sql_valid": [("readonly", False)],
"model_valid": [("readonly", False)],
},
)
has_group_changed = fields.Boolean(copy=False) has_group_changed = fields.Boolean(copy=False)
bi_sql_view_field_ids = fields.One2many( bi_sql_view_field_ids = fields.One2many(
string='SQL Fields', comodel_name='bi.sql.view.field',
inverse_name='bi_sql_view_id')
string="SQL Fields",
comodel_name="bi.sql.view.field",
inverse_name="bi_sql_view_id",
)
model_id = fields.Many2one( model_id = fields.Many2one(
string='Odoo Model', comodel_name='ir.model', readonly=True)
string="Odoo Model", comodel_name="ir.model", readonly=True
)
tree_view_id = fields.Many2one( tree_view_id = fields.Many2one(
string='Odoo Tree View', comodel_name='ir.ui.view', readonly=True)
string="Odoo Tree View", comodel_name="ir.ui.view", readonly=True
)
graph_view_id = fields.Many2one( graph_view_id = fields.Many2one(
string='Odoo Graph View', comodel_name='ir.ui.view', readonly=True)
string="Odoo Graph View", comodel_name="ir.ui.view", readonly=True
)
pivot_view_id = fields.Many2one( pivot_view_id = fields.Many2one(
string='Odoo Pivot View', comodel_name='ir.ui.view', readonly=True)
string="Odoo Pivot View", comodel_name="ir.ui.view", readonly=True
)
search_view_id = fields.Many2one( search_view_id = fields.Many2one(
string='Odoo Search View', comodel_name='ir.ui.view', readonly=True)
string="Odoo Search View", comodel_name="ir.ui.view", readonly=True
)
action_id = fields.Many2one( action_id = fields.Many2one(
string='Odoo Action', comodel_name='ir.actions.act_window',
readonly=True)
string="Odoo Action", comodel_name="ir.actions.act_window", readonly=True
)
menu_id = fields.Many2one( menu_id = fields.Many2one(
string='Odoo Menu', comodel_name='ir.ui.menu', readonly=True)
string="Odoo Menu", comodel_name="ir.ui.menu", readonly=True
)
cron_id = fields.Many2one( cron_id = fields.Many2one(
string='Odoo Cron', comodel_name='ir.cron', readonly=True,
help="Cron Task that will refresh the materialized view")
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)
rule_id = fields.Many2one(string="Odoo Rule", comodel_name="ir.rule", readonly=True)
group_ids = fields.Many2many( group_ids = fields.Many2many(
comodel_name='res.groups', readonly=True, states={
'draft': [('readonly', False)],
'sql_valid': [('readonly', False)],
})
comodel_name="res.groups",
readonly=True,
states={"draft": [("readonly", False)], "sql_valid": [("readonly", False)]},
)
sequence = fields.Integer(string='sequence')
sequence = fields.Integer(string="sequence")
# Constrains Section # Constrains Section
@api.constrains('is_materialized')
@api.constrains("is_materialized")
@api.multi @api.multi
def _check_index_materialized(self): def _check_index_materialized(self):
for rec in self.filtered(lambda x: not x.is_materialized): for rec in self.filtered(lambda x: not x.is_materialized):
if rec.bi_sql_view_field_ids.filtered(lambda x: x.is_index): if rec.bi_sql_view_field_ids.filtered(lambda x: x.is_index):
raise UserError(_(
'You can not create indexes on non materialized views'))
raise UserError(
_("You can not create indexes on non materialized views")
)
@api.constrains('view_order')
@api.constrains("view_order")
@api.multi @api.multi
def _check_view_order(self): def _check_view_order(self):
for rec in self: for rec in self:
if rec.view_order: if rec.view_order:
for vtype in rec.view_order.split(','):
if vtype not in ('graph', 'pivot', 'tree'):
raise UserError(_(
'Only graph, pivot or tree views are supported'))
for vtype in rec.view_order.split(","):
if vtype not in ("graph", "pivot", "tree"):
raise UserError(
_("Only graph, pivot or tree views are supported")
)
# Compute Section # Compute Section
@api.depends("bi_sql_view_field_ids.graph_type") @api.depends("bi_sql_view_field_ids.graph_type")
@ -207,60 +234,71 @@ class BiSQLView(models.Model):
"pivot_column_groupby": [], "pivot_column_groupby": [],
} }
for field in rec.bi_sql_view_field_ids.filtered( for field in rec.bi_sql_view_field_ids.filtered(
lambda x: x.graph_type == "measure"):
lambda x: x.graph_type == "measure"
):
action["pivot_measures"].append(field.name) action["pivot_measures"].append(field.name)
for field in rec.bi_sql_view_field_ids.filtered( for field in rec.bi_sql_view_field_ids.filtered(
lambda x: x.graph_type == "row"):
lambda x: x.graph_type == "row"
):
action["pivot_row_groupby"].append(field.name) action["pivot_row_groupby"].append(field.name)
for field in rec.bi_sql_view_field_ids.filtered( for field in rec.bi_sql_view_field_ids.filtered(
lambda x: x.graph_type == "col"):
lambda x: x.graph_type == "col"
):
action["pivot_column_groupby"].append(field.name) action["pivot_column_groupby"].append(field.name)
rec.computed_action_context = str(action) rec.computed_action_context = str(action)
@api.depends('is_materialized')
@api.depends("is_materialized")
@api.multi @api.multi
def _compute_materialized_text(self): def _compute_materialized_text(self):
for sql_view in self: for sql_view in self:
sql_view.materialized_text =\
sql_view.is_materialized and 'MATERIALIZED' or ''
sql_view.materialized_text = (
sql_view.is_materialized and "MATERIALIZED" or ""
)
@api.depends('technical_name')
@api.depends("technical_name")
@api.multi @api.multi
def _compute_view_name(self): def _compute_view_name(self):
for sql_view in self: for sql_view in self:
sql_view.view_name = '%s%s' % (
sql_view._sql_prefix, sql_view.technical_name)
sql_view.view_name = "{}{}".format(
sql_view._sql_prefix,
sql_view.technical_name,
)
@api.depends('technical_name')
@api.depends("technical_name")
@api.multi @api.multi
def _compute_model_name(self): def _compute_model_name(self):
for sql_view in self: for sql_view in self:
sql_view.model_name = '%s%s' % (
sql_view._model_prefix, sql_view.technical_name)
sql_view.model_name = "{}{}".format(
sql_view._model_prefix,
sql_view.technical_name,
)
@api.onchange('group_ids')
@api.onchange("group_ids")
def onchange_group_ids(self): def onchange_group_ids(self):
if self.state not in ('draft', 'sql_valid'):
if self.state not in ("draft", "sql_valid"):
self.has_group_changed = True self.has_group_changed = True
# Overload Section # Overload Section
@api.multi @api.multi
def write(self, vals): def write(self, vals):
res = super(BiSQLView, self).write(vals) res = super(BiSQLView, self).write(vals)
if vals.get('sequence', False):
if vals.get("sequence", False):
for rec in self.filtered(lambda x: x.menu_id): for rec in self.filtered(lambda x: x.menu_id):
rec.menu_id.sequence = rec.sequence rec.menu_id.sequence = rec.sequence
return res return res
@api.multi @api.multi
def unlink(self): def unlink(self):
if any(view.state not in ('draft', 'sql_valid') for view in self):
if any(view.state not in ("draft", "sql_valid") for view in self):
raise UserError( raise UserError(
_("You can only unlink draft views."
"If you want to delete them, first set them to draft."))
_(
"You can only unlink draft views."
"If you want to delete them, first set them to draft."
)
)
self.cron_id.unlink() self.cron_id.unlink()
return super(BiSQLView, self).unlink() return super(BiSQLView, self).unlink()
@ -268,10 +306,12 @@ class BiSQLView(models.Model):
def copy(self, default=None): def copy(self, default=None):
self.ensure_one() self.ensure_one()
default = dict(default or {}) default = dict(default or {})
default.update({
'name': _('%s (Copy)') % self.name,
'technical_name': '%s_copy' % self.technical_name,
})
default.update(
{
"name": _("%s (Copy)") % self.name,
"technical_name": "%s_copy" % self.technical_name,
}
)
return super(BiSQLView, self).copy(default=default) return super(BiSQLView, self).copy(default=default)
# Action Section # Action Section
@ -288,11 +328,12 @@ class BiSQLView(models.Model):
if sql_view.is_materialized: if sql_view.is_materialized:
if not sql_view.cron_id: if not sql_view.cron_id:
sql_view.cron_id = self.env['ir.cron'].create(
sql_view._prepare_cron()).id
sql_view.cron_id = (
self.env["ir.cron"].create(sql_view._prepare_cron()).id
)
else: else:
sql_view.cron_id.active = True sql_view.cron_id.active = True
sql_view.state = 'model_valid'
sql_view.state = "model_valid"
@api.multi @api.multi
def button_set_draft(self): def button_set_draft(self):
@ -304,7 +345,7 @@ class BiSQLView(models.Model):
sql_view.pivot_view_id.unlink() sql_view.pivot_view_id.unlink()
sql_view.search_view_id.unlink() sql_view.search_view_id.unlink()
if sql_view.state in ('model_valid', 'ui_valid'):
if sql_view.state in ("model_valid", "ui_valid"):
# Drop SQL View (and indexes by cascade) # Drop SQL View (and indexes by cascade)
if sql_view.is_materialized: if sql_view.is_materialized:
sql_view._drop_view() sql_view._drop_view()
@ -321,25 +362,27 @@ class BiSQLView(models.Model):
@api.multi @api.multi
def button_create_ui(self): def button_create_ui(self):
self.tree_view_id = self.env['ir.ui.view'].create(
self._prepare_tree_view()).id
self.graph_view_id = self.env['ir.ui.view'].create(
self._prepare_graph_view()).id
self.pivot_view_id = self.env['ir.ui.view'].create(
self._prepare_pivot_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'})
self.tree_view_id = self.env["ir.ui.view"].create(self._prepare_tree_view()).id
self.graph_view_id = (
self.env["ir.ui.view"].create(self._prepare_graph_view()).id
)
self.pivot_view_id = (
self.env["ir.ui.view"].create(self._prepare_pivot_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 @api.multi
def button_update_model_access(self): def button_update_model_access(self):
self._drop_model_access() self._drop_model_access()
self._create_model_access() self._create_model_access()
self.write({'has_group_changed': False})
self.write({"has_group_changed": False})
@api.multi @api.multi
def button_refresh_materialized_view(self): def button_refresh_materialized_view(self):
@ -348,10 +391,10 @@ class BiSQLView(models.Model):
@api.multi @api.multi
def button_open_view(self): def button_open_view(self):
return { return {
'type': 'ir.actions.act_window',
'res_model': self.model_id.model,
'search_view_id': self.search_view_id.id,
'view_mode': self.action_id.view_mode,
"type": "ir.actions.act_window",
"res_model": self.model_id.model,
"search_view_id": self.search_view_id.id,
"view_mode": self.action_id.view_mode,
} }
# Prepare Function # Prepare Function
@ -360,13 +403,14 @@ class BiSQLView(models.Model):
self.ensure_one() self.ensure_one()
field_id = [] field_id = []
for field in self.bi_sql_view_field_ids.filtered( for field in self.bi_sql_view_field_ids.filtered(
lambda x: x.field_description is not False):
lambda x: x.field_description is not False
):
field_id.append([0, False, field._prepare_model_field()]) field_id.append([0, False, field._prepare_model_field()])
return { return {
'name': self.name,
'model': self.model_name,
'access_ids': [],
'field_id': field_id,
"name": self.name,
"model": self.model_name,
"access_ids": [],
"field_id": field_id,
} }
@api.multi @api.multi
@ -374,118 +418,120 @@ class BiSQLView(models.Model):
self.ensure_one() self.ensure_one()
res = [] res = []
for group in self.group_ids: 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,
})
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 return res
@api.multi @api.multi
def _prepare_cron(self): def _prepare_cron(self):
now = datetime.now() now = datetime.now()
return { return {
'name': _('Refresh Materialized View %s') % self.view_name,
'user_id': SUPERUSER_ID,
'model_id': self.env['ir.model'].search([
('model', '=', self._name)], limit=1).id,
'state': 'code',
'code': 'model._refresh_materialized_view_cron(%s)' % self.ids,
'numbercall': -1,
'interval_number': 1,
'interval_type': 'days',
'nextcall': datetime(now.year, now.month, now.day+1),
'active': True,
"name": _("Refresh Materialized View %s") % self.view_name,
"user_id": SUPERUSER_ID,
"model_id": self.env["ir.model"]
.search([("model", "=", self._name)], limit=1)
.id,
"state": "code",
"code": "model._refresh_materialized_view_cron(%s)" % self.ids,
"numbercall": -1,
"interval_number": 1,
"interval_type": "days",
"nextcall": datetime(now.year, now.month, now.day + 1),
"active": True,
} }
@api.multi @api.multi
def _prepare_rule(self): def _prepare_rule(self):
self.ensure_one() self.ensure_one()
return { return {
'name': _('Access %s') % self.name,
'model_id': self.model_id.id,
'domain_force': self.domain_force,
'global': True,
"name": _("Access %s") % self.name,
"model_id": self.model_id.id,
"domain_force": self.domain_force,
"global": True,
} }
@api.multi @api.multi
def _prepare_tree_view(self): def _prepare_tree_view(self):
self.ensure_one() self.ensure_one()
return { return {
'name': self.name,
'type': 'tree',
'model': self.model_id.model,
'arch':
"""<?xml version="1.0"?>"""
"name": self.name,
"type": "tree",
"model": self.model_id.model,
"arch": """<?xml version="1.0"?>"""
"""<tree string="Analysis">{}""" """<tree string="Analysis">{}"""
"""</tree>""".format("".join(
[x._prepare_tree_field()
for x in self.bi_sql_view_field_ids]))
"""</tree>""".format(
"".join([x._prepare_tree_field() for x in self.bi_sql_view_field_ids])
),
} }
@api.multi @api.multi
def _prepare_graph_view(self): def _prepare_graph_view(self):
self.ensure_one() self.ensure_one()
return { return {
'name': self.name,
'type': 'graph',
'model': self.model_id.model,
'arch':
"""<?xml version="1.0"?>"""
"name": self.name,
"type": "graph",
"model": self.model_id.model,
"arch": """<?xml version="1.0"?>"""
"""<graph string="Analysis" type="bar" stacked="True">{}""" """<graph string="Analysis" type="bar" stacked="True">{}"""
"""</graph>""".format("".join(
[x._prepare_graph_field()
for x in self.bi_sql_view_field_ids]))
"""</graph>""".format(
"".join([x._prepare_graph_field() for x in self.bi_sql_view_field_ids])
),
} }
@api.multi @api.multi
def _prepare_pivot_view(self): def _prepare_pivot_view(self):
self.ensure_one() self.ensure_one()
return { return {
'name': self.name,
'type': 'pivot',
'model': self.model_id.model,
'arch':
"""<?xml version="1.0"?>"""
"name": self.name,
"type": "pivot",
"model": self.model_id.model,
"arch": """<?xml version="1.0"?>"""
"""<pivot string="Analysis" stacked="True">{}""" """<pivot string="Analysis" stacked="True">{}"""
"""</pivot>""".format("".join(
[x._prepare_pivot_field()
for x in self.bi_sql_view_field_ids]))
"""</pivot>""".format(
"".join([x._prepare_pivot_field() for x in self.bi_sql_view_field_ids])
),
} }
@api.multi @api.multi
def _prepare_search_view(self): def _prepare_search_view(self):
self.ensure_one() self.ensure_one()
return { return {
'name': self.name,
'type': 'search',
'model': self.model_id.model,
'arch':
"""<?xml version="1.0"?>"""
"name": self.name,
"type": "search",
"model": self.model_id.model,
"arch": """<?xml version="1.0"?>"""
"""<search string="Analysis">{}""" """<search string="Analysis">{}"""
"""<group expand="1" string="Group By">{}</group>""" """<group expand="1" string="Group By">{}</group>"""
"""</search>""".format( """</search>""".format(
"".join( "".join(
[x._prepare_search_field()
for x in self.bi_sql_view_field_ids]),
[x._prepare_search_field() for x in self.bi_sql_view_field_ids]
),
"".join( "".join(
[x._prepare_search_filter_field()
for x in self.bi_sql_view_field_ids]))
[
x._prepare_search_filter_field()
for x in self.bi_sql_view_field_ids
]
),
),
} }
@api.multi @api.multi
def _prepare_action(self): def _prepare_action(self):
self.ensure_one() self.ensure_one()
view_mode = self.view_order view_mode = self.view_order
first_view = view_mode.split(',')[0]
if first_view == 'tree':
first_view = view_mode.split(",")[0]
if first_view == "tree":
view_id = self.tree_view_id.id view_id = self.tree_view_id.id
elif first_view == 'pivot':
elif first_view == "pivot":
view_id = self.pivot_view_id.id view_id = self.pivot_view_id.id
else: else:
view_id = self.graph_view_id.id view_id = self.graph_view_id.id
@ -493,13 +539,13 @@ class BiSQLView(models.Model):
for k, v in safe_eval(self.action_context).items(): for k, v in safe_eval(self.action_context).items():
action[k] = v action[k] = v
return { return {
'name': self._prepare_action_name(),
'res_model': self.model_id.model,
'type': 'ir.actions.act_window',
'view_mode': view_mode,
'view_id': view_id,
'search_view_id': self.search_view_id.id,
'context': str(action),
"name": self._prepare_action_name(),
"res_model": self.model_id.model,
"type": "ir.actions.act_window",
"view_mode": view_mode,
"view_id": view_id,
"search_view_id": self.search_view_id.id,
"context": str(action),
} }
@api.multi @api.multi
@ -507,18 +553,19 @@ class BiSQLView(models.Model):
self.ensure_one() self.ensure_one()
if not self.is_materialized: if not self.is_materialized:
return self.name return self.name
return "%s (%s)" % (
return "{} ({})".format(
self.name, self.name,
datetime.utcnow().strftime(_("%m/%d/%Y %H:%M:%S UTC")))
datetime.utcnow().strftime(_("%m/%d/%Y %H:%M:%S UTC")),
)
@api.multi @api.multi
def _prepare_menu(self): def _prepare_menu(self):
self.ensure_one() self.ensure_one()
return { 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,
'sequence': self.sequence,
"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,
"sequence": self.sequence,
} }
# Custom Section # Custom Section
@ -530,8 +577,9 @@ class BiSQLView(models.Model):
def _drop_view(self): def _drop_view(self):
for sql_view in self: for sql_view in self:
self._log_execute( self._log_execute(
"DROP %s VIEW IF EXISTS %s" % (
sql_view.materialized_text, sql_view.view_name))
"DROP %s VIEW IF EXISTS %s"
% (sql_view.materialized_text, sql_view.view_name)
)
sql_view.size = False sql_view.size = False
@api.multi @api.multi
@ -542,29 +590,28 @@ class BiSQLView(models.Model):
self._log_execute(sql_view._prepare_request_for_execution()) self._log_execute(sql_view._prepare_request_for_execution())
sql_view._refresh_size() sql_view._refresh_size()
except ProgrammingError as e: 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))
raise UserError(
_("SQL Error while creating %s VIEW %s :\n %s")
% (sql_view.materialized_text, sql_view.view_name, str(e))
)
@api.multi @api.multi
def _create_index(self): def _create_index(self):
for sql_view in self: for sql_view in self:
for sql_field in sql_view.bi_sql_view_field_ids.filtered( for sql_field in sql_view.bi_sql_view_field_ids.filtered(
lambda x: x.is_index is True):
lambda x: x.is_index is True
):
self._log_execute( self._log_execute(
"CREATE INDEX %s ON %s (%s);" % (
sql_field.index_name, sql_view.view_name,
sql_field.name))
"CREATE INDEX %s ON %s (%s);"
% (sql_field.index_name, sql_view.view_name, sql_field.name)
)
@api.multi @api.multi
def _create_model_and_fields(self): def _create_model_and_fields(self):
for sql_view in self: for sql_view in self:
# Create model # 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
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 # Drop table, created by the ORM
if sql.table_exists(self._cr, sql_view.view_name): if sql.table_exists(self._cr, sql_view.view_name):
req = "DROP TABLE %s" % sql_view.view_name req = "DROP TABLE %s" % sql_view.view_name
@ -574,13 +621,14 @@ class BiSQLView(models.Model):
def _create_model_access(self): def _create_model_access(self):
for sql_view in self: for sql_view in self:
for item in sql_view._prepare_model_access(): for item in sql_view._prepare_model_access():
self.env['ir.model.access'].create(item)
self.env["ir.model.access"].create(item)
@api.multi @api.multi
def _drop_model_access(self): def _drop_model_access(self):
for sql_view in self: for sql_view in self:
self.env['ir.model.access'].search(
[('model_id', '=', sql_view.model_name)]).unlink()
self.env["ir.model.access"].search(
[("model_id", "=", sql_view.model_name)]
).unlink()
@api.multi @api.multi
def _drop_model_and_fields(self): def _drop_model_and_fields(self):
@ -593,7 +641,8 @@ class BiSQLView(models.Model):
@api.multi @api.multi
def _hook_executed_request(self): def _hook_executed_request(self):
self.ensure_one() self.ensure_one()
req = """
req = (
"""
SELECT attnum, SELECT attnum,
attname AS column, attname AS column,
format_type(atttypid, atttypmod) AS type format_type(atttypid, atttypmod) AS type
@ -601,19 +650,22 @@ class BiSQLView(models.Model):
WHERE attrelid = '%s'::regclass WHERE attrelid = '%s'::regclass
AND NOT attisdropped AND NOT attisdropped
AND attnum > 0 AND attnum > 0
ORDER BY attnum;""" % self.view_name
ORDER BY attnum;"""
% self.view_name
)
self._log_execute(req) self._log_execute(req)
return self.env.cr.fetchall() return self.env.cr.fetchall()
@api.multi @api.multi
def _prepare_request_check_execution(self): def _prepare_request_check_execution(self):
self.ensure_one() self.ensure_one()
return "CREATE VIEW %s AS (%s);" % (self.view_name, self.query)
return "CREATE VIEW {} AS ({});".format(self.view_name, self.query)
@api.multi @api.multi
def _prepare_request_for_execution(self): def _prepare_request_for_execution(self):
self.ensure_one() self.ensure_one()
query = """
query = (
"""
SELECT SELECT
CAST(row_number() OVER () as integer) AS id, CAST(row_number() OVER () as integer) AS id,
CAST(Null as timestamp without time zone) as create_date, CAST(Null as timestamp without time zone) as create_date,
@ -623,9 +675,14 @@ class BiSQLView(models.Model):
my_query.* my_query.*
FROM FROM
(%s) as my_query (%s) as my_query
""" % self.query
return "CREATE %s VIEW %s AS (%s);" % (
self.materialized_text, self.view_name, query)
"""
% self.query
)
return "CREATE {} VIEW {} AS ({});".format(
self.materialized_text,
self.view_name,
query,
)
@api.multi @api.multi
def _check_execution(self): def _check_execution(self):
@ -635,54 +692,59 @@ class BiSQLView(models.Model):
After the execution, and before the rollback, an analysis of After the execution, and before the rollback, an analysis of
the database structure is done, to know fields type.""" the database structure is done, to know fields type."""
self.ensure_one() self.ensure_one()
sql_view_field_obj = self.env['bi.sql.view.field']
sql_view_field_obj = self.env["bi.sql.view.field"]
columns = super(BiSQLView, self)._check_execution() columns = super(BiSQLView, self)._check_execution()
field_ids = [] field_ids = []
for column in columns: for column in columns:
existing_field = self.bi_sql_view_field_ids.filtered( existing_field = self.bi_sql_view_field_ids.filtered(
lambda x: x.name == column[1])
lambda x: x.name == column[1]
)
if existing_field: if existing_field:
# Update existing field # Update existing field
field_ids.append(existing_field.id) field_ids.append(existing_field.id)
existing_field.write({
'sequence': column[0],
'sql_type': column[2],
})
existing_field.write({"sequence": column[0], "sql_type": column[2]})
else: else:
# Create a new one if name is prefixed by x_ # 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)
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 # Drop obsolete view field
self.bi_sql_view_field_ids.filtered(
lambda x: x.id not in field_ids).unlink()
self.bi_sql_view_field_ids.filtered(lambda x: x.id not in field_ids).unlink()
if not self.bi_sql_view_field_ids: if not self.bi_sql_view_field_ids:
raise UserError(_(
"No Column was found.\n"
"Columns name should be prefixed by 'x_'."))
raise UserError(
_("No Column was found.\n" "Columns name should be prefixed by 'x_'.")
)
return columns return columns
@api.model @api.model
def _refresh_materialized_view_cron(self, view_ids): def _refresh_materialized_view_cron(self, view_ids):
sql_views = self.search([
('is_materialized', '=', True),
('state', 'in', ['model_valid', 'ui_valid']),
('id', 'in', view_ids),
])
sql_views = self.search(
[
("is_materialized", "=", True),
("state", "in", ["model_valid", "ui_valid"]),
("id", "in", view_ids),
]
)
return sql_views._refresh_materialized_view() return sql_views._refresh_materialized_view()
@api.multi @api.multi
def _refresh_materialized_view(self): def _refresh_materialized_view(self):
for sql_view in self.filtered(lambda x: x.is_materialized): for sql_view in self.filtered(lambda x: x.is_materialized):
req = "REFRESH %s VIEW %s" % (
sql_view.materialized_text, sql_view.view_name)
req = "REFRESH {} VIEW {}".format(
sql_view.materialized_text,
sql_view.view_name,
)
self._log_execute(req) self._log_execute(req)
sql_view._refresh_size() sql_view._refresh_size()
if sql_view.action_id: if sql_view.action_id:
@ -696,7 +758,8 @@ class BiSQLView(models.Model):
def _refresh_size(self): def _refresh_size(self):
for sql_view in self: for sql_view in self:
req = "SELECT pg_size_pretty(pg_total_relation_size('%s'));" % ( req = "SELECT pg_size_pretty(pg_total_relation_size('%s'));" % (
sql_view.view_name)
sql_view.view_name
)
self._log_execute(req) self._log_execute(req)
sql_view.size = self.env.cr.fetchone()[0] sql_view.size = self.env.cr.fetchone()[0]
@ -704,4 +767,4 @@ class BiSQLView(models.Model):
def button_preview_sql_expression(self): def button_preview_sql_expression(self):
self.button_validate_sql_expression() self.button_validate_sql_expression()
res = self._execute_sql_request() res = self._execute_sql_request()
raise UserError('\n'.join(map(lambda x: str(x), res[:100])))
raise UserError("\n".join(map(lambda x: str(x), res[:100])))

211
bi_sql_editor/models/bi_sql_view_field.py

@ -9,145 +9,165 @@ from odoo.exceptions import UserError
class BiSQLViewField(models.Model): class BiSQLViewField(models.Model):
_name = 'bi.sql.view.field'
_description = 'Bi SQL View Field'
_order = 'sequence'
_name = "bi.sql.view.field"
_description = "Bi SQL View Field"
_order = "sequence"
_TTYPE_SELECTION = [ _TTYPE_SELECTION = [
('boolean', 'boolean'),
('char', 'char'),
('date', 'date'),
('datetime', 'datetime'),
('float', 'float'),
('integer', 'integer'),
('many2one', 'many2one'),
('selection', 'selection'),
("boolean", "boolean"),
("char", "char"),
("date", "date"),
("datetime", "datetime"),
("float", "float"),
("integer", "integer"),
("many2one", "many2one"),
("selection", "selection"),
] ]
_GRAPH_TYPE_SELECTION = [ _GRAPH_TYPE_SELECTION = [
('col', 'Column'),
('row', 'Row'),
('measure', 'Measure'),
("col", "Column"),
("row", "Row"),
("measure", "Measure"),
] ]
_TREE_VISIBILITY_SELECTION = [ _TREE_VISIBILITY_SELECTION = [
('unavailable', 'Unavailable'),
('hidden', 'Hidden'),
('available', 'Available'),
("unavailable", "Unavailable"),
("hidden", "Hidden"),
("available", "Available"),
] ]
# Mapping to guess Odoo field type, from SQL column type # Mapping to guess Odoo field type, from SQL column type
_SQL_MAPPING = { _SQL_MAPPING = {
'boolean': 'boolean',
'bigint': 'integer',
'integer': 'integer',
'double precision': 'float',
'numeric': 'float',
'text': 'char',
'character varying': 'char',
'date': 'date',
'timestamp without time zone': 'datetime',
"boolean": "boolean",
"bigint": "integer",
"integer": "integer",
"double precision": "float",
"numeric": "float",
"text": "char",
"character varying": "char",
"date": "date",
"timestamp without time zone": "datetime",
} }
name = fields.Char(string='Name', required=True, readonly=True)
name = fields.Char(string="Name", required=True, readonly=True)
sql_type = fields.Char( sql_type = fields.Char(
string='SQL Type', required=True, readonly=True,
help="SQL Type in the database")
string="SQL Type", required=True, readonly=True, help="SQL Type in the database"
)
sequence = fields.Integer(string='sequence', required=True, readonly=True)
sequence = fields.Integer(string="sequence", required=True, readonly=True)
bi_sql_view_id = fields.Many2one( bi_sql_view_id = fields.Many2one(
string='SQL View', comodel_name='bi.sql.view', ondelete='cascade')
string="SQL View", comodel_name="bi.sql.view", ondelete="cascade"
)
is_index = fields.Boolean( is_index = fields.Boolean(
string='Is Index', help="Check this box if you want to create"
string="Is Index",
help="Check this box if you want to create"
" an index on that field. This is recommended for searchable and" " an index on that field. This is recommended for searchable and"
" groupable fields, to reduce duration")
" groupable fields, to reduce duration",
)
is_group_by = fields.Boolean( 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")
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')
index_name = fields.Char(string="Index Name", compute="_compute_index_name")
graph_type = fields.Selection(
string='Graph Type', selection=_GRAPH_TYPE_SELECTION)
graph_type = fields.Selection(string="Graph Type", selection=_GRAPH_TYPE_SELECTION)
tree_visibility = fields.Selection( tree_visibility = fields.Selection(
string='Tree Visibility', selection=_TREE_VISIBILITY_SELECTION,
default='available', required=True)
string="Tree Visibility",
selection=_TREE_VISIBILITY_SELECTION,
default="available",
required=True,
)
field_description = fields.Char( field_description = fields.Char(
string='Field Description', help="This will be used as the name"
" of the Odoo field, displayed for users")
string="Field Description",
help="This will be used as the name" " of the Odoo field, displayed for users",
)
ttype = fields.Selection( ttype = fields.Selection(
string='Field Type', selection=_TTYPE_SELECTION, help="Type of the"
string="Field Type",
selection=_TTYPE_SELECTION,
help="Type of the"
" Odoo field that will be created. Keep empty if you don't want to" " Odoo field that will be created. Keep empty if you don't want to"
" create a new field. If empty, this field will not be displayed" " create a new field. If empty, this field will not be displayed"
" neither available for search or group by function")
" neither available for search or group by function",
)
selection = fields.Text( selection = fields.Text(
string='Selection Options', default='[]',
string="Selection Options",
default="[]",
help="For 'Selection' Odoo field.\n" help="For 'Selection' Odoo field.\n"
" List of options, specified as a Python expression defining a list of" " List of options, specified as a Python expression defining a list of"
" (key, label) pairs. For example:" " (key, label) pairs. For example:"
" [('blue','Blue'), ('yellow','Yellow')]")
" [('blue','Blue'), ('yellow','Yellow')]",
)
many2one_model_id = fields.Many2one( many2one_model_id = fields.Many2one(
comodel_name='ir.model', string='Model',
help="For 'Many2one' Odoo field.\n"
" Comodel of the field.")
comodel_name="ir.model",
string="Model",
help="For 'Many2one' Odoo field.\n" " Comodel of the field.",
)
# Constrains Section # Constrains Section
@api.constrains('is_index')
@api.constrains("is_index")
@api.multi @api.multi
def _check_index_materialized(self): def _check_index_materialized(self):
for rec in self.filtered(lambda x: x.is_index): for rec in self.filtered(lambda x: x.is_index):
if not rec.bi_sql_view_id.is_materialized: if not rec.bi_sql_view_id.is_materialized:
raise UserError(_(
'You can not create indexes on non materialized views'))
raise UserError(
_("You can not create indexes on non materialized views")
)
# Compute Section # Compute Section
@api.multi @api.multi
def _compute_index_name(self): def _compute_index_name(self):
for sql_field in self: for sql_field in self:
sql_field.index_name = '%s_%s' % (
sql_field.bi_sql_view_id.view_name, sql_field.name)
sql_field.index_name = "{}_{}".format(
sql_field.bi_sql_view_id.view_name,
sql_field.name,
)
# Overload Section # Overload Section
@api.model @api.model
def create(self, vals): def create(self, vals):
field_without_prefix = vals['name'][2:]
field_without_prefix = vals["name"][2:]
# guess field description # guess field description
field_description = re.sub( field_description = re.sub(
r'\w+', lambda m: m.group(0).capitalize(),
field_without_prefix.replace('_id', '').replace('_', ' '))
r"\w+",
lambda m: m.group(0).capitalize(),
field_without_prefix.replace("_id", "").replace("_", " "),
)
# Guess ttype # Guess ttype
# Don't execute as simple .get() in the dict to manage # Don't execute as simple .get() in the dict to manage
# correctly the type 'character varying(x)' # correctly the type 'character varying(x)'
ttype = False ttype = False
for k, v in self._SQL_MAPPING.items(): for k, v in self._SQL_MAPPING.items():
if k in vals['sql_type']:
if k in vals["sql_type"]:
ttype = v ttype = v
# Guess many2one_model_id # Guess many2one_model_id
many2one_model_id = False 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,
})
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) return super(BiSQLViewField, self).create(vals)
# Custom Section # Custom Section
@ -157,8 +177,9 @@ class BiSQLViewField(models.Model):
field name. Sample : field name. Sample :
{'account_id': 'account.account'; 'product_id': 'product.product'} {'account_id': 'account.account'; 'product_id': 'product.product'}
""" """
relation_fields = self.env['ir.model.fields'].search([
('ttype', '=', 'many2one')])
relation_fields = self.env["ir.model.fields"].search(
[("ttype", "=", "many2one")]
)
res = {} res = {}
keys_to_pop = [] keys_to_pop = []
for field in relation_fields: for field in relation_fields:
@ -177,49 +198,49 @@ class BiSQLViewField(models.Model):
def _prepare_model_field(self): def _prepare_model_field(self):
self.ensure_one() self.ensure_one()
return { 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,
"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 @api.multi
def _prepare_tree_field(self): def _prepare_tree_field(self):
self.ensure_one() self.ensure_one()
res = ''
if self.field_description and self.tree_visibility != 'unavailable':
res = ""
if self.field_description and self.tree_visibility != "unavailable":
res = """<field name="{}" {}/>""".format( res = """<field name="{}" {}/>""".format(
self.name,
self.tree_visibility == 'hidden' and 'invisible="1"' or '')
self.name, self.tree_visibility == "hidden" and 'invisible="1"' or ""
)
return res return res
@api.multi @api.multi
def _prepare_graph_field(self): def _prepare_graph_field(self):
self.ensure_one() self.ensure_one()
res = ''
res = ""
if self.graph_type and self.field_description: if self.graph_type and self.field_description:
res = """<field name="{}" type="{}" />\n""".format( res = """<field name="{}" type="{}" />\n""".format(
self.name, self.graph_type)
self.name, self.graph_type
)
return res return res
@api.multi @api.multi
def _prepare_pivot_field(self): def _prepare_pivot_field(self):
self.ensure_one() self.ensure_one()
res = ''
res = ""
if self.field_description: if self.field_description:
graph_type_text =\
self.graph_type and "type=\"%s\"" % (self.graph_type) or ""
res = """<field name="{}" {} />\n""".format(
self.name, graph_type_text)
graph_type_text = self.graph_type and 'type="%s"' % (self.graph_type) or ""
res = """<field name="{}" {} />\n""".format(self.name, graph_type_text)
return res return res
@api.multi @api.multi
def _prepare_search_field(self): def _prepare_search_field(self):
self.ensure_one() self.ensure_one()
res = ''
res = ""
if self.field_description: if self.field_description:
res = """<field name="{}"/>\n""".format(self.name) res = """<field name="{}"/>\n""".format(self.name)
return res return res
@ -227,10 +248,12 @@ class BiSQLViewField(models.Model):
@api.multi @api.multi
def _prepare_search_filter_field(self): def _prepare_search_filter_field(self):
self.ensure_one() self.ensure_one()
res = ''
res = ""
if self.field_description and self.is_group_by: if self.field_description and self.is_group_by:
res = """<filter name="group_by_%s" string="%s" res = """<filter name="group_by_%s" string="%s"
context="{'group_by':'%s'}"/>\n""" % ( context="{'group_by':'%s'}"/>\n""" % (
self.name, self.field_description, self.name
self.name,
self.field_description,
self.name,
) )
return res return res

95
bi_sql_editor/tests/test_bi_sql_view.py

@ -1,91 +1,94 @@
# Copyright 2017 Onestein (<http://www.onestein.eu>) # Copyright 2017 Onestein (<http://www.onestein.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests.common import SingleTransactionCase, at_install, post_install
from odoo.exceptions import AccessError, UserError from odoo.exceptions import AccessError, UserError
from odoo.tests.common import SingleTransactionCase, at_install, post_install
@at_install(False) @at_install(False)
@post_install(True) @post_install(True)
class TestBiSqlViewEditor(SingleTransactionCase): class TestBiSqlViewEditor(SingleTransactionCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestBiSqlViewEditor, cls).setUpClass() super(TestBiSqlViewEditor, cls).setUpClass()
cls.res_partner = cls.env['res.partner']
cls.res_users = cls.env['res.users']
cls.bi_sql_view = cls.env['bi.sql.view']
cls.res_partner = cls.env["res.partner"]
cls.res_users = cls.env["res.users"]
cls.bi_sql_view = cls.env["bi.sql.view"]
cls.group_bi_user = cls.env.ref( cls.group_bi_user = cls.env.ref(
'sql_request_abstract.group_sql_request_manager')
cls.group_user = cls.env.ref(
'base.group_user')
cls.view = cls.bi_sql_view.create({
'name': 'Partners View 2',
'is_materialized': True,
'technical_name': 'partners_view_2',
'query': "SELECT name as x_name, street as x_street,"
"sql_request_abstract.group_sql_request_manager"
)
cls.group_user = cls.env.ref("base.group_user")
cls.view = cls.bi_sql_view.create(
{
"name": "Partners View 2",
"is_materialized": True,
"technical_name": "partners_view_2",
"query": "SELECT name as x_name, street as x_street,"
"company_id as x_company_id FROM res_partner " "company_id as x_company_id FROM res_partner "
"ORDER BY name"
})
cls.company = cls.env.ref('base.main_company')
"ORDER BY name",
}
)
cls.company = cls.env.ref("base.main_company")
# Create bi user # Create bi user
cls.bi_user = cls._create_user('bi_user', cls.group_bi_user,
cls.company)
cls.no_bi_user = cls._create_user('no_bi_user', cls.group_user,
cls.company)
cls.bi_user = cls._create_user("bi_user", cls.group_bi_user, cls.company)
cls.no_bi_user = cls._create_user("no_bi_user", cls.group_user, cls.company)
@classmethod @classmethod
def _create_user(cls, login, groups, company): def _create_user(cls, login, groups, company):
"""Create a user.""" """Create a user."""
user = cls.res_users.create({
'name': login,
'login': login,
'password': 'demo',
'email': 'example@yourcompany.com',
'company_id': company.id,
'groups_id': [(6, 0, groups.ids)]
})
user = cls.res_users.create(
{
"name": login,
"login": login,
"password": "demo",
"email": "example@yourcompany.com",
"company_id": company.id,
"groups_id": [(6, 0, groups.ids)],
}
)
return user return user
def test_process_view(self): def test_process_view(self):
view = self.view view = self.view
self.assertEqual(view.state, 'draft', 'state not draft')
self.assertEqual(view.state, "draft", "state not draft")
view.button_validate_sql_expression() view.button_validate_sql_expression()
self.assertEqual(view.state, 'sql_valid', 'state not sql_valid')
self.assertEqual(view.state, "sql_valid", "state not sql_valid")
view.button_create_sql_view_and_model() view.button_create_sql_view_and_model()
self.assertEqual(view.state, 'model_valid', 'state not model_valid')
self.assertEqual(view.state, "model_valid", "state not model_valid")
view.button_create_ui() view.button_create_ui()
self.assertEqual(view.state, 'ui_valid', 'state not ui_valid')
self.assertEqual(view.state, "ui_valid", "state not ui_valid")
view.button_update_model_access() view.button_update_model_access()
self.assertEqual(view.has_group_changed, False,
'has_group_changed not False')
self.assertEqual(view.has_group_changed, False, "has_group_changed not False")
cron_res = view.cron_id.method_direct_trigger() cron_res = view.cron_id.method_direct_trigger()
self.assertEqual(cron_res, True, 'something went wrong with the cron')
self.assertEqual(cron_res, True, "something went wrong with the cron")
def test_copy(self): def test_copy(self):
copy_view = self.view.copy() copy_view = self.view.copy()
self.assertEqual(
copy_view.name, 'Partners View 2 (Copy)', 'Wrong name')
self.assertEqual(copy_view.name, "Partners View 2 (Copy)", "Wrong name")
def test_security(self): def test_security(self):
with self.assertRaises(AccessError): with self.assertRaises(AccessError):
self.bi_sql_view.sudo(self.no_bi_user.id).search( self.bi_sql_view.sudo(self.no_bi_user.id).search(
[('name', '=', 'Partners View 2')])
[("name", "=", "Partners View 2")]
)
bi = self.bi_sql_view.sudo(self.bi_user.id).search( bi = self.bi_sql_view.sudo(self.bi_user.id).search(
[('name', '=', 'Partners View 2')])
self.assertEqual(len(bi), 1, 'Bi user should not have access to '
'bi %s' % self.view.name)
[("name", "=", "Partners View 2")]
)
self.assertEqual(
len(bi), 1, "Bi user should not have access to " "bi %s" % self.view.name
)
def test_unlink(self): def test_unlink(self):
self.assertEqual(self.view.state, 'ui_valid', 'state not ui_valid')
self.assertEqual(self.view.state, "ui_valid", "state not ui_valid")
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.view.unlink() self.view.unlink()
self.view.button_set_draft() self.view.button_set_draft()
self.assertNotEqual( self.assertNotEqual(
self.view.cron_id, False, 'Set to draft materialized view should'
' not unlink cron'
self.view.cron_id,
False,
"Set to draft materialized view should" " not unlink cron",
) )
self.view.unlink() self.view.unlink()
res = self.bi_sql_view.search([('name', '=', 'Partners View 2')])
self.assertEqual(len(res), 0, 'View not deleted')
res = self.bi_sql_view.search([("name", "=", "Partners View 2")])
self.assertEqual(len(res), 0, "View not deleted")

3
bi_sql_editor/views/action.xml

@ -4,9 +4,7 @@ Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain) @author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
--> -->
<odoo> <odoo>
<record id="action_bi_sql_view" model="ir.actions.act_window"> <record id="action_bi_sql_view" model="ir.actions.act_window">
<field name="name">SQL Views</field> <field name="name">SQL Views</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
@ -14,5 +12,4 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
</record> </record>
</odoo> </odoo>

16
bi_sql_editor/views/menu.xml

@ -4,19 +4,19 @@ Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain) @author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
--> -->
<odoo> <odoo>
<!-- Menu that will contain all the SQL report generated by this module --> <!-- Menu that will contain all the SQL report generated by this module -->
<menuitem id="menu_bi_sql_editor"
<menuitem
id="menu_bi_sql_editor"
name="SQL Reports" name="SQL Reports"
parent="base.menu_board_root" parent="base.menu_board_root"
groups="sql_request_abstract.group_sql_request_user" groups="sql_request_abstract.group_sql_request_user"
sequence="0"/>
<menuitem id="menu_bi_sql_view"
sequence="0"
/>
<menuitem
id="menu_bi_sql_view"
parent="base.next_id_9" parent="base.next_id_9"
groups="sql_request_abstract.group_sql_request_manager" groups="sql_request_abstract.group_sql_request_manager"
action="action_bi_sql_view"/>
action="action_bi_sql_view"
/>
</odoo> </odoo>

192
bi_sql_editor/views/view_bi_sql_view.xml

@ -4,13 +4,14 @@ Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain) @author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
--> -->
<odoo> <odoo>
<record id="view_bi_sql_view_tree" model="ir.ui.view"> <record id="view_bi_sql_view_tree" model="ir.ui.view">
<field name="model">bi.sql.view</field> <field name="model">bi.sql.view</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree decoration-info="state=='draft'" decoration-warning="state in ('sql_valid', 'model_valid')">
<tree
decoration-info="state=='draft'"
decoration-warning="state in ('sql_valid', 'model_valid')"
>
<field name="sequence" widget="handle" /> <field name="sequence" widget="handle" />
<field name="name" /> <field name="name" />
<field name="technical_name" /> <field name="technical_name" />
@ -19,81 +20,172 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
</tree> </tree>
</field> </field>
</record> </record>
<record id="view_bi_sql_view_form" model="ir.ui.view"> <record id="view_bi_sql_view_form" model="ir.ui.view">
<field name="model">bi.sql.view</field> <field name="model">bi.sql.view</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header> <header>
<button name="button_validate_sql_expression" type="object" states="draft"
string="Validate SQL Expression" class="oe_highlight"/>
<button name="button_set_draft" type="object" states="sql_valid"
string="Set to Draft" groups="sql_request_abstract.group_sql_request_manager"/>
<button name="button_set_draft" type="object" states="model_valid,ui_valid"
string="Set to Draft" groups="sql_request_abstract.group_sql_request_manager"
confirm="Are you sure you want to set to draft this SQL View. It will delete the materialized view, and all the previous mapping realized with the columns"/>
<button name="button_preview_sql_expression" type="object" states="draft" string="Preview SQL Expression" />
<button name="button_create_sql_view_and_model" type="object" states="sql_valid"
string="Create SQL View, Indexes and Models" class="oe_highlight"
help="This will try to create an SQL View, based on the SQL request and the according Transient Model and fields, based on settings"/>
<button name="button_update_model_access" type="object"
<button
name="button_validate_sql_expression"
type="object"
states="draft"
string="Validate SQL Expression"
class="oe_highlight"
/>
<button
name="button_set_draft"
type="object"
states="sql_valid"
string="Set to Draft"
groups="sql_request_abstract.group_sql_request_manager"
/>
<button
name="button_set_draft"
type="object"
states="model_valid,ui_valid"
string="Set to Draft"
groups="sql_request_abstract.group_sql_request_manager"
confirm="Are you sure you want to set to draft this SQL View. It will delete the materialized view, and all the previous mapping realized with the columns"
/>
<button
name="button_preview_sql_expression"
type="object"
states="draft"
string="Preview SQL Expression"
/>
<button
name="button_create_sql_view_and_model"
type="object"
states="sql_valid"
string="Create SQL View, Indexes and Models"
class="oe_highlight"
help="This will try to create an SQL View, based on the SQL request and the according Transient Model and fields, based on settings"
/>
<button
name="button_update_model_access"
type="object"
attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('has_group_changed', '=', False)]}" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('has_group_changed', '=', False)]}"
string="Update Model Access" class="oe_highlight"
help="Update Model Access. Required if you changed groups list after having created the model"/>
<button name="button_create_ui" type="object" states="model_valid" string="Create UI"
class="oe_highlight" help="This will create Odoo View, Action and Menu"/>
<button name="button_refresh_materialized_view" type="object" string="Refresh Materialized View"
string="Update Model Access"
class="oe_highlight"
help="Update Model Access. Required if you changed groups list after having created the model"
/>
<button
name="button_create_ui"
type="object"
states="model_valid"
string="Create UI"
class="oe_highlight"
help="This will create Odoo View, Action and Menu"
/>
<button
name="button_refresh_materialized_view"
type="object"
string="Refresh Materialized View"
attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('is_materialized', '=', False)]}" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('is_materialized', '=', False)]}"
help="this will refresh the materialized view"/>
<button name="button_open_view" type="object" string="Open View" states="ui_valid" class="oe_highlight" />
help="this will refresh the materialized view"
/>
<button
name="button_open_view"
type="object"
string="Open View"
states="ui_valid"
class="oe_highlight"
/>
<field name="state" widget="statusbar" /> <field name="state" widget="statusbar" />
</header> </header>
<sheet> <sheet>
<h1> <h1>
<field name="name" attrs="{'readonly': [('state','!=','draft')]}" colspan="4"/>
<field
name="name"
attrs="{'readonly': [('state','!=','draft')]}"
colspan="4"
/>
</h1> </h1>
<group> <group>
<group> <group>
<group> <group>
<field name="technical_name" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field
name="technical_name"
attrs="{'readonly': [('state', '!=', 'draft')]}"
/>
<field name="view_name" /> <field name="view_name" />
<field name="view_order" /> <field name="view_order" />
<field name="is_materialized" /> <field name="is_materialized" />
<field name="size"
attrs="{'invisible': ['|', ('state', '=', 'draft'), ('is_materialized', '=', False)]}"/>
<field name="cron_id"
attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('is_materialized', '=', False)]}"/>
<field
name="size"
attrs="{'invisible': ['|', ('state', '=', 'draft'), ('is_materialized', '=', False)]}"
/>
<field
name="cron_id"
attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('is_materialized', '=', False)]}"
/>
</group> </group>
</group> </group>
</group> </group>
<notebook> <notebook>
<page string="SQL Query"> <page string="SQL Query">
<field name="query" nolabel="1" colspan="4" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field
name="query"
nolabel="1"
colspan="4"
attrs="{'readonly': [('state', '!=', 'draft')]}"
/>
</page> </page>
<page string="SQL Fields" attrs="{'invisible': [('state', '=', 'draft')]}">
<field name="bi_sql_view_field_ids" nolabel="1" colspan="4" attrs="{'readonly': [('state', '!=', 'sql_valid')]}">
<tree editable="bottom" decoration-info="field_description==False">
<page
string="SQL Fields"
attrs="{'invisible': [('state', '=', 'draft')]}"
>
<field
name="bi_sql_view_field_ids"
nolabel="1"
colspan="4"
attrs="{'readonly': [('state', '!=', 'sql_valid')]}"
>
<tree
editable="bottom"
decoration-info="field_description==False"
>
<field name="sequence" /> <field name="sequence" />
<field name="name" /> <field name="name" />
<field name="sql_type" /> <field name="sql_type" />
<field name="field_description" /> <field name="field_description" />
<field name="ttype" attrs="{
'required': [('field_description', '!=', False)]}"/>
<field name="many2one_model_id" attrs="{
<field
name="ttype"
attrs="{
'required': [('field_description', '!=', False)]}"
/>
<field
name="many2one_model_id"
attrs="{
'invisible': [('ttype', '!=', 'many2one')], 'invisible': [('ttype', '!=', 'many2one')],
'required': [ 'required': [
('field_description', '!=', False), ('field_description', '!=', False),
('ttype', '=', 'many2one')]}"/>
<field name="selection" attrs="{
('ttype', '=', 'many2one')]}"
/>
<field
name="selection"
attrs="{
'invisible': [('ttype', '!=', 'selection')], 'invisible': [('ttype', '!=', 'selection')],
'required': [ 'required': [
('field_description', '!=', False), ('field_description', '!=', False),
('ttype', '=', 'selection')]}"/>
<field name="is_index" attrs="{'invisible': [('field_description', '=', False)]}"/>
<field name="is_group_by" attrs="{'invisible': [('field_description', '=', False)]}"/>
<field name="graph_type" attrs="{'invisible': [('field_description', '=', False)]}"/>
<field name="tree_visibility" attrs="{'invisible': [('field_description', '=', False)]}"/>
('ttype', '=', 'selection')]}"
/>
<field
name="is_index"
attrs="{'invisible': [('field_description', '=', False)]}"
/>
<field
name="is_group_by"
attrs="{'invisible': [('field_description', '=', False)]}"
/>
<field
name="graph_type"
attrs="{'invisible': [('field_description', '=', False)]}"
/>
<field
name="tree_visibility"
attrs="{'invisible': [('field_description', '=', False)]}"
/>
</tree> </tree>
</field> </field>
</page> </page>
@ -108,7 +200,11 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
</page> </page>
<page string="Action Settings"> <page string="Action Settings">
<group string="Computed Context"> <group string="Computed Context">
<field name="computed_action_context" nolabel="1" colspan="4"/>
<field
name="computed_action_context"
nolabel="1"
colspan="4"
/>
</group> </group>
<group string="Custom Context"> <group string="Custom Context">
<field name="action_context" nolabel="1" colspan="4" /> <field name="action_context" nolabel="1" colspan="4" />
@ -118,7 +214,10 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
<group> <group>
<group string="Model"> <group string="Model">
<field name="model_name" /> <field name="model_name" />
<field name="model_id" attrs="{'invisible': [('state', '=', 'draft')]}"/>
<field
name="model_id"
attrs="{'invisible': [('state', '=', 'draft')]}"
/>
</group> </group>
<group string="User Interface"> <group string="User Interface">
<field name="tree_view_id" /> <field name="tree_view_id" />
@ -135,5 +234,4 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
</form> </form>
</field> </field>
</record> </record>
</odoo> </odoo>

1
setup/bi_sql_editor/odoo/addons/bi_sql_editor

@ -0,0 +1 @@
../../../../bi_sql_editor

6
setup/bi_sql_editor/setup.py

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
Loading…
Cancel
Save