diff --git a/bi_sql_editor/demo/bi_sql_view_demo.xml b/bi_sql_editor/demo/bi_sql_view_demo.xml
index d9cc095b6..669cd38e3 100644
--- a/bi_sql_editor/demo/bi_sql_view_demo.xml
+++ b/bi_sql_editor/demo/bi_sql_view_demo.xml
@@ -20,6 +20,7 @@ ORDER BY unexisting_field
Partners View
partners_view
+
-
-
-
diff --git a/bi_sql_editor/models/bi_sql_view.py b/bi_sql_editor/models/bi_sql_view.py
index 53026da22..f8f08282d 100644
--- a/bi_sql_editor/models/bi_sql_view.py
+++ b/bi_sql_editor/models/bi_sql_view.py
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
from psycopg2 import ProgrammingError
from odoo import SUPERUSER_ID, _, api, fields, models
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, ValidationError
from odoo.tools import sql, table_columns
from odoo.tools.safe_eval import safe_eval
@@ -169,12 +169,6 @@ class BiSQLView(models.Model):
sequence = fields.Integer(string="sequence")
- option_context_field = fields.Boolean(
- string="Use Context Field",
- help="Check this box if you want to add a context column in the field list view."
- " Custom Context will be inserted in the created views.",
- )
-
# Constrains Section
@api.constrains("is_materialized")
def _check_index_materialized(self):
@@ -269,17 +263,24 @@ class BiSQLView(models.Model):
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,
- }
- )
+ if "name" not in default:
+ default["name"] = _("%s (Copy)") % self.name
+ if "technical_name" not in default:
+ default["technical_name"] = f"{self.technical_name}_copy"
return super().copy(default=default)
# Action Section
def button_create_sql_view_and_model(self):
for sql_view in self.filtered(lambda x: x.state == "sql_valid"):
+ # Check if many2one fields are correctly
+ bad_fields = sql_view.bi_sql_view_field_ids.filtered(
+ lambda x: x.ttype == "many2one" and not x.many2one_model_id.id
+ )
+ if bad_fields:
+ raise ValidationError(
+ _("Please set related models on the following fields %s")
+ % ",".join(bad_fields.mapped("name"))
+ )
# Create ORM and access
sql_view._create_model_and_fields()
sql_view._create_model_access()
@@ -297,28 +298,33 @@ class BiSQLView(models.Model):
sql_view.cron_id.active = True
sql_view.state = "model_valid"
+ def button_reset_to_model_valid(self):
+ views = self.filtered(lambda x: x.state == "ui_valid")
+ views.mapped("tree_view_id").unlink()
+ views.mapped("graph_view_id").unlink()
+ views.mapped("pivot_view_id").unlink()
+ views.mapped("search_view_id").unlink()
+ views.mapped("action_id").unlink()
+ views.mapped("menu_id").unlink()
+ return views.write({"state": "model_valid"})
+
+ def button_reset_to_sql_valid(self):
+ self.button_reset_to_model_valid()
+ views = self.filtered(lambda x: x.state == "model_valid")
+ for sql_view in views:
+ # Drop SQL View (and indexes by cascade)
+ if sql_view.is_materialized:
+ sql_view._drop_view()
+ if sql_view.cron_id:
+ sql_view.cron_id.active = False
+ # Drop ORM
+ sql_view._drop_model_and_fields()
+ return views.write({"state": "sql_valid"})
+
def button_set_draft(self):
- for sql_view in self.filtered(lambda x: x.state != "draft"):
- sql_view.menu_id.unlink()
- sql_view.action_id.unlink()
- sql_view.tree_view_id.unlink()
- sql_view.graph_view_id.unlink()
- sql_view.pivot_view_id.unlink()
- sql_view.search_view_id.unlink()
-
- if sql_view.state in ("model_valid", "ui_valid"):
- # Drop SQL View (and indexes by cascade)
- if sql_view.is_materialized:
- sql_view._drop_view()
-
- if sql_view.cron_id:
- sql_view.cron_id.active = False
-
- # Drop ORM
- sql_view._drop_model_and_fields()
-
- super(BiSQLView, sql_view).button_set_draft()
- return True
+ self.button_reset_to_model_valid()
+ self.button_reset_to_sql_valid()
+ return super().button_set_draft()
def button_create_ui(self):
self.tree_view_id = self.env["ir.ui.view"].create(self._prepare_tree_view()).id
diff --git a/bi_sql_editor/models/bi_sql_view_field.py b/bi_sql_editor/models/bi_sql_view_field.py
index 2a89e291f..7444d3463 100644
--- a/bi_sql_editor/models/bi_sql_view_field.py
+++ b/bi_sql_editor/models/bi_sql_view_field.py
@@ -70,31 +70,40 @@ class BiSQLViewField(models.Model):
string="SQL View", comodel_name="bi.sql.view", ondelete="cascade"
)
+ state = fields.Selection(related="bi_sql_view_id.state", store=True)
+
is_index = fields.Boolean(
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",
+ states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]},
)
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",
+ states={"ui_valid": [("readonly", True)]},
)
index_name = fields.Char(compute="_compute_index_name")
- graph_type = fields.Selection(selection=_GRAPH_TYPE_SELECTION)
+ graph_type = fields.Selection(
+ selection=_GRAPH_TYPE_SELECTION,
+ states={"ui_valid": [("readonly", True)]},
+ )
tree_visibility = fields.Selection(
selection=_TREE_VISIBILITY_SELECTION,
default="available",
required=True,
+ states={"ui_valid": [("readonly", True)]},
)
field_description = fields.Char(
help="This will be used as the name of the Odoo field, displayed for users",
required=True,
+ states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]},
)
ttype = fields.Selection(
@@ -104,6 +113,7 @@ class BiSQLViewField(models.Model):
" 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"
" neither available for search or group by function",
+ states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]},
)
selection = fields.Text(
@@ -113,24 +123,28 @@ class BiSQLViewField(models.Model):
" List of options, specified as a Python expression defining a list of"
" (key, label) pairs. For example:"
" [('blue','Blue'), ('yellow','Yellow')]",
+ states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]},
)
many2one_model_id = fields.Many2one(
comodel_name="ir.model",
string="Model",
help="For 'Many2one' Odoo field.\n" " Comodel of the field.",
+ states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]},
)
group_operator = fields.Selection(
selection=_GROUP_OPERATOR_SELECTION,
help="By default, Odoo will sum the values when grouping. If you wish "
"to alter the behaviour, choose an alternate Group Operator",
+ states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]},
)
field_context = fields.Char(
default="{}",
help="Context value that will be inserted for this field in all the views."
" Important note : please write a context with single quote.",
+ states={"ui_valid": [("readonly", True)]},
)
# Constrains Section
@@ -188,6 +202,16 @@ class BiSQLViewField(models.Model):
)
return super().create(vals_list)
+ def unlink(self):
+ if self.filtered(lambda x: x.state in ("model_valid", "ui_valid")):
+ raise UserError(
+ _(
+ "Impossible to delete fields if the view"
+ " is in the state 'Model Valid' or 'UI Valid'."
+ )
+ )
+ return super().unlink()
+
# Custom Section
@api.model
def _model_mapping(self):
@@ -233,12 +257,12 @@ class BiSQLViewField(models.Model):
if self.tree_visibility == "invisible":
visibility_text = 'invisible="1"'
elif self.tree_visibility == "optional_hide":
- visibility_text = 'option="hide"'
+ visibility_text = 'optional="hide"'
elif self.tree_visibility == "optional_show":
- visibility_text = 'option="show"'
+ visibility_text = 'optional="show"'
return (
- f"""\n"""
)
diff --git a/bi_sql_editor/tests/test_bi_sql_view.py b/bi_sql_editor/tests/test_bi_sql_view.py
index 02622d331..d7999ebfa 100644
--- a/bi_sql_editor/tests/test_bi_sql_view.py
+++ b/bi_sql_editor/tests/test_bi_sql_view.py
@@ -1,7 +1,7 @@
# Copyright 2017 Onestein ()
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-from odoo.exceptions import AccessError, UserError
+from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests import tagged
from odoo.tests.common import SingleTransactionCase
@@ -10,85 +10,107 @@ from odoo.tests.common import SingleTransactionCase
class TestBiSqlViewEditor(SingleTransactionCase):
@classmethod
def setUpClass(cls):
- super(TestBiSqlViewEditor, cls).setUpClass()
+ super().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.group_bi_user = cls.env.ref(
+ cls.group_bi_manager = 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,"
- "company_id as x_company_id FROM res_partner "
- "ORDER BY name",
- }
- )
- cls.company = cls.env.ref("base.main_company")
- # 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.group_bi_no_access = cls.env.ref("base.group_user")
+ cls.demo_user = cls.env.ref("base.user_demo")
+ cls.view = cls.env.ref("bi_sql_editor.partner_sql_view")
@classmethod
- def _create_user(cls, login, groups, company):
- """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)],
- }
- )
- return user
+ def _get_user(cls, access_level=False):
+ if access_level == "manager":
+ cls.demo_user.write({"groups_id": [(6, 0, cls.group_bi_manager.ids)]})
+ else:
+ cls.demo_user.write({"groups_id": [(6, 0, cls.group_bi_no_access.ids)]})
+ return cls.demo_user
def test_process_view(self):
- view = self.view
- self.assertEqual(view.state, "draft", "state not draft")
- view.button_validate_sql_expression()
- self.assertEqual(view.state, "sql_valid", "state not sql_valid")
- view.button_create_sql_view_and_model()
- self.assertEqual(view.state, "model_valid", "state not model_valid")
- view.button_create_ui()
- self.assertEqual(view.state, "ui_valid", "state not ui_valid")
- view.button_update_model_access()
- self.assertEqual(view.has_group_changed, False, "has_group_changed not False")
- cron_res = view.cron_id.method_direct_trigger()
- self.assertEqual(cron_res, True, "something went wrong with the cron")
+ copy_view = self.view.copy(default={"technical_name": "test_process_view"})
+ self.assertEqual(copy_view.state, "draft")
+ copy_view.button_validate_sql_expression()
+ self.assertEqual(copy_view.state, "sql_valid")
+
+ field_lines = copy_view.bi_sql_view_field_ids
+ self.assertEqual(len(field_lines), 3)
+ field_lines.filtered(lambda x: x.name == "x_company_id").is_index = True
+
+ copy_view.button_create_sql_view_and_model()
+ self.assertEqual(copy_view.state, "model_valid")
+
+ field_lines.filtered(lambda x: x.name == "x_name").tree_visibility = "invisible"
+ field_lines.filtered(
+ lambda x: x.name == "x_street"
+ ).tree_visibility = "optional_hide"
+ field_lines.filtered(
+ lambda x: x.name == "x_company_id"
+ ).tree_visibility = "optional_show"
+
+ field_lines.filtered(lambda x: x.name == "x_company_id").is_group_by = True
+
+ field_lines.filtered(lambda x: x.name == "x_company_id").graph_type = "row"
+
+ copy_view.button_create_ui()
+ self.assertEqual(copy_view.state, "ui_valid")
+ copy_view.button_update_model_access()
+ self.assertEqual(copy_view.has_group_changed, False)
+ # Check that cron works correctly
+ copy_view.cron_id.method_direct_trigger()
def test_copy(self):
- copy_view = self.view.copy()
- self.assertEqual(copy_view.name, "Partners View 2 (Copy)", "Wrong name")
+ copy_view = self.view.copy(default={"technical_name": "test_copy"})
+ self.assertEqual(copy_view.name, f"{self.view.name} (Copy)")
def test_security(self):
with self.assertRaises(AccessError):
- self.bi_sql_view.with_user(self.no_bi_user.id).search(
- [("name", "=", "Partners View 2")]
+ self.bi_sql_view.with_user(self._get_user()).search(
+ [("name", "=", self.view.name)]
)
- bi = self.bi_sql_view.with_user(self.bi_user.id).search(
- [("name", "=", "Partners View 2")]
+ bi = self.bi_sql_view.with_user(self._get_user("manager")).search(
+ [("name", "=", self.view.name)]
)
self.assertEqual(
- len(bi), 1, "Bi user should not have access to " "bi %s" % self.view.name
+ len(bi), 1, "Bi Manager should have access to bi %s" % self.view.name
)
def test_unlink(self):
- self.assertEqual(self.view.state, "ui_valid", "state not ui_valid")
- with self.assertRaises(UserError):
- self.view.unlink()
- self.view.button_set_draft()
- self.assertNotEqual(
- self.view.cron_id,
- False,
- "Set to draft materialized view should" " not unlink cron",
+ copy_view = self.view.copy(
+ default={
+ "name": "Test Unlink",
+ "technical_name": "test_unlink",
+ }
)
- self.view.unlink()
- res = self.bi_sql_view.search([("name", "=", "Partners View 2")])
+ view_name = copy_view.name
+ copy_view.button_validate_sql_expression()
+ copy_view.button_create_sql_view_and_model()
+ copy_view.button_create_ui()
+ self.assertEqual(copy_view.state, "ui_valid")
+ with self.assertRaises(UserError):
+ copy_view.unlink()
+ copy_view.button_set_draft()
+ self.assertNotEqual(
+ copy_view.cron_id,
+ False,
+ "Set to draft materialized view should not unlink cron",
+ )
+ copy_view.unlink()
+ res = self.bi_sql_view.search([("name", "=", view_name)])
self.assertEqual(len(res), 0, "View not deleted")
+
+ def test_many2one_not_found(self):
+ copy_view = self.view.copy(
+ default={"technical_name": "test_many2one_not_found"}
+ )
+
+ copy_view.query = "SELECT parent_id as x_weird_name_id FROM res_partner;"
+ copy_view.button_validate_sql_expression()
+ field_lines = copy_view.bi_sql_view_field_ids
+ self.assertEqual(len(field_lines), 1)
+ self.assertEqual(field_lines[0].ttype, "many2one")
+ self.assertEqual(field_lines[0].many2one_model_id.id, False)
+
+ with self.assertRaises(ValidationError):
+ copy_view.button_create_sql_view_and_model()
diff --git a/bi_sql_editor/views/view_bi_sql_view.xml b/bi_sql_editor/views/view_bi_sql_view.xml
index 207440b4e..4f82187f6 100644
--- a/bi_sql_editor/views/view_bi_sql_view.xml
+++ b/bi_sql_editor/views/view_bi_sql_view.xml
@@ -34,14 +34,20 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
-
-
+
@@ -98,7 +104,6 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
name="cron_id"
attrs="{'invisible': ['|', ('state', 'in', ('draft', 'sql_valid')), ('is_materialized', '=', False)]}"
/>
-
@@ -106,16 +111,8 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
string="SQL Fields"
attrs="{'invisible': [('state', '=', 'draft')]}"
>
-
-
+
+
@@ -132,9 +129,8 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
name="selection"
attrs="{
'invisible': [('ttype', '!=', 'selection')],
- 'required': [
- ('field_description', '!=', False),
- ('ttype', '=', 'selection')]}"
+ 'required': [('ttype', '=', 'selection')],
+ }"
/>
-
-
-
-
-
+
+
+
+
+
+