From 10763f3131be32d6015c74e6bddb6cdc70433970 Mon Sep 17 00:00:00 2001 From: Ernesto Tejeda Date: Mon, 8 May 2023 09:55:11 +0200 Subject: [PATCH] [IMP] update chained fields name in ir_exports_line and ir_filters --- openupgradelib/openupgrade.py | 284 +++++++++++++++++++++++++++------- requirements.txt | 1 + 2 files changed, 233 insertions(+), 52 deletions(-) diff --git a/openupgradelib/openupgrade.py b/openupgradelib/openupgrade.py index 5b64ced8..6492afb4 100644 --- a/openupgradelib/openupgrade.py +++ b/openupgradelib/openupgrade.py @@ -4,6 +4,7 @@ # Copyright Odoo Community Association (OCA) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import ast import inspect import logging as _logging_module import os @@ -12,6 +13,11 @@ from datetime import datetime from functools import wraps +if sys.version_info >= (3, 9): + from ast import unparse +else: + from astunparse import unparse + try: from StringIO import StringIO except ImportError: @@ -147,6 +153,9 @@ def do_raise(error): "copy_columns", "copy_fields_multilang", "remove_tables_fks", + "update_ir_exports_line", + "update_ir_filters", + "update_domain_str", "rename_columns", "rename_fields", "rename_tables", @@ -627,7 +636,188 @@ def rename_columns(cr, column_spec): ) -def rename_fields(env, field_spec, no_deep=False): +def _update_chained_field_name( + env, chained_field_name, first_model_name, model, new_field, old_field, separator +): + """Rename a field name in a string. The string can represent + a chained fields name separated by a specific character. + Example: 'x_id/old_field/name'. + + :param (Environment) env: Odoo environment. + :param (str) chained_field_name: String that may contain + the old field name. + :param (str) first_model_name: Model corresponding to the + first field of 'chained field name'. + :param (str) model: Model name of the field to rename. + :param (str) new_field: New field name. + :param (str) old_field: Old field name. + :param (str) separator: Separator of the 'chained field name'. + Example: in 'x_id/old_field/name' the separator is the character '/'. + + :return: The updated 'chained field name' string if the field was + successfully renamed, False otherwise. + + """ + current_model_name = first_model_name + field_name_list = chained_field_name.split(separator) + for index in range(len(field_name_list) - 1): + current_field_name = field_name_list[index] + if current_field_name == old_field and current_model_name == model: + field_name_list[index] = new_field + return separator.join(field_name_list) + env.cr.execute( + """SELECT + im.model + FROM + ir_model_fields imf + INNER JOIN ir_model im ON im.model = imf.relation + WHERE + imf.name = %s AND imf.model = %s""", + (current_field_name, current_model_name), + ) + res = env.cr.fetchone() + if not res: + return False + current_model_name = res[0] + if field_name_list[-1] == old_field and current_model_name == model: + field_name_list[-1] = new_field + return separator.join(field_name_list) + return False + + +def update_ir_exports_line(env, model, new_field, old_field): + """Rename a field name in 'name' column in the 'ir_exports_line' + table. The field name in this column can be a chained + fields name separated by the character '/'. + + + :param (Environment) env: Odoo environment. + :param (str) model: Model name of the field to rename. + :param (str) new_field: New field name. + :param (str) old_field: Old field name. + """ + env.cr.execute( + """SELECT + iel.id, iel.name, ie.resource + FROM + ir_exports_line iel + INNER JOIN ir_exports ie ON ie.id = iel.export_id""", + ) + exports_lines = env.cr.fetchall() + for line_data in exports_lines: + line_id, chained_field_name, first_model_name = line_data + new_chained_field_name = _update_chained_field_name( + env, + chained_field_name=chained_field_name, + first_model_name=first_model_name, + model=model, + new_field=new_field, + old_field=old_field, + separator="/", + ) + if new_chained_field_name: + logged_query( + env.cr, + """UPDATE ir_exports_line + SET name = %s + WHERE id = %s + """, + (new_chained_field_name, line_id), + ) + + +def update_domain_str(env, domain_str, first_model_name, model, new_field, old_field): + """Rename a field that is part of a domain ('domain' column) in the + 'ir_filters' table even if it is part of a dotted name. + + Example: + If the domain string is "[('x_id.old_field.name', '=', 'value')]" and + 'new_field' corresponds to 'model', then the domain string will be + updated to "[('x_id.new_field.name', '=', 'value')]". + + :param (Environment) env: Odoo environment. + :param (str) domain_str: Domain string that may contain + the old field name. + :param (str) model: Model name of the field to rename. + :param (str) new_field: New field name. + :param (str) old_field: Old field name. + + :return: The updated domain string if the field + was successfully renamed, False otherwise. + """ + domain_exp = ast.parse(domain_str, mode="eval") + domain_exp_list = domain_exp.body.elts + new_domain_exp_list = [] + transformed = False + for elem in domain_exp_list: + new_elem = elem + if isinstance(elem, ast.Tuple) or isinstance(elem, ast.List): + field_name_node = elem.elts[0] + new_chained_field_name = _update_chained_field_name( + env, + chained_field_name=field_name_node.s, + first_model_name=first_model_name, + model=model, + new_field=new_field, + old_field=old_field, + separator=".", + ) + if new_chained_field_name: + if sys.version_info < (3, 8): + new_field_name_node = ast.Str(s=new_chained_field_name) + else: + new_field_name_node = ast.Constant( + value=new_chained_field_name, + kind=field_name_node.kind, + ) + new_elem = type(elem)( + elts=[new_field_name_node] + elem.elts[1:], ctx=elem.ctx + ) + transformed = True + new_domain_exp_list.append(new_elem) + if transformed: + new_domain_expr = ast.Expression( + ast.List(elts=new_domain_exp_list, ctx=ast.Load()) + ) + return unparse(new_domain_expr).strip() + return False + + +def update_ir_filters(env, model, new_field, old_field): + """Rename a field that is part of a domain ('domain' column) in the + 'ir_filters' table even if it is part of a dotted name. + + :param (Environment) env: Odoo environment. + :param (str) model: Model name of the field to rename. + :param (str) new_field: New field name. + :param (str) old_field: Old field name. + """ + env.cr.execute( + """SELECT + if.id, if.domain, if.model_id + FROM + ir_filters if + WHERE + if.domain <> '[]'""", + ) + ir_filters_data = env.cr.fetchall() + for data in ir_filters_data: + filter_id, domain_str, first_model_name = data + new_domain_str = update_domain_str( + env, domain_str, first_model_name, model, new_field, old_field + ) + if new_domain_str: + logged_query( + env.cr, + """UPDATE ir_filters + SET domain = %s + WHERE id = %s + """, + (new_domain_str, filter_id), + ) + + +def rename_fields(env, field_spec, no_deep=False, dry=False): """Rename fields. Typically called in the pre script. WARNING: If using this on base module, pass the argument ``no_deep`` with True value for avoiding the using of the environment (which is not yet loaded). @@ -654,34 +844,36 @@ def rename_fields(env, field_spec, no_deep=False): :param no_deep: If True, avoids to perform any operation that involves the environment. Not used for now. """ + import wdb;wdb.set_trace() cr = env.cr for model, table, old_field, new_field in field_spec: - if column_exists(cr, table, old_field): - rename_columns(cr, {table: [(old_field, new_field)]}) - # Rename corresponding field entry - cr.execute( - """ - UPDATE ir_model_fields - SET name = %s - WHERE name = %s - AND model = %s - """, - (new_field, old_field, model), - ) - # Rename translations - if version_info[0] < 16: + if not dry: + if column_exists(cr, table, old_field): + rename_columns(cr, {table: [(old_field, new_field)]}) + # Rename corresponding field entry cr.execute( """ - UPDATE ir_translation + UPDATE ir_model_fields SET name = %s WHERE name = %s - AND type = 'model' + AND model = %s """, - ( - "%s,%s" % (model, new_field), - "%s,%s" % (model, old_field), - ), + (new_field, old_field, model), ) + # Rename translations + if version_info[0] < 16: + cr.execute( + """ + UPDATE ir_translation + SET name = %s + WHERE name = %s + AND type = 'model' + """, + ( + "%s,%s" % (model, new_field), + "%s,%s" % (model, old_field), + ), + ) # Rename possible attachments (if field is Binary with attachment=True) if column_exists(cr, "ir_attachment", "res_field"): cr.execute( @@ -693,37 +885,17 @@ def rename_fields(env, field_spec, no_deep=False): """, (new_field, model, old_field), ) - # Rename appearances on export profiles - # TODO: Rename when the field is part of a submodel (ex. m2one.field) - cr.execute( - """ - UPDATE ir_exports_line iel - SET name = %s - FROM ir_exports ie - WHERE iel.name = %s - AND ie.id = iel.export_id - AND ie.resource = %s - """, - (new_field, old_field, model), - ) - # Rename appearances on filters - # Example of replaced domain: [['field', '=', self], ...] - # TODO: Rename when the field is part of a submodel (ex. m2one.field) - cr.execute( - """ - UPDATE ir_filters - SET domain = regexp_replace( - domain, %(old_pattern)s, %(new_pattern)s, 'g' - ) - WHERE model_id = %%s - AND domain ~ %(old_pattern)s - """ - % { - "old_pattern": r"""$$('|")%s('|")$$""" % old_field, - "new_pattern": r"$$\1%s\2$$" % new_field, - }, - (model,), - ) + # # Rename appearances on export profiles + # Covered the renaming when the field is part of a submodel + # Example: 'field_x.old_field.name' -> 'field_x.new_field.name' + update_ir_exports_line(env, model, new_field, old_field) + + # # Rename appearances on filters + # Update field names inside domains, including dotted field names. + # Example: + # [('field_x.old_field.name', '=' name)] -> [('field_x.new_field.name', '=' name)] + update_ir_filters(env, model, new_field, old_field) + # Examples of replaced contexts: # {'group_by': ['field', 'other_field'], 'other_key':value} # {'group_by': ['date_field:month']} @@ -1833,6 +2005,7 @@ def m2o_to_x2m(cr, model, table, field, source_field): .. versionadded:: 8.0 """ + import wdb;wdb.set_trace() try: columns = model._fields except AttributeError: @@ -1896,6 +2069,13 @@ def m2o_to_x2m(cr, model, table, field, source_field): "m2o_to_x2m: field %s of model %s is not a " "many2many/one2many one" % (field, model._name) ) + if field != source_field: + rename_fields( + env=model.env, + field_spec=[(model._name, model._table, source_field, field)], + no_deep=False, + dry=True, + ) # Backwards compatibility diff --git a/requirements.txt b/requirements.txt index 2148b55e..6b7a2cec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pep8-naming lxml<=4.3.4 psycopg2==2.7.3.1 setuptools<58.0 +astunparse; python_version < '3.9'