6
6
from datetime import datetime , timedelta
7
7
8
8
from psycopg2 import ProgrammingError
9
+ from psycopg2 .sql import SQL , Identifier
9
10
10
11
from odoo import SUPERUSER_ID , _ , api , fields , models
11
12
from odoo .exceptions import UserError , ValidationError
12
- from odoo .tools import sql , table_columns
13
+ from odoo .tools import sql
13
14
from odoo .tools .safe_eval import safe_eval
14
15
15
16
_logger = logging .getLogger (__name__ )
@@ -44,14 +45,12 @@ class BiSQLView(models.Model):
44
45
45
46
view_name = fields .Char (
46
47
compute = "_compute_view_name" ,
47
- readonly = True ,
48
48
store = True ,
49
49
help = "Full name of the SQL view" ,
50
50
)
51
51
52
52
model_name = fields .Char (
53
53
compute = "_compute_model_name" ,
54
- readonly = True ,
55
54
store = True ,
56
55
help = "Full Qualified Name of the transient model that will" " be created." ,
57
56
)
@@ -65,16 +64,15 @@ class BiSQLView(models.Model):
65
64
66
65
size = fields .Char (
67
66
string = "Database Size" ,
68
- readonly = True ,
69
67
help = "Size of the materialized view and its indexes" ,
70
68
)
71
69
72
70
state = fields .Selection (selection_add = _STATE_SQL_EDITOR )
73
71
74
72
view_order = fields .Char (
75
73
required = True ,
76
- default = "pivot,graph,tree " ,
77
- help = "Comma-separated text. Possible values:" ' "graph", "pivot" or "tree "' ,
74
+ default = "pivot,graph,list " ,
75
+ help = "Comma-separated text. Possible values:" ' "graph", "pivot" or "list "' ,
78
76
)
79
77
80
78
query = fields .Text (
@@ -111,9 +109,7 @@ class BiSQLView(models.Model):
111
109
inverse_name = "bi_sql_view_id" ,
112
110
)
113
111
114
- model_id = fields .Many2one (
115
- string = "Odoo Model" , comodel_name = "ir.model" , readonly = True
116
- )
112
+ model_id = fields .Many2one (string = "Odoo Model" , comodel_name = "ir.model" )
117
113
# UI related fields
118
114
# 1. Editable fields, which can be set by the user (optional) before
119
115
# creating the UI elements
@@ -133,39 +129,30 @@ def _default_parent_menu_id(self):
133
129
)
134
130
135
131
# 2. Readonly fields, non editable by the user
136
- tree_view_id = fields .Many2one (
137
- string = "Odoo Tree View" , comodel_name = "ir.ui.view" , readonly = True
138
- )
132
+ tree_view_id = fields .Many2one (string = "Odoo List View" , comodel_name = "ir.ui.view" )
139
133
140
- graph_view_id = fields .Many2one (
141
- string = "Odoo Graph View" , comodel_name = "ir.ui.view" , readonly = True
142
- )
134
+ graph_view_id = fields .Many2one (string = "Odoo Graph View" , comodel_name = "ir.ui.view" )
143
135
144
- pivot_view_id = fields .Many2one (
145
- string = "Odoo Pivot View" , comodel_name = "ir.ui.view" , readonly = True
146
- )
136
+ pivot_view_id = fields .Many2one (string = "Odoo Pivot View" , comodel_name = "ir.ui.view" )
147
137
148
138
search_view_id = fields .Many2one (
149
- string = "Odoo Search View" , comodel_name = "ir.ui.view" , readonly = True
139
+ string = "Odoo Search View" , comodel_name = "ir.ui.view"
150
140
)
151
141
152
142
action_id = fields .Many2one (
153
- string = "Odoo Action" , comodel_name = "ir.actions.act_window" , readonly = True
143
+ string = "Odoo Action" , comodel_name = "ir.actions.act_window"
154
144
)
155
145
156
- menu_id = fields .Many2one (
157
- string = "Odoo Menu" , comodel_name = "ir.ui.menu" , readonly = True
158
- )
146
+ menu_id = fields .Many2one (string = "Odoo Menu" , comodel_name = "ir.ui.menu" )
159
147
160
148
cron_id = fields .Many2one (
161
149
string = "Odoo Cron" ,
162
150
comodel_name = "ir.cron" ,
163
- readonly = True ,
164
151
help = "Cron Task that will refresh the materialized view" ,
165
152
ondelete = "cascade" ,
166
153
)
167
154
168
- rule_id = fields .Many2one (string = "Odoo Rule" , comodel_name = "ir.rule" , readonly = True )
155
+ rule_id = fields .Many2one (string = "Odoo Rule" , comodel_name = "ir.rule" )
169
156
170
157
sequence = fields .Integer (string = "sequence" )
171
158
@@ -183,9 +170,9 @@ def _check_view_order(self):
183
170
for rec in self :
184
171
if rec .view_order :
185
172
for vtype in rec .view_order .split ("," ):
186
- if vtype not in ("graph" , "pivot" , "tree " ):
173
+ if vtype not in ("graph" , "pivot" , "list " ):
187
174
raise UserError (
188
- _ ("Only graph, pivot or tree views are supported" )
175
+ _ ("Only graph, pivot or list views are supported" )
189
176
)
190
177
191
178
# Compute Section
@@ -244,15 +231,15 @@ def write(self, vals):
244
231
rec .menu_id .sequence = rec .sequence
245
232
return res
246
233
247
- def unlink (self ):
234
+ @api .ondelete (at_uninstall = False )
235
+ def _check_unlink_constraints (self ):
248
236
if any (view .state not in ("draft" , "sql_valid" ) for view in self ):
249
237
raise UserError (
250
238
_ (
251
- "You can only unlink draft views."
239
+ "You can only unlink draft views. "
252
240
"If you want to delete them, first set them to draft."
253
241
)
254
242
)
255
- return super ().unlink ()
256
243
257
244
def copy (self , default = None ):
258
245
self .ensure_one ()
@@ -395,8 +382,7 @@ def _prepare_cron(self):
395
382
.search ([("model" , "=" , self ._name )], limit = 1 )
396
383
.id ,
397
384
"state" : "code" ,
398
- "code" : "model._refresh_materialized_view_cron(%s)" % self .ids ,
399
- "numbercall" : - 1 ,
385
+ "code" : f"model._refresh_materialized_view_cron({ self .ids } )" ,
400
386
"interval_number" : 1 ,
401
387
"interval_type" : "days" ,
402
388
"nextcall" : now + timedelta (days = 1 ),
@@ -416,11 +402,11 @@ def _prepare_tree_view(self):
416
402
self .ensure_one ()
417
403
return {
418
404
"name" : self .name ,
419
- "type" : "tree " ,
405
+ "type" : "list " ,
420
406
"model" : self .model_id .model ,
421
407
"arch" : """<?xml version="1.0"?>"""
422
- """<tree name="Analysis">{}"""
423
- """</tree >""" .format (
408
+ """<list name="Analysis">{}"""
409
+ """</list >""" .format (
424
410
"" .join ([x ._prepare_tree_field () for x in self .bi_sql_view_field_ids ])
425
411
),
426
412
}
@@ -477,7 +463,7 @@ def _prepare_action(self):
477
463
self .ensure_one ()
478
464
view_mode = self .view_order
479
465
first_view = view_mode .split ("," )[0 ]
480
- if first_view == "tree " :
466
+ if first_view == "list " :
481
467
view_id = self .tree_view_id .id
482
468
elif first_view == "pivot" :
483
469
view_id = self .pivot_view_id .id
@@ -510,19 +496,22 @@ def _prepare_menu(self):
510
496
return {
511
497
"name" : self .name ,
512
498
"parent_id" : self .parent_menu_id .id ,
513
- "action" : "ir.actions.act_window,%s" % self .action_id .id ,
499
+ "action" : f "ir.actions.act_window,{ self .action_id .id } " ,
514
500
"sequence" : self .sequence ,
515
501
}
516
502
517
503
# Custom Section
518
504
def _log_execute (self , req ):
519
- _logger .info ("Executing SQL Request %s ..." % req )
505
+ _logger .info (f "Executing SQL Request { req } ..." )
520
506
self .env .cr .execute (req )
521
507
522
508
def _drop_view (self ):
523
509
for sql_view in self :
524
510
self ._log_execute (
525
- f"DROP { sql_view .materialized_text } VIEW IF EXISTS { sql_view .view_name } "
511
+ SQL ("DROP {materialized_text} VIEW IF EXISTS {view_name}" ).format (
512
+ materialized_text = SQL (sql_view .materialized_text ),
513
+ view_name = Identifier (sql_view .view_name ),
514
+ )
526
515
)
527
516
sql_view .size = False
528
517
@@ -551,8 +540,11 @@ def _create_index(self):
551
540
lambda x : x .is_index is True
552
541
):
553
542
self ._log_execute (
554
- f"CREATE INDEX { sql_field .index_name } ON { sql_view .view_name } "
555
- f"({ sql_field .name } );"
543
+ SQL ("CREATE INDEX {index_name} ON {view_name} ({name});" ).format (
544
+ index_name = SQL (sql_field .index_name ),
545
+ view_name = Identifier (sql_view .view_name ),
546
+ name = Identifier (sql_field .name ),
547
+ )
556
548
)
557
549
558
550
def _create_model_and_fields (self ):
@@ -562,7 +554,7 @@ def _create_model_and_fields(self):
562
554
sql_view .rule_id = self .env ["ir.rule" ].create (self ._prepare_rule ()).id
563
555
# Drop table, created by the ORM
564
556
if sql .table_exists (self ._cr , sql_view .view_name ):
565
- req = "DROP TABLE %s" % sql_view .view_name
557
+ req = SQL ( "DROP TABLE {}" ). format ( Identifier ( sql_view .view_name ))
566
558
self ._log_execute (req )
567
559
568
560
def _create_model_access (self ):
@@ -585,28 +577,29 @@ def _drop_model_and_fields(self):
585
577
586
578
def _hook_executed_request (self ):
587
579
self .ensure_one ()
588
- req = (
580
+ req = SQL (
589
581
"""
590
582
SELECT attnum,
591
583
attname AS column,
592
584
format_type(atttypid, atttypmod) AS type
593
585
FROM pg_attribute
594
- WHERE attrelid = '%s '::regclass
586
+ WHERE attrelid = '{view_name} '::regclass
595
587
AND NOT attisdropped
596
588
AND attnum > 0
597
589
ORDER BY attnum;"""
598
- % self .view_name
599
- )
590
+ ).format (view_name = Identifier (self .view_name ))
600
591
self ._log_execute (req )
601
592
return self .env .cr .fetchall ()
602
593
603
594
def _prepare_request_check_execution (self ):
604
595
self .ensure_one ()
605
- return f"CREATE VIEW { self .view_name } AS ({ self .query } );"
596
+ return SQL ("CREATE VIEW {view_name} AS ({query});" ).format (
597
+ view_name = Identifier (self .view_name ), query = SQL (self .query )
598
+ )
606
599
607
600
def _prepare_request_for_execution (self ):
608
601
self .ensure_one ()
609
- query = (
602
+ query = SQL (
610
603
"""
611
604
SELECT
612
605
CAST(row_number() OVER () as integer) AS id,
@@ -616,11 +609,14 @@ def _prepare_request_for_execution(self):
616
609
CAST(Null as integer) as write_uid,
617
610
my_query.*
618
611
FROM
619
- (%s ) as my_query
612
+ ({} ) as my_query
620
613
"""
621
- % self .query
614
+ ).format (SQL (self .query ))
615
+ return SQL ("CREATE {materialized_text} VIEW {view_name} AS ({query});" ).format (
616
+ materialized_text = SQL (self .materialized_text ),
617
+ view_name = Identifier (self .view_name ),
618
+ query = query ,
622
619
)
623
- return f"CREATE { self .materialized_text } VIEW { self .view_name } AS ({ query } );"
624
620
625
621
def _check_execution (self ):
626
622
"""Ensure that the query is valid, trying to execute it.
@@ -689,8 +685,8 @@ def _refresh_materialized_view(self):
689
685
690
686
def _refresh_size (self ):
691
687
for sql_view in self :
692
- req = "SELECT pg_size_pretty(pg_total_relation_size('%s '));" % (
693
- sql_view .view_name
688
+ req = SQL ( "SELECT pg_size_pretty(pg_total_relation_size('{} '));" ). format (
689
+ Identifier ( sql_view .view_name )
694
690
)
695
691
self ._log_execute (req )
696
692
sql_view .size = self .env .cr .fetchone ()[0 ]
@@ -700,7 +696,7 @@ def check_manual_fields(self, model):
700
696
# early on install / startup - particularly problematic during upgrade
701
697
if model ._name .startswith (
702
698
self ._model_prefix
703
- ) and "group_operator" in table_columns (self .env .cr , "bi_sql_view_field" ):
699
+ ) and "group_operator" in sql . table_columns (self .env .cr , "bi_sql_view_field" ):
704
700
# Use SQL instead of ORM, as ORM might not be fully initialised -
705
701
# we have no control over the order that fields are defined!
706
702
# We are not concerned about user security rules.
0 commit comments