From 1d289b8d6484de57e00c24d6c45febd04efc89f4 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Tue, 14 Jul 2026 19:09:10 +0200 Subject: [PATCH 1/4] [IMP] upgrade_analysis: generate changes for renamed noupdate records --- upgrade_analysis/compare.py | 2 +- upgrade_analysis/models/upgrade_analysis.py | 49 ++++++- upgrade_analysis/tests/test_module.py | 140 ++++++++++++++++++++ 3 files changed, 187 insertions(+), 4 deletions(-) diff --git a/upgrade_analysis/compare.py b/upgrade_analysis/compare.py index 044e4a959ec..8760cc09834 100644 --- a/upgrade_analysis/compare.py +++ b/upgrade_analysis/compare.py @@ -540,7 +540,7 @@ def match(match_fields, match_type="direct"): if entry["noupdate_switched"]: content += " (noupdate switched)" reprs[module_map(entry["module"])].append(content) - return reprs + return reprs, moved_records, renamed_records, modified_records def compare_model_sets(old_records, new_records): diff --git a/upgrade_analysis/models/upgrade_analysis.py b/upgrade_analysis/models/upgrade_analysis.py index 8eefad21ba7..f60ca97afe6 100644 --- a/upgrade_analysis/models/upgrade_analysis.py +++ b/upgrade_analysis/models/upgrade_analysis.py @@ -164,7 +164,9 @@ def analyze(self): {field: record[field] for field in flds} for record in RemoteRecord.read(remote_xml_record_ids, flds) ] - res_xml = compare.compare_xml_sets(remote_xml_records, local_xml_records) + res_xml, moved_xml_records, renamed_xml_records, modified_xml_records = ( + compare.compare_xml_sets(remote_xml_records, local_xml_records) + ) # Retrieve model representations and compare flds = [ @@ -270,7 +272,9 @@ def analyze(self): ) noupdate_modules = [] try: - noupdate_modules = self.generate_noupdate_changes() + noupdate_modules = self.generate_noupdate_changes( + moved_xml_records, renamed_xml_records, modified_xml_records + ) except Exception as e: _logger.exception(f"Error generating noupdate changes: {e}") general_log += "ERROR: error when generating noupdate changes: {e}\n" @@ -486,7 +490,9 @@ def _parse_files(self, xml_files, module_name): return records_update, records_noupdate - def generate_noupdate_changes(self): + def generate_noupdate_changes( + self, moved_xml_records, renamed_xml_records, modified_xml_records + ): """Communicate with the remote server to fetch all xml data records per module, and generate a diff in XML format that can be imported from the module's migration script using openupgrade.load_data() @@ -498,6 +504,14 @@ def generate_noupdate_changes(self): local_modules = local_record_obj.list_modules() all_remote_modules = remote_record_obj.list_modules() changed_modules = [] + # {new_module: {name: previous_module}} + renamed_xmlids = {} + for renamed_xml_record in renamed_xml_records: + if renamed_xml_record.get("old") or not renamed_xml_record.get("new"): + continue + renamed_xmlids.setdefault(renamed_xml_record["module"], {}).update( + {renamed_xml_record["suffix"]: renamed_xml_record["renamed"]} + ) for local_module in local_modules: remote_files = [] remote_modules = [] @@ -515,6 +529,35 @@ def generate_noupdate_changes(self): ) remote_update.update(add_remote_update) remote_noupdate.update(add_remote_noupdate) + if any( + renamed_from_module == remote_module + for renamed_from_module in renamed_xmlids.get( + local_module, {} + ).values() + ): + # if xmlids have been renamed (moved) to the current module, query + # their definition from the module that contained it previously + remote_modules.append(remote_module) + renamed_from_module_files = remote_record_obj.get_xml_records( + remote_module + ) + renamed_from_module_update, renamed_from_module_noupdate = ( + self._parse_files(renamed_from_module_files, remote_module) + ) + remote_update.update( + { + name: xml + for name, xml in renamed_from_module_update.items() + if renamed_xmlids[local_module].get(name) == remote_module + } + ) + remote_noupdate.update( + { + name: xml + for name, xml in renamed_from_module_noupdate.items() + if renamed_xmlids[local_module].get(name) == remote_module + } + ) if not remote_modules: continue local_files = local_record_obj.get_xml_records(local_module) diff --git a/upgrade_analysis/tests/test_module.py b/upgrade_analysis/tests/test_module.py index 65d93b8d924..f7cfd34cea3 100644 --- a/upgrade_analysis/tests/test_module.py +++ b/upgrade_analysis/tests/test_module.py @@ -1,4 +1,5 @@ from copy import deepcopy +from unittest.mock import patch from lxml import etree @@ -152,3 +153,142 @@ def test_xml_comparison(self): ) self.assertIn('', diff) self.assertIn('', diff) + + def test_analyze(self): + """ + Test a full analysis run. + For the time being, only xmlid related functionality is tested + """ + analysis = self.env["upgrade.analysis"].create( + { + "config_id": self.env["upgrade.comparison.config"] + .create( + { + "database": self.env.cr.dbname, + } + ) + .id, + } + ) + upgrade_analysis_version = ( + self.env["ir.module.module"] + .search([("name", "=", "upgrade_analysis")]) + .latest_version + ) + + self.env["upgrade.record"].create( + { + "name": "upgrade_analysis.test_noupdate_xmlid", + "mode": "create", + "type": "xmlid", + "module": "upgrade_analysis", + "model": "upgrade.comparison.config", + "noupdate": True, + } + ) + + class RemoteUpgradeRecord: + _records = { + 1: { + "name": "other_module.test_noupdate_xmlid", + "mode": "create", + "type": "xmlid", + "module": "other_module", + "prefix": "other_module", + "model": "upgrade.comparison.config", + "noupdate": True, + "suffix": "test_noupdate_xmlid", + }, + } + + def search(self, domain): + if domain == [("type", "=", "xmlid")]: + return [ + _id + for _id, vals in self._records.items() + if vals["type"] == "xmlid" + ] + return [] + + def read(self, ids, fields): # pylint: disable=method-required-super + return [ + {field: record.get(field) for field in fields} + for _id, record in self._records.items() + if _id in ids + ] + + def field_dump(self): + return [] + + def list_modules(self): + return set(record["module"] for record in self._records.values()) + + def get_xml_records(self, module): + if module == "other_module": + return [ + """ + + + Noupdate xmlid from other_module + some_server + + + """ + ] + + def local_get_xml_records(module): + if module == "upgrade_analysis": + return [ + """ + + + Noupdate xmlid from upgrade_analysis + some_server + + + """ + ] + + written_files = {} + + def write_file(module_name, version, content, filename="upgrade_analysis.txt"): + written_files[f"{module_name}-{version}-{filename}"] = content + + with ( + patch.object(analysis.config_id.__class__, "get_connection"), + patch.object( + analysis.__class__, "_get_remote_model" + ) as patched_get_remote_model, + patch.object(analysis.__class__, "_write_file") as patched_write_file, + patch.object( + self.env["upgrade.record"].__class__, "get_xml_records" + ) as patched_get_xml_records, + ): + patched_get_remote_model.side_effect = lambda *args: RemoteUpgradeRecord() + patched_get_xml_records.side_effect = local_get_xml_records + patched_write_file.side_effect = write_file + analysis.analyze() + + expected_noupdate_content = """ + + + Noupdate xmlid from upgrade_analysis + + +""" + self.assertEqual( + written_files[ + f"upgrade_analysis-{upgrade_analysis_version}-noupdate_changes.xml" + ], + expected_noupdate_content, + ) From 63acacb5b619465c4b03db8341396eef955a3969 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 15 Jul 2026 08:33:09 +0000 Subject: [PATCH 2/4] [BOT] post-merge updates --- README.md | 2 +- upgrade_analysis/README.rst | 2 +- upgrade_analysis/__manifest__.py | 2 +- upgrade_analysis/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 82ba5aa676e..be29c22e4c4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ addon | version | maintainers | summary [test_auditlog](test_auditlog/) | 19.0.1.0.0 | | Additional unit tests for Audit Log based on accounting models [tracking_manager](tracking_manager/) | 19.0.1.0.0 | Kev-Roche sebastienbeau | This module tracks all fields of a model, including one2many and many2many ones. [tracking_manager_domain](tracking_manager_domain/) | 19.0.1.0.0 | CRogos | This module extends the tracking manager to allow to define a domain on fields to track changes only when certain conditions apply. -[upgrade_analysis](upgrade_analysis/) | 19.0.1.0.4 | StefanRijnhart legalsylvain | Performs a difference analysis between modules installed on two different Odoo instances +[upgrade_analysis](upgrade_analysis/) | 19.0.1.1.0 | StefanRijnhart legalsylvain | Performs a difference analysis between modules installed on two different Odoo instances [//]: # (end addons) diff --git a/upgrade_analysis/README.rst b/upgrade_analysis/README.rst index a04184f09af..0f02f6fbfe4 100644 --- a/upgrade_analysis/README.rst +++ b/upgrade_analysis/README.rst @@ -11,7 +11,7 @@ Upgrade Analysis !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:89a10d9f558bdf888f62fbc7c03e763b17b3ee1c836644ed5dc752d1fafe9dd9 + !! source digest: sha256:112f61f3fb75494e71f3c9b69211ed9067623258eca4d6c6c531981be496fefe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/upgrade_analysis/__manifest__.py b/upgrade_analysis/__manifest__.py index bdb164acd0e..7fd6ae0513c 100644 --- a/upgrade_analysis/__manifest__.py +++ b/upgrade_analysis/__manifest__.py @@ -5,7 +5,7 @@ "name": "Upgrade Analysis", "summary": "Performs a difference analysis between modules" " installed on two different Odoo instances", - "version": "19.0.1.0.4", + "version": "19.0.1.1.0", "category": "Migration", "author": "Therp BV, Opener B.V., GRAP, Odoo Community Association (OCA)", "maintainers": ["StefanRijnhart", "legalsylvain"], diff --git a/upgrade_analysis/static/description/index.html b/upgrade_analysis/static/description/index.html index 65e50089f39..dd3aae1284a 100644 --- a/upgrade_analysis/static/description/index.html +++ b/upgrade_analysis/static/description/index.html @@ -372,7 +372,7 @@

Upgrade Analysis

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:89a10d9f558bdf888f62fbc7c03e763b17b3ee1c836644ed5dc752d1fafe9dd9 +!! source digest: sha256:112f61f3fb75494e71f3c9b69211ed9067623258eca4d6c6c531981be496fefe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

This module provides the tool to generate the database analysis files From 1be1ccf9debf87673c44e53f2b433f16746559fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miquel=20Ra=C3=AFch?= Date: Wed, 15 Jul 2026 12:35:17 +0200 Subject: [PATCH 3/4] [FIX] upgrade_analysis: reset discarded fields to their default, not falsy value When the previous version set a field explicitly and the new version omits it, _get_xml_diff generated eval="False" using field.falsy_value. This is wrong for fields whose default is truthy (e.g. ir.rule.perm_read/write/create/unlink or active): a fresh install applies the field's default, not its falsy value. The generated noupdate_changes.xml then mismatched a clean install and could even violate model constraints (e.g. ir.rule's _no_access_rights CHECK), breaking the migration. Use model.default_get() to reset omitted fields to the value a fresh install would give them, falling back to falsy_value when there is no default or it is not a scalar. --- upgrade_analysis/models/upgrade_analysis.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/upgrade_analysis/models/upgrade_analysis.py b/upgrade_analysis/models/upgrade_analysis.py index f60ca97afe6..23b42785911 100644 --- a/upgrade_analysis/models/upgrade_analysis.py +++ b/upgrade_analysis/models/upgrade_analysis.py @@ -370,11 +370,23 @@ def _get_xml_diff( "name" }: # if previous version has set a field but current version - # doesn't, set whatever the NULL value of the field is - # (usually None) + # doesn't, reset it to the value a fresh install would give + # it: the field's default. Only fall back to the field's + # falsy value when there is no default. Using falsy_value + # unconditionally is wrong for fields whose default is truthy + # (e.g. ir.rule.perm_* or active) and can even produce + # records that violate model constraints. model = self.env[local_record.attrib["model"]] field = model._fields[attribs["name"]] - eval_constant = ast.unparse(ast.Constant(field.falsy_value)) + reset_value = model.default_get([field.name]).get( + field.name, field.falsy_value + ) + try: + eval_constant = ast.unparse(ast.Constant(reset_value)) + except ValueError: + # non-scalar default (e.g. x2many command list): + # keep the previous behaviour + eval_constant = ast.unparse(ast.Constant(field.falsy_value)) if eval_constant != "''": attribs["eval"] = eval_constant element.append(etree.Element(record_remote_dict[key].tag, attribs)) From f7ada69a1e8ba73e19006663e3b09ecca2cbe348 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 15 Jul 2026 12:05:14 +0000 Subject: [PATCH 4/4] [BOT] post-merge updates --- README.md | 2 +- upgrade_analysis/README.rst | 2 +- upgrade_analysis/__manifest__.py | 2 +- upgrade_analysis/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index be29c22e4c4..6277f7a3db4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ addon | version | maintainers | summary [test_auditlog](test_auditlog/) | 19.0.1.0.0 | | Additional unit tests for Audit Log based on accounting models [tracking_manager](tracking_manager/) | 19.0.1.0.0 | Kev-Roche sebastienbeau | This module tracks all fields of a model, including one2many and many2many ones. [tracking_manager_domain](tracking_manager_domain/) | 19.0.1.0.0 | CRogos | This module extends the tracking manager to allow to define a domain on fields to track changes only when certain conditions apply. -[upgrade_analysis](upgrade_analysis/) | 19.0.1.1.0 | StefanRijnhart legalsylvain | Performs a difference analysis between modules installed on two different Odoo instances +[upgrade_analysis](upgrade_analysis/) | 19.0.1.1.1 | StefanRijnhart legalsylvain | Performs a difference analysis between modules installed on two different Odoo instances [//]: # (end addons) diff --git a/upgrade_analysis/README.rst b/upgrade_analysis/README.rst index 0f02f6fbfe4..6a77d2e0e53 100644 --- a/upgrade_analysis/README.rst +++ b/upgrade_analysis/README.rst @@ -11,7 +11,7 @@ Upgrade Analysis !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:112f61f3fb75494e71f3c9b69211ed9067623258eca4d6c6c531981be496fefe + !! source digest: sha256:2f7fa794d0292655d9bb5907cd6c1dacce384ed506e9b9c7f6b0ae19ef3a2c93 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/upgrade_analysis/__manifest__.py b/upgrade_analysis/__manifest__.py index 7fd6ae0513c..475441cfc32 100644 --- a/upgrade_analysis/__manifest__.py +++ b/upgrade_analysis/__manifest__.py @@ -5,7 +5,7 @@ "name": "Upgrade Analysis", "summary": "Performs a difference analysis between modules" " installed on two different Odoo instances", - "version": "19.0.1.1.0", + "version": "19.0.1.1.1", "category": "Migration", "author": "Therp BV, Opener B.V., GRAP, Odoo Community Association (OCA)", "maintainers": ["StefanRijnhart", "legalsylvain"], diff --git a/upgrade_analysis/static/description/index.html b/upgrade_analysis/static/description/index.html index dd3aae1284a..c6aa59edbda 100644 --- a/upgrade_analysis/static/description/index.html +++ b/upgrade_analysis/static/description/index.html @@ -372,7 +372,7 @@

Upgrade Analysis

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:112f61f3fb75494e71f3c9b69211ed9067623258eca4d6c6c531981be496fefe +!! source digest: sha256:2f7fa794d0292655d9bb5907cd6c1dacce384ed506e9b9c7f6b0ae19ef3a2c93 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

This module provides the tool to generate the database analysis files