1616
1717import psycopg2
1818from psycopg2 import sql
19- from psycopg2 .extras import Json
19+ from psycopg2 .extras import Json , execute_values
2020
2121try :
2222 from odoo import release
@@ -40,7 +40,7 @@ def make_index_name(table_name, column_name):
4040from .const import ENVIRON
4141from .domains import _adapt_one_domain , _replace_path , _valid_path_to , adapt_domains
4242from .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
4444from .inherit import for_each_inherit
4545from .misc import SelfPrintEvalContext , log_progress , version_gte
4646from .orm import env , invalidate
@@ -79,6 +79,126 @@ def make_index_name(table_name, column_name):
7979)
8080
8181
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+
82202def ensure_m2o_func_field_data (cr , src_table , column , dst_table ):
83203 """
84204 Fix broken m2o relations.
@@ -202,6 +322,9 @@ def clean_context(context):
202322 [(fieldname , fieldname + " desc" ), model , r"\y{}\y" .format (fieldname )],
203323 )
204324
325+ # ir.exports.line
326+ remove_ir_exports_lines (cr , fields = [(model , fieldname )])
327+
205328 def adapter (leaf , is_or , negated ):
206329 # replace by TRUE_LEAF, unless negated or in a OR operation but not negated
207330 if is_or ^ negated :
@@ -1071,21 +1194,8 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
10711194 cr .execute (q .format (col_prefix = col_prefix ), p )
10721195
10731196 # ir.exports.line
1074- q = """
1075- UPDATE ir_exports_line l
1076- SET name = regexp_replace(l.name, %(old)s, %(new)s, 'g')
1077- """
10781197 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 })
10891199
10901200 # mail.alias
10911201 if column_exists (cr , "mail_alias" , "alias_defaults" ):
0 commit comments