Skip to content

Commit 7563b22

Browse files
committed
[ADD] fields/make_field_non_stored
opw-4502675 When a field becomes non-stored, the associated column must be removed during the upgrade. Unless the field remains selectable, such removal will cause `ir.filters` relying on such field to fail. This commit introduces a new util to clean up user-defined filters. In order to do that, the relevant part of the `remove_field` util is factored out into a new private method. --- Example of problem, with reproducer: `account.account`'s `group_id` field has become non-stored[^1] in 18.0, therefore: 1. Create db in 17.0, with `account` installed; 2. Create user-defined default filter on model `account.account`, with domain containing `group_id`. Example: `{"group_by", ["group_id"]}`; 3. Upgrade db to 18.0 4. Open chart of account (and select filter, if not automatically selected) Error should look something like the following: ``` RPC_ERROR Odoo Server Error Occured on localhost:8069 on model account.account and id 6 on 2025-02-06 09:34:41 GMT Traceback (most recent call last): File "/home/odoo/src/odoo/18.0/odoo/http.py", line 1957, in _transactioning return service_model.retrying(func, env=self.env) File "/home/odoo/src/odoo/18.0/odoo/service/model.py", line 137, in retrying result = func() File "/home/odoo/src/odoo/18.0/odoo/http.py", line 1924, in _serve_ir_http response = self.dispatcher.dispatch(rule.endpoint, args) File "/home/odoo/src/odoo/18.0/odoo/http.py", line 2172, in dispatch result = self.request.registry['ir.http']._dispatch(endpoint) File "/home/odoo/src/odoo/18.0/odoo/addons/base/models/ir_http.py", line 329, in _dispatch result = endpoint(**request.params) File "/home/odoo/src/odoo/18.0/odoo/http.py", line 727, in route_wrapper result = endpoint(self, *args, **params_ok) File "/home/odoo/src/odoo/18.0/addons/web/controllers/dataset.py", line 35, in call_kw return call_kw(request.env[model], method, args, kwargs) File "/home/odoo/src/odoo/18.0/odoo/api.py", line 517, in call_kw result = getattr(recs, name)(*args, **kwargs) File "/home/odoo/src/odoo/18.0/addons/web/models/models.py", line 243, in web_read_group groups = self._web_read_group(domain, fields, groupby, limit, offset, orderby, lazy) File "/home/odoo/src/odoo/18.0/addons/web/models/models.py", line 269, in _web_read_group groups = self.read_group(domain, fields, groupby, offset=offset, limit=limit, File "/home/odoo/src/odoo/18.0/odoo/models.py", line 2858, in read_group rows = self._read_group(domain, annotated_groupby.values(), annotated_aggregates.values(), offset=offset, limit=limit, order=orderby) File "/home/odoo/src/odoo/18.0/odoo/models.py", line 1973, in _read_group groupby_terms: dict[str, SQL] = { File "/home/odoo/src/odoo/18.0/odoo/models.py", line 1974, in <dictcomp> spec: self._read_group_groupby(spec, query) File "/home/odoo/src/odoo/18.0/odoo/models.py", line 2090, in _read_group_groupby sql_expr = self._field_to_sql(self._table, fname, query) File "/home/odoo/src/odoo/18.0/addons/account/models/account_account.py", line 184, in _field_to_sql return super()._field_to_sql(alias, fname, query, flush) File "/home/odoo/src/odoo/18.0/odoo/models.py", line 2946, in _field_to_sql raise ValueError(f"Cannot convert {field} to SQL because it is not stored") ValueError: Cannot convert account.account.group_id to SQL because it is not stored The above server error caused the following client error: OwlError: An error occured in the owl lifecycle (see this Error's "cause" property) Error: An error occured in the owl lifecycle (see this Error's "cause" property) at handleError (http://localhost:8069/web/assets/fd2cc33/web.assets_web.min.js:959:101) at App.handleError (http://localhost:8069/web/assets/fd2cc33/web.assets_web.min.js:1610:29) at ComponentNode.initiateRender (http://localhost:8069/web/assets/fd2cc33/web.assets_web.min.js:1051:19) Caused by: RPC_ERROR: Odoo Server Error RPC_ERROR at makeErrorFromResponse (http://localhost:8069/web/assets/fd2cc33/web.assets_web.min.js:3134:163) at XMLHttpRequest.<anonymous> (http://localhost:8069/web/assets/fd2cc33/web.assets_web.min.js:3139:13) ``` [^1]: https://github.com/odoo/upgrade/blob/6197269809a7007fd7eadfc8fb6d2c6a83bc5ca4/migrations/account/saas~17.5.1.2/pre-migrate.py#L97 closes #208 Related: odoo/upgrade#7177 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent f53e454 commit 7563b22

File tree

1 file changed

+87
-68
lines changed

1 file changed

+87
-68
lines changed

src/util/fields.py

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -110,25 +110,42 @@ def ensure_m2o_func_field_data(cr, src_table, column, dst_table):
110110
remove_column(cr, src_table, column, cascade=True)
111111

112112

113-
def remove_field(cr, model, fieldname, cascade=False, drop_column=True, skip_inherit=()):
114-
"""
115-
Remove a field and its references from the database.
116-
117-
This function also removes the field from inheriting models, unless exceptions are
118-
specified in `skip_inherit`. When the field is stored we can choose to not drop the
119-
column.
113+
def _remove_field_from_filters(cr, model, field):
114+
cr.execute(
115+
"SELECT id, name, context FROM ir_filters WHERE model_id = %s AND context ~ %s",
116+
[model, r"\y{}\y".format(field)],
117+
)
118+
for id_, name, context_s in cr.fetchall():
119+
context = safe_eval(context_s or "{}", SelfPrintEvalContext(), nocopy=True)
120+
changed = _remove_field_from_context(context, field)
121+
cr.execute("UPDATE ir_filters SET context = %s WHERE id = %s", [unicode(context), id_])
122+
if changed:
123+
add_to_migration_reports(("ir.filters", id_, name), "Filters/Dashboards")
120124

121-
:param str model: model name of the field to remove
122-
:param str fieldname: name of the field to remove
123-
:param bool cascade: whether the field column(s) are removed in `CASCADE` mode
124-
:param bool drop_column: whether the field's column is dropped
125-
:param list(str) or str skip_inherit: list of inheriting models to skip the removal
126-
of the field, use `"*"` to skip all
127-
"""
128-
_validate_model(model)
125+
if column_exists(cr, "ir_filters", "sort"):
126+
cr.execute(
127+
"""
128+
WITH to_update AS (
129+
SELECT f.id,
130+
COALESCE(ARRAY_TO_JSON(ARRAY_AGG(s.sort_item ORDER BY rn) filter (WHERE s.sort_item not in %s)), '[]') AS sort
131+
FROM ir_filters f
132+
JOIN LATERAL JSONB_ARRAY_ELEMENTS_TEXT(f.sort::jsonb)
133+
WITH ORDINALITY AS s(sort_item, rn)
134+
ON true
135+
WHERE f.model_id = %s
136+
AND f.sort ~ %s
137+
GROUP BY id
138+
)
139+
UPDATE ir_filters f
140+
SET sort = t.sort
141+
FROM to_update t
142+
WHERE f.id = t.id
143+
""",
144+
[(field, field + " desc"), model, r"\y{}\y".format(field)],
145+
)
129146

130-
ENVIRON["__renamed_fields"][model][fieldname] = None
131147

148+
def _remove_field_from_context(context, fieldname):
132149
def filter_value(key, value):
133150
if key == "orderedBy" and isinstance(value, dict):
134151
res = {k: (filter_value(None, v) if k == "name" else v) for k, v in value.items()}
@@ -142,72 +159,59 @@ def filter_value(key, value):
142159
return value
143160
return None # value filtered out
144161

145-
def clean_context(context):
146-
if not isinstance(context, dict):
147-
return False
162+
if not isinstance(context, dict):
163+
return False
164+
165+
changed = False
166+
for key in _CONTEXT_KEYS_TO_CLEAN:
167+
if context.get(key):
168+
context_part = [filter_value(key, e) for e in context[key]]
169+
changed |= context_part != context[key]
170+
context[key] = [e for e in context_part if e is not None]
171+
172+
for vt in ["pivot", "graph", "cohort"]:
173+
key = "{}_measure".format(vt)
174+
if key in context:
175+
new_value = filter_value(key, context[key])
176+
changed |= context[key] != new_value
177+
context[key] = new_value if new_value is not None else "id"
148178

149-
changed = False
150-
for key in _CONTEXT_KEYS_TO_CLEAN:
151-
if context.get(key):
152-
context_part = [filter_value(key, e) for e in context[key]]
153-
changed |= context_part != context[key]
154-
context[key] = [e for e in context_part if e is not None]
179+
if vt in context:
180+
changed |= _remove_field_from_context(context[vt], fieldname)
155181

156-
for vt in ["pivot", "graph", "cohort"]:
157-
key = "{}_measure".format(vt)
158-
if key in context:
159-
new_value = filter_value(key, context[key])
160-
changed |= context[key] != new_value
161-
context[key] = new_value if new_value is not None else "id"
182+
return changed
162183

163-
if vt in context:
164-
changed |= clean_context(context[vt])
165184

166-
return changed
185+
def remove_field(cr, model, fieldname, cascade=False, drop_column=True, skip_inherit=()):
186+
"""
187+
Remove a field and its references from the database.
188+
189+
This function also removes the field from inheriting models, unless exceptions are
190+
specified in `skip_inherit`. When the field is stored we can choose to not drop the
191+
column.
192+
193+
:param str model: model name of the field to remove
194+
:param str fieldname: name of the field to remove
195+
:param bool cascade: whether the field column(s) are removed in `CASCADE` mode
196+
:param bool drop_column: whether the field's column is dropped
197+
:param list(str) or str skip_inherit: list of inheriting models to skip the removal
198+
of the field, use `"*"` to skip all
199+
"""
200+
_validate_model(model)
201+
202+
ENVIRON["__renamed_fields"][model][fieldname] = None
167203

168204
# clean dashboard's contexts
169205
for id_, action in _dashboard_actions(cr, r"\y{}\y".format(fieldname), model):
170206
context = safe_eval(action.get("context", "{}"), SelfPrintEvalContext(), nocopy=True)
171-
changed = clean_context(context)
207+
changed = _remove_field_from_context(context, fieldname)
172208
action.set("context", unicode(context))
173209
if changed:
174210
add_to_migration_reports(
175211
("ir.ui.view.custom", id_, action.get("string", "ir.ui.view.custom")), "Filters/Dashboards"
176212
)
177213

178-
# clean filter's contexts
179-
cr.execute(
180-
"SELECT id, name, context FROM ir_filters WHERE model_id = %s AND context ~ %s",
181-
[model, r"\y{}\y".format(fieldname)],
182-
)
183-
for id_, name, context_s in cr.fetchall():
184-
context = safe_eval(context_s or "{}", SelfPrintEvalContext(), nocopy=True)
185-
changed = clean_context(context)
186-
cr.execute("UPDATE ir_filters SET context = %s WHERE id = %s", [unicode(context), id_])
187-
if changed:
188-
add_to_migration_reports(("ir.filters", id_, name), "Filters/Dashboards")
189-
190-
if column_exists(cr, "ir_filters", "sort"):
191-
cr.execute(
192-
"""
193-
WITH to_update AS (
194-
SELECT f.id,
195-
COALESCE(ARRAY_TO_JSON(ARRAY_AGG(s.sort_item ORDER BY rn) filter (WHERE s.sort_item not in %s)), '[]') AS sort
196-
FROM ir_filters f
197-
JOIN LATERAL JSONB_ARRAY_ELEMENTS_TEXT(f.sort::jsonb)
198-
WITH ORDINALITY AS s(sort_item, rn)
199-
ON true
200-
WHERE f.model_id = %s
201-
AND f.sort ~ %s
202-
GROUP BY id
203-
)
204-
UPDATE ir_filters f
205-
SET sort = t.sort
206-
FROM to_update t
207-
WHERE f.id = t.id
208-
""",
209-
[(fieldname, fieldname + " desc"), model, r"\y{}\y".format(fieldname)],
210-
)
214+
_remove_field_from_filters(cr, model, fieldname)
211215

212216
_remove_import_export_paths(cr, model, fieldname)
213217

@@ -372,6 +376,21 @@ def adapter(leaf, is_or, negated):
372376
remove_field(cr, inh.model, fieldname, cascade=cascade, drop_column=drop_column, skip_inherit=skip_inherit)
373377

374378

379+
def make_field_non_stored(cr, model, field, selectable=False):
380+
"""
381+
Convert field to non-stored.
382+
383+
:param str model: model name of the field to convert
384+
:param str fieldname: name of the field to convert
385+
:param bool selectable: whether the field is still selectable, if True custom `ir.filters` are not updated
386+
"""
387+
_validate_model(model)
388+
389+
remove_column(cr, table_of_model(cr, model), field)
390+
if not selectable:
391+
_remove_field_from_filters(cr, model, field)
392+
393+
375394
def remove_field_metadata(cr, model, fieldname, skip_inherit=()):
376395
"""
377396
Remove metadata of a field.

0 commit comments

Comments
 (0)