From 0064092d67f34292db839410826cbbbec6be9160 Mon Sep 17 00:00:00 2001 From: Roy Le Date: Thu, 1 Oct 2026 16:39:42 +0700 Subject: [PATCH] [FIX] base: reset the noupdate flag of the XML-IDs declared updatable Odoo never changes the noupdate flag of an existing XML-ID: a database upgraded from old versions keeps noupdate on records that the modules of 17.0 declare updatable (e.g. the groups of base, the languages, some views and actions), and the updates never write them: old names, missing categories and implied groups (Settings without Website Designer, Access Rights without Internal User...). Before the modules are loaded, the flag of the XML-IDs owned by an installed module is set as in a new database, i.e. from the first declaration of the record in the data files of the module. The records Odoo creates itself (base_data.sql, module categories) are left as they are. --- .../scripts/base/17.0.1.3/pre-migration.py | 93 +++++++++++++++++++ 1 file changed, 93 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 3c2f9b4ef40..f02bec251dc 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 @@ -1,11 +1,16 @@ # Copyright 2024 Viindoo Technology Joint Stock Company (Viindoo) # Copyright 2024 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import csv import logging +import os +from lxml import etree from openupgradelib import openupgrade from odoo import tools +from odoo.modules.module import get_manifest +from odoo.tools.misc import file_path from odoo.addons.openupgrade_scripts.apriori import ( # pylint: disable=odoo-addons-relative-import merged_modules, @@ -277,6 +282,93 @@ def _fix_address_format_placeholders(cr): ) +# created noupdate by base_data.sql, before any data file +_SQL_CREATED_XMLIDS = { + ("base", "USD"), + ("base", "main_company"), + ("base", "main_partner"), + ("base", "user_root"), + ("base", "group_user"), +} + + +def _xml_declarations(node, noupdate, declared): + """(XML-ID, noupdate) of the records of an XML data file, in order.""" + for child in node: + if not isinstance(child.tag, str): + continue + if child.tag == "data": + child_noupdate = child.get("noupdate", "0") in ("1", "True", "true") + _xml_declarations(child, child_noupdate, declared) + elif child.get("id") and child.tag not in ("function", "delete"): + declared.append((child.get("id"), noupdate)) + return declared + + +def _first_declarations(module): + """{name: noupdate} of the XML-IDs of ``module`` declared in its data files, + with the noupdate flag of their first declaration (manifest order): the flag + of a new database, as Odoo never changes the flag of an existing XML-ID.""" + first = {} + for fname in get_manifest(module).get("data", []): + try: + path = file_path(os.path.join(module, fname)) + except FileNotFoundError: + continue + if fname.endswith(".xml"): + root = etree.parse(path).getroot() + noupdate = root.get("noupdate", "0") in ("1", "True", "true") + declared = _xml_declarations(root, noupdate, []) + elif fname.endswith(".csv"): + with open(path, encoding="utf-8") as f: + rows = csv.DictReader(f) + declared = [(row["id"], False) for row in rows if row.get("id")] + else: + continue + for xmlid, noupdate in declared: + owner, _dot, name = xmlid.rpartition(".") + if owner in ("", module): + first.setdefault(name, noupdate) + return first + + +def _reset_stale_noupdate(cr): + """XML-IDs that a database upgraded from old versions has noupdate while + their module declares them updatable (e.g. the groups of base, noupdate since + 8.0 in some databases): the module updates never write them, they keep their + old name, category, implied groups, arch... Their flag is set as in a new + database, so that the update writes them.""" + cr.execute( + "SELECT name FROM ir_module_module WHERE state IN ('installed', 'to upgrade')" + ) + for (module,) in cr.fetchall(): + if not get_manifest(module): + continue + names = [ + name + for name, noupdate in _first_declarations(module).items() + if not noupdate and (module, name) not in _SQL_CREATED_XMLIDS + ] + if not names: + continue + cr.execute( + """ + UPDATE ir_model_data SET noupdate = FALSE + WHERE module = %s AND name IN %s AND noupdate + AND model NOT IN ('ir.module.category', 'ir.module.module') + RETURNING name + """, + (module, tuple(names)), + ) + reset = [name for (name,) in cr.fetchall()] + if reset: + _logger.info( + "%s: noupdate reset on %s, declared updatable by the module", + module, + ", ".join(sorted(reset)), + ) + + @openupgrade.migrate(use_env=False) def migrate(cr, version): """ @@ -300,3 +392,4 @@ def migrate(cr, version): _fill_empty_country_codes(cr) _handle_partner_private_type(cr) _fix_address_format_placeholders(cr) + _reset_stale_noupdate(cr)