16
16
17
17
import psycopg2
18
18
from psycopg2 import sql
19
- from psycopg2 .extras import Json
19
+ from psycopg2 .extras import Json , execute_values
20
20
21
21
try :
22
22
from odoo import release
@@ -40,7 +40,7 @@ def make_index_name(table_name, column_name):
40
40
from .const import ENVIRON
41
41
from .domains import _adapt_one_domain , _replace_path , _valid_path_to , adapt_domains
42
42
from .exceptions import SleepyDeveloperError
43
- from .helpers import _dashboard_actions , _validate_model , table_of_model
43
+ from .helpers import _dashboard_actions , _validate_model , resolve_model_fields_path , table_of_model
44
44
from .inherit import for_each_inherit
45
45
from .misc import SelfPrintEvalContext , log_progress , version_gte
46
46
from .orm import env , invalidate
@@ -79,6 +79,126 @@ def make_index_name(table_name, column_name):
79
79
)
80
80
81
81
82
+ def _get_resolved_ir_exports (cr , models = None , fields = None ):
83
+ """
84
+ Return a list of ir.exports.line records which models or fields match the given arguments.
85
+
86
+ Export lines can reference nested models through relationship field "paths"
87
+ (e.g. "partner_id/country_id/name"), therefore these needs to be resolved properly.
88
+
89
+ Only one of ``models`` or ``fields`` arguments should be provided.
90
+
91
+ :param list[str] models: a list of model names to match in exports
92
+ :param list[(str, str)] fields: a list of (model, field) tuples to match in exports
93
+ :return: the resolved field paths parts for each matched export line id
94
+ :rtype: dict[int, list[FieldsPathPart]]
95
+
96
+ :meta private: exclude from online docs
97
+ """
98
+ assert bool (models ) ^ bool (fields ), "One of models or fields must be given, and not both."
99
+
100
+ # Get the model fields paths for exports.
101
+ # When matching fields we can already broadly filter on field names (will be double-checked later).
102
+ # When matching models we can't exclude anything because we don't know intermediate models.
103
+ where = ""
104
+ params = {}
105
+ if fields :
106
+ fields = {(model , fields ) for model , fields in fields } # noqa: C416 # make sure set[tuple]
107
+ where = "WHERE el.name ~ ANY(%(field_names)s)"
108
+ params ["field_names" ] = [f [1 ] for f in fields ]
109
+ cr .execute (
110
+ """
111
+ SELECT el.id, e.resource AS model, string_to_array(el.name, '/') AS path
112
+ FROM ir_exports e
113
+ JOIN ir_exports_line el ON e.id = el.export_id
114
+ {where}
115
+ """ .format (where = where ),
116
+ params ,
117
+ )
118
+ paths_to_line_ids = {}
119
+ for line_id , model , path in cr .fetchall ():
120
+ paths_to_line_ids .setdefault ((model , tuple (path )), set ()).add (line_id )
121
+
122
+ # Resolve intermediate models for all model fields paths, filter only matching paths parts
123
+ matching_paths_parts = {}
124
+ for model , path in paths_to_line_ids :
125
+ resolved_paths = resolve_model_fields_path (cr , model , path )
126
+ if fields :
127
+ matching_parts = [p for p in resolved_paths if (p .field_model , p .field_name ) in fields ]
128
+ else :
129
+ matching_parts = [p for p in resolved_paths if p .field_model in models ]
130
+ if not matching_parts :
131
+ continue
132
+ matching_paths_parts [(model , path )] = matching_parts
133
+
134
+ # Return the matched parts for each export line id
135
+ result = {}
136
+ for (model , path ), matching_parts in matching_paths_parts .items ():
137
+ line_ids = paths_to_line_ids .get ((model , path ))
138
+ if not line_ids :
139
+ continue # wut?
140
+ for line_id in line_ids :
141
+ result .setdefault (line_id , []).extend (matching_parts )
142
+ return result
143
+
144
+
145
+ def rename_ir_exports_fields (cr , models_fields_map ):
146
+ """
147
+ Rename fields references in ir.exports.line records.
148
+
149
+ :param dict[str, dict[str, str]] models_fields_map: a dict of models to the fields rename dict,
150
+ like: `{"model.name": {"old_field": "new_field", ...}, ...}`
151
+
152
+ :meta private: exclude from online docs
153
+ """
154
+ matching_exports = _get_resolved_ir_exports (
155
+ cr ,
156
+ fields = [(model , field ) for model , fields_map in models_fields_map .items () for field in fields_map ],
157
+ )
158
+ if not matching_exports :
159
+ return
160
+ _logger .debug ("Renaming %d export template lines with renamed fields" , len (matching_exports ))
161
+ fixed_lines_paths = {}
162
+ for line_id , resolved_paths in matching_exports .items ():
163
+ for path_part in resolved_paths :
164
+ assert path_part .field_model in models_fields_map
165
+ fields_map = models_fields_map [path_part .field_model ]
166
+ assert path_part .field_name in fields_map
167
+ assert path_part .path [path_part .part_index - 1 ] == path_part .field_name
168
+ new_field_name = fields_map [path_part .field_name ]
169
+ fixed_path = fixed_lines_paths .get (line_id , list (path_part .path ))
170
+ fixed_path [path_part .part_index - 1 ] = new_field_name
171
+ fixed_lines_paths [line_id ] = fixed_path
172
+ execute_values (
173
+ cr ,
174
+ """
175
+ UPDATE ir_exports_line el
176
+ SET name = v.name
177
+ FROM (VALUES %s) AS v(id, name)
178
+ WHERE el.id = v.id
179
+ """ ,
180
+ [(k , "/" .join (v )) for k , v in fixed_lines_paths .items ()],
181
+ )
182
+
183
+
184
+ def remove_ir_exports_lines (cr , models = None , fields = None ):
185
+ """
186
+ Delete ir.exports.line records that reference models or fields that are/will be removed.
187
+
188
+ Only one of ``models`` or ``fields`` arguments should be provided.
189
+
190
+ :param list[str] models: a list of model names to match in exports
191
+ :param list[(str, str)] fields: a list of (model, field) tuples to match in exports
192
+
193
+ :meta private: exclude from online docs
194
+ """
195
+ matching_exports = _get_resolved_ir_exports (cr , models = models , fields = fields )
196
+ if not matching_exports :
197
+ return
198
+ _logger .debug ("Deleting %d export template lines with removed models/fields" , len (matching_exports ))
199
+ cr .execute ("DELETE FROM ir_exports_line WHERE id IN %s" , [tuple (matching_exports .keys ())])
200
+
201
+
82
202
def ensure_m2o_func_field_data (cr , src_table , column , dst_table ):
83
203
"""
84
204
Fix broken m2o relations.
@@ -202,6 +322,9 @@ def clean_context(context):
202
322
[(fieldname , fieldname + " desc" ), model , r"\y{}\y" .format (fieldname )],
203
323
)
204
324
325
+ # ir.exports
326
+ remove_ir_exports_lines (cr , fields = [(model , fieldname )])
327
+
205
328
def adapter (leaf , is_or , negated ):
206
329
# replace by TRUE_LEAF, unless negated or in a OR operation but not negated
207
330
if is_or ^ negated :
@@ -1070,22 +1193,9 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
1070
1193
"""
1071
1194
cr .execute (q .format (col_prefix = col_prefix ), p )
1072
1195
1073
- # ir.exports.line
1074
- q = """
1075
- UPDATE ir_exports_line l
1076
- SET name = regexp_replace(l.name, %(old)s, %(new)s, 'g')
1077
- """
1196
+ # ir.exports
1078
1197
if only_models :
1079
- q += """
1080
- FROM ir_exports e
1081
- WHERE e.id = l.export_id
1082
- AND e.resource IN %(models)s
1083
- AND
1084
- """
1085
- else :
1086
- q += "WHERE "
1087
- q += "l.name ~ %(old)s"
1088
- cr .execute (q , p )
1198
+ rename_ir_exports_fields (cr , {model : {old : new } for model in only_models })
1089
1199
1090
1200
# mail.alias
1091
1201
if column_exists (cr , "mail_alias" , "alias_defaults" ):
0 commit comments