From ef817c0f2dfbbcee0e6b994b814428e739e46f61 Mon Sep 17 00:00:00 2001 From: Joshua Kreuder Date: Wed, 15 Jul 2026 14:10:11 +0200 Subject: [PATCH] [FIX] module_auto_update: skip db-only modules in checksum computation Modules like studio_customization created by web_studio exist only in the database and have no filesystem directory. Previously, module_auto_update would include them in checksum computation, returning False for the checksum. This caused the first upgrade_changed_checksum() run to detect them as "changed" (False != None) and attempt to mark them for upgrade, which fails since the module has no filesystem path. This fix adds _module_exists_on_disk() helper and uses it in both _save_installed_checksums() and _get_modules_with_changed_checksum() to silently skip modules without a filesystem path. Signed-off-by: Joshua Kreuder --- module_auto_update/models/module.py | 11 ++++++++++- module_auto_update/tests/test_module.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/module_auto_update/models/module.py b/module_auto_update/models/module.py index 97b9325df4e..0789d01b9f2 100644 --- a/module_auto_update/models/module.py +++ b/module_auto_update/models/module.py @@ -45,6 +45,12 @@ def ensure_module_state(env, modules, state): class Module(models.Model): _inherit = "ir.module.module" + def _module_exists_on_disk(self): + """Check if the module has a directory on the filesystem.""" + self.ensure_one() + module_path = get_module_path(self.name) + return bool(module_path and os.path.isdir(module_path)) + def _get_checksum_dir(self): self.ensure_one() @@ -84,6 +90,8 @@ def _save_installed_checksums(self): checksums = {} installed_modules = self.search([("state", "=", "installed")]) for module in installed_modules: + if not module._module_exists_on_disk(): + continue checksums[module.name] = module._get_checksum_dir() self._save_checksums(checksums) @@ -96,7 +104,8 @@ def _get_modules_with_changed_checksum(self): saved_checksums = self._get_saved_checksums() installed_modules = self.search([("state", "=", "installed")]) return installed_modules.filtered( - lambda r: r._get_checksum_dir() != saved_checksums.get(r.name), + lambda r: r._module_exists_on_disk() + and r._get_checksum_dir() != saved_checksums.get(r.name), ) @api.model diff --git a/module_auto_update/tests/test_module.py b/module_auto_update/tests/test_module.py index 6929b3cae53..a6233b20c13 100644 --- a/module_auto_update/tests/test_module.py +++ b/module_auto_update/tests/test_module.py @@ -80,6 +80,23 @@ def test_get_modules_with_changed_checksum(self): Imm._save_installed_checksums() self.assertFalse(Imm._get_modules_with_changed_checksum()) + def test_get_modules_with_changed_checksum_skips_db_only_modules(self): + """Skip modules that exist only in DB (no filesystem path).""" + Imm = self.env["ir.module.module"] + base_module = Imm.search([("name", "=", "base")]) + # Mock _module_exists_on_disk to simulate a diskless module + with mock.patch.object( + type(base_module), "_module_exists_on_disk", return_value=False + ): + self.assertFalse(base_module._module_exists_on_disk()) + # Save checksums — base should be skipped + Imm._save_installed_checksums() + saved_checksums = Imm._get_saved_checksums() + self.assertNotIn("base", saved_checksums) + # Changed checksum detection should not include it + changed = Imm._get_modules_with_changed_checksum() + self.assertNotIn(base_module, changed) + @odoo.tests.tagged("post_install", "-at_install") class TestModuleAfterInstall(TransactionCase):