From ebbd6466a60893e71b0b7f620dd4adcade0350cd Mon Sep 17 00:00:00 2001 From: Roy Le Date: Sun, 27 Sep 2026 17:13:11 +0700 Subject: [PATCH 1/2] [FIX] openupgrade_framework: delete_record_translations only on jsonb columns openupgradelib.delete_record_translations picks every field flagged translate in ir_model_fields and applies the jsonb operator ? to the column of that name. Such a column is not always jsonb: it can belong to a module not loaded yet, or be a column left behind by an older version. The account_edi 17.0 post-migration died that way on ir_cron.name, a varchar column from the time the name was stored on ir_cron (the field is now delegated to ir.actions.server): "operator does not exist: character varying ? unknown" (tvtma, upgrading from 16.0; 18 scripts call the helper). Restrict the call to the translated columns that are jsonb already. (cherry picked from commit a6f47d8d80149361ef8e48dd14014ab2362a664b) --- openupgrade_framework/__init__.py | 1 + openupgrade_framework/openupgradelib_patch.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 openupgrade_framework/openupgradelib_patch.py diff --git a/openupgrade_framework/__init__.py b/openupgrade_framework/__init__.py index 2aee639f0d3d..6b8dea598600 100644 --- a/openupgrade_framework/__init__.py +++ b/openupgrade_framework/__init__.py @@ -5,6 +5,7 @@ from odoo.tools import config from . import odoo_patch +from . import openupgradelib_patch if not config.get("upgrade_path"): path = get_module_path("openupgrade_scripts", display_warning=False) diff --git a/openupgrade_framework/openupgradelib_patch.py b/openupgrade_framework/openupgradelib_patch.py new file mode 100644 index 000000000000..2b1eb336fe26 --- /dev/null +++ b/openupgrade_framework/openupgradelib_patch.py @@ -0,0 +1,56 @@ +# Copyright Odoo Community Association (OCA) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openupgradelib import openupgrade + + +def _jsonb_translated_columns(cr, model, table): + cr.execute( + """ + SELECT isc.column_name + FROM information_schema.columns isc + JOIN ir_model_fields imf ON imf.name = isc.column_name AND imf.model = %s + WHERE isc.table_name = %s AND imf.translate AND isc.data_type = 'jsonb' + """, + (model, table), + ) + return [name for (name,) in cr.fetchall()] + + +def delete_record_translations(cr, module, xml_ids, field_list=None): + """Only touch the translated columns that are jsonb already. + + openupgradelib picks every field flagged translate in ir_model_fields and + applies the jsonb operator ? to its column. The translated columns of a + module that has not been loaded yet are still text at that point (e.g. the + website SEO fields of ir.ui.view while digest migrates), and the query dies + on "operator does not exist: text ? unknown". Those columns get their + translations converted when their own module loads. + """ + if not isinstance(xml_ids, (list, tuple)) or not xml_ids: + return delete_record_translations._original_method( + cr, module, xml_ids, field_list=field_list + ) + cr.execute( + """SELECT model, array_agg(name) FROM ir_model_data + WHERE module = %s AND name IN %s GROUP BY model""", + (module, tuple(xml_ids)), + ) + for model, names in cr.fetchall(): + table = openupgrade.get_model2table(model) + if not openupgrade.table_exists(cr, table): + continue + columns = _jsonb_translated_columns(cr, model, table) + if field_list: + columns = [c for c in columns if c in field_list] + if not columns: + continue + delete_record_translations._original_method( + cr, module, names, field_list=columns + ) + + +if openupgrade.version_info[0] >= 16: + delete_record_translations._original_method = ( + openupgrade.delete_record_translations + ) + openupgrade.delete_record_translations = delete_record_translations From 808594e05f6930f5ff2dcd03377e9a3da4c80448 Mon Sep 17 00:00:00 2001 From: Roy Le Date: Tue, 29 Sep 2026 11:08:15 +0700 Subject: [PATCH 2/2] [FIX] base: repair address formats with a placeholder that lacks its conversion type In '%(street2)' followed by a line break, the line break becomes the conversion type and formatting the address raises "unsupported format character". Odoo shipped that format for Japan in 11.0 (13ee5596c6dd, fixed by 087bebf87a64); countries are noupdate data, so databases that loaded it back then still have it. The end-migration of account_edi_ubl_cii writes on every partner with a country, which computes the partner's address, and the 16.0 -> 17.0 upgrade of tvtma died there on its two Japanese partners. Add the missing conversion type to such placeholders before anything reads the formats. --- .../scripts/base/17.0.1.3/pre-migration.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openupgrade_scripts/scripts/base/17.0.1.3/pre-migration.py b/openupgrade_scripts/scripts/base/17.0.1.3/pre-migration.py index cb452dc7ed37..3c2f9b4ef402 100644 --- a/openupgrade_scripts/scripts/base/17.0.1.3/pre-migration.py +++ b/openupgrade_scripts/scripts/base/17.0.1.3/pre-migration.py @@ -256,6 +256,27 @@ def _handle_partner_private_type(cr): ) +def _fix_address_format_placeholders(cr): + """A placeholder without its conversion type breaks the display of every + address of the country: after '%(street2)' the line break that follows + becomes the conversion type, and formatting raises "unsupported format + character". + Odoo shipped that format for Japan in 11.0 (13ee5596c6dd, fixed by + 087bebf87a64) and the countries are noupdate data, so databases that loaded it + back then still have it. The end-migration of account_edi_ubl_cii writes on + every partner with a country, which computes their address and dies on it. + """ + pattern = r"(%\(\w+\))(?![a-zA-Z])" + openupgrade.logged_query( + cr, + """ + UPDATE res_country + SET address_format = regexp_replace(address_format, %s, %s, 'g') + WHERE address_format ~ %s""", + (pattern, r"\1s", pattern), + ) + + @openupgrade.migrate(use_env=False) def migrate(cr, version): """ @@ -278,3 +299,4 @@ def migrate(cr, version): _fill_ir_server_object_lines_into_action_server(cr) _fill_empty_country_codes(cr) _handle_partner_private_type(cr) + _fix_address_format_placeholders(cr)