|
13 | 13 | import logging
|
14 | 14 | import re
|
15 | 15 | import warnings
|
| 16 | +from collections import namedtuple |
16 | 17 |
|
17 | 18 | import psycopg2
|
18 | 19 | from psycopg2 import sql
|
19 |
| -from psycopg2.extras import Json |
| 20 | +from psycopg2.extras import Json, execute_values |
20 | 21 |
|
21 | 22 | try:
|
22 | 23 | from odoo import release
|
@@ -79,6 +80,150 @@ def make_index_name(table_name, column_name):
|
79 | 80 | )
|
80 | 81 |
|
81 | 82 |
|
| 83 | +ResolvedExportsLine = namedtuple( |
| 84 | + "ResolvedExportsLine", |
| 85 | + "export_id export_name export_model line_id path_parts part_index field_name field_model field_id relation_model", |
| 86 | +) |
| 87 | + |
| 88 | + |
| 89 | +def get_resolved_ir_exports(cr, models=None, fields=None, only_missing=False): |
| 90 | + """ |
| 91 | + Return a list of ir.exports.line records which models or fields match the given arguments. |
| 92 | +
|
| 93 | + Export lines can reference nested models through relationship field "paths" |
| 94 | + (e.g. "partner_id/country_id/name"), therefore these needs to be resolved properly. |
| 95 | +
|
| 96 | + Only one of ``models`` or ``fields`` arguments should be provided. |
| 97 | +
|
| 98 | + :param list[str] models: a list of model names to match in exports |
| 99 | + :param list[(str, str)] fields: a list of (model, field) tuples to match in exports |
| 100 | + :param bool only_missing: include only lines which contain missing models/fields |
| 101 | + :return: the matched resolved exports lines |
| 102 | + :rtype: list[ResolvedExportsLine] |
| 103 | +
|
| 104 | + :meta private: exclude from online docs |
| 105 | + """ |
| 106 | + assert bool(models) ^ bool(fields), "One of models or fields must be given, and not both." |
| 107 | + extra_where = "" |
| 108 | + query_params = {} |
| 109 | + if models: |
| 110 | + extra_where += " AND field_model = ANY(%(models)s)" |
| 111 | + query_params["models"] = list(models) |
| 112 | + if fields: |
| 113 | + extra_where += " AND (field_model, field_name) IN %(fields)s" |
| 114 | + query_params["fields"] = tuple((model, field) for model, field in fields) |
| 115 | + if only_missing: |
| 116 | + extra_where += " AND field_id IS NULL" |
| 117 | + # Resolve exports using a recursive CTE query |
| 118 | + cr.execute( |
| 119 | + """ |
| 120 | + WITH RECURSIVE resolved_exports_fields AS ( |
| 121 | + -- non-recursive term |
| 122 | + SELECT e.id AS export_id, |
| 123 | + e.name AS export_name, |
| 124 | + e.resource AS export_model, |
| 125 | + el.id AS line_id, |
| 126 | + string_to_array(el.name, '/') AS path_parts, |
| 127 | + 1 AS part_index, |
| 128 | + replace((string_to_array(el.name, '/'))[1], '.id', 'id') AS field_name, |
| 129 | + e.resource AS field_model, |
| 130 | + elf.id AS field_id, |
| 131 | + elf.relation AS relation_model |
| 132 | + FROM ir_exports_line el |
| 133 | + JOIN ir_exports e |
| 134 | + ON el.export_id = e.id |
| 135 | + LEFT JOIN ir_model_fields elf |
| 136 | + ON elf.model = e.resource AND elf.name = (string_to_array(el.name, '/'))[1] |
| 137 | + LEFT JOIN ir_model em |
| 138 | + ON em.model = e.resource |
| 139 | +
|
| 140 | + UNION ALL |
| 141 | +
|
| 142 | + -- recursive term |
| 143 | + SELECT ref.export_id, |
| 144 | + ref.export_name, |
| 145 | + ref.export_model, |
| 146 | + ref.line_id, |
| 147 | + ref.path_parts, |
| 148 | + ref.part_index + 1 AS part_index, |
| 149 | + replace(ref.path_parts[ref.part_index + 1], '.id', 'id') AS field_name, |
| 150 | + ref.relation_model AS field_model, |
| 151 | + refmf.id AS field_id, |
| 152 | + refmf.relation AS relation_model |
| 153 | + FROM resolved_exports_fields ref |
| 154 | + LEFT JOIN ir_model_fields refmf |
| 155 | + ON refmf.model = ref.relation_model AND refmf.name = ref.path_parts[ref.part_index + 1] |
| 156 | + WHERE cardinality(ref.path_parts) > ref.part_index AND ref.relation_model IS NOT NULL |
| 157 | + ) |
| 158 | + SELECT * |
| 159 | + FROM resolved_exports_fields |
| 160 | + WHERE field_name != 'id' {extra_where} |
| 161 | + ORDER BY export_id, line_id, part_index |
| 162 | + """.format(extra_where=extra_where), |
| 163 | + query_params, |
| 164 | + ) |
| 165 | + return [ResolvedExportsLine(**row) for row in cr.dictfetchall()] |
| 166 | + |
| 167 | + |
| 168 | +def rename_ir_exports_fields(cr, models_fields_map): |
| 169 | + """ |
| 170 | + Rename fields references in ir.exports.line records. |
| 171 | +
|
| 172 | + :param dict[str, dict[str, str]] models_fields_map: a dict of models to the fields rename dict, |
| 173 | + like: `{"model.name": {"old_field": "new_field", ...}, ...}` |
| 174 | +
|
| 175 | + :meta private: exclude from online docs |
| 176 | + """ |
| 177 | + matching_exports = get_resolved_ir_exports( |
| 178 | + cr, |
| 179 | + fields=[(model, field) for model, fields_map in models_fields_map.items() for field in fields_map], |
| 180 | + ) |
| 181 | + if not matching_exports: |
| 182 | + return |
| 183 | + _logger.debug("Renaming %d export template lines with renamed fields", len(matching_exports)) |
| 184 | + fixed_lines_paths = {} |
| 185 | + for row in matching_exports: |
| 186 | + assert row.field_model in models_fields_map |
| 187 | + fields_map = models_fields_map[row.field_model] |
| 188 | + assert row.field_name in fields_map |
| 189 | + assert row.path_parts[row.part_index - 1] == row.field_name |
| 190 | + new_field_name = fields_map[row.field_name] |
| 191 | + fixed_path = list(row.path_parts) |
| 192 | + fixed_path[row.part_index - 1] = new_field_name |
| 193 | + fixed_lines_paths[row.line_id] = fixed_path |
| 194 | + execute_values( |
| 195 | + cr, |
| 196 | + """ |
| 197 | + UPDATE ir_exports_line el |
| 198 | + SET name = v.name |
| 199 | + FROM (VALUES %s) AS v(id, name) |
| 200 | + WHERE el.id = v.id |
| 201 | + """, |
| 202 | + [(k, "/".join(v)) for k, v in fixed_lines_paths.items()], |
| 203 | + ) |
| 204 | + |
| 205 | + |
| 206 | +def remove_ir_exports_lines(cr, models=None, fields=None): |
| 207 | + """ |
| 208 | + Delete ir.exports.line records that reference models or fields that are/will be removed. |
| 209 | +
|
| 210 | + Only one of ``models`` or ``fields`` arguments should be provided. |
| 211 | +
|
| 212 | + :param list[str] models: a list of model names to match in exports |
| 213 | + :param list[(str, str)] fields: a list of (model, field) tuples to match in exports |
| 214 | +
|
| 215 | + :meta private: exclude from online docs |
| 216 | + """ |
| 217 | + matching_exports = get_resolved_ir_exports(cr, models=models, fields=fields) |
| 218 | + if not matching_exports: |
| 219 | + return |
| 220 | + lines_ids = {row.line_id for row in matching_exports} |
| 221 | + _logger.debug("Deleting %d export template lines with removed models/fields", len(lines_ids)) |
| 222 | + cr.execute("DELETE FROM ir_exports_line WHERE id IN %s", [tuple(lines_ids)]) |
| 223 | + for row in matching_exports: |
| 224 | + add_to_migration_reports(row, category="Export Templates") |
| 225 | + |
| 226 | + |
82 | 227 | def ensure_m2o_func_field_data(cr, src_table, column, dst_table):
|
83 | 228 | """
|
84 | 229 | Fix broken m2o relations.
|
@@ -202,6 +347,9 @@ def clean_context(context):
|
202 | 347 | [(fieldname, fieldname + " desc"), model, r"\y{}\y".format(fieldname)],
|
203 | 348 | )
|
204 | 349 |
|
| 350 | + # ir.exports |
| 351 | + remove_ir_exports_lines(cr, fields=[(model, fieldname)]) |
| 352 | + |
205 | 353 | def adapter(leaf, is_or, negated):
|
206 | 354 | # replace by TRUE_LEAF, unless negated or in a OR operation but not negated
|
207 | 355 | if is_or ^ negated:
|
@@ -1065,22 +1213,9 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
|
1065 | 1213 | """
|
1066 | 1214 | cr.execute(q.format(col_prefix=col_prefix), p)
|
1067 | 1215 |
|
1068 |
| - # ir.exports.line |
1069 |
| - q = """ |
1070 |
| - UPDATE ir_exports_line l |
1071 |
| - SET name = regexp_replace(l.name, %(old)s, %(new)s, 'g') |
1072 |
| - """ |
| 1216 | + # ir.exports |
1073 | 1217 | if only_models:
|
1074 |
| - q += """ |
1075 |
| - FROM ir_exports e |
1076 |
| - WHERE e.id = l.export_id |
1077 |
| - AND e.resource IN %(models)s |
1078 |
| - AND |
1079 |
| - """ |
1080 |
| - else: |
1081 |
| - q += "WHERE " |
1082 |
| - q += "l.name ~ %(old)s" |
1083 |
| - cr.execute(q, p) |
| 1218 | + rename_ir_exports_fields(cr, {model: {old: new} for model in only_models}) |
1084 | 1219 |
|
1085 | 1220 | # mail.alias
|
1086 | 1221 | if column_exists(cr, "mail_alias", "alias_defaults"):
|
|
0 commit comments