From cd5cc69bf696b4fe7d46d50d08ba03a1cf466370 Mon Sep 17 00:00:00 2001 From: Sebastian Glasl Date: Fri, 31 Jul 2026 16:06:13 +0200 Subject: [PATCH 1/6] FIX: ignore module "studio_customization" that is only present in the database, The Odoo Enterprise "Studio" module creates a new module "studio_customization" in the database. Since we're only comparing checksums of modules stored in the file system, ignoring this fixes the update mechanism on Odoo Enterprise installations. --- module_auto_update/models/module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module_auto_update/models/module.py b/module_auto_update/models/module.py index 86a9c584b41..7e9f9878665 100644 --- a/module_auto_update/models/module.py +++ b/module_auto_update/models/module.py @@ -81,19 +81,19 @@ def _save_checksums(self, checksums): @api.model def _save_installed_checksums(self): checksums = {} - installed_modules = self.search([("state", "=", "installed")]) + installed_modules = self.search(["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")]) for module in installed_modules: checksums[module.name] = module._get_checksum_dir() self._save_checksums(checksums) @api.model def _get_modules_partially_installed(self): - return self.search([("state", "in", ("to install", "to remove", "to upgrade"))]) + return self.search(["&", ("state", "in", ["to install", "to remove", "to upgrade"]), ("name", "!=", "studio_customization")]) @api.model def _get_modules_with_changed_checksum(self): saved_checksums = self._get_saved_checksums() - installed_modules = self.search([("state", "=", "installed")]) + installed_modules = self.search(["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")]) return installed_modules.filtered( lambda r: r._get_checksum_dir() != saved_checksums.get(r.name), ) From 102caef393ceac27fc1894f0550897fb80a324f7 Mon Sep 17 00:00:00 2001 From: Sebastian Glasl Date: Fri, 31 Jul 2026 17:09:45 +0200 Subject: [PATCH 2/6] [FIX] format with ruff -> fix E501 Line too long --- module_auto_update/models/module.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/module_auto_update/models/module.py b/module_auto_update/models/module.py index 7e9f9878665..700efc9236f 100644 --- a/module_auto_update/models/module.py +++ b/module_auto_update/models/module.py @@ -31,7 +31,7 @@ def ensure_module_state(env, modules, state): if not modules: return env.cr.execute( - "SELECT name FROM ir_module_module " "WHERE id IN %s AND state != %s", + "SELECT name FROM ir_module_module WHERE id IN %s AND state != %s", (tuple(modules.ids), state), ) names = [r[0] for r in env.cr.fetchall()] @@ -81,19 +81,29 @@ def _save_checksums(self, checksums): @api.model def _save_installed_checksums(self): checksums = {} - installed_modules = self.search(["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")]) + installed_modules = self.search( + ["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")] + ) for module in installed_modules: checksums[module.name] = module._get_checksum_dir() self._save_checksums(checksums) @api.model def _get_modules_partially_installed(self): - return self.search(["&", ("state", "in", ["to install", "to remove", "to upgrade"]), ("name", "!=", "studio_customization")]) + return self.search( + [ + "&", + ("state", "in", ["to install", "to remove", "to upgrade"]), + ("name", "!=", "studio_customization"), + ] + ) @api.model def _get_modules_with_changed_checksum(self): saved_checksums = self._get_saved_checksums() - installed_modules = self.search(["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")]) + installed_modules = self.search( + ["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")] + ) return installed_modules.filtered( lambda r: r._get_checksum_dir() != saved_checksums.get(r.name), ) From cf83e1013854606b31bc9681424c0385d795d77d Mon Sep 17 00:00:00 2001 From: Sebastian Glasl Date: Fri, 31 Jul 2026 17:43:30 +0200 Subject: [PATCH 3/6] [IMP] don't create/check checksums for modules with the field `imported = True`. This applies to studio_customizations, too, since it's only imported in the database. Mitigates breaking the updater, when other modules are imported via the web interface. --- module_auto_update/models/module.py | 6 ++--- module_auto_update/tests/test_module.py | 33 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/module_auto_update/models/module.py b/module_auto_update/models/module.py index 700efc9236f..a4aba2a94b6 100644 --- a/module_auto_update/models/module.py +++ b/module_auto_update/models/module.py @@ -82,7 +82,7 @@ def _save_checksums(self, checksums): def _save_installed_checksums(self): checksums = {} installed_modules = self.search( - ["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")] + ["&", ("state", "=", "installed"), ("imported", "=", False)] ) for module in installed_modules: checksums[module.name] = module._get_checksum_dir() @@ -94,7 +94,7 @@ def _get_modules_partially_installed(self): [ "&", ("state", "in", ["to install", "to remove", "to upgrade"]), - ("name", "!=", "studio_customization"), + ("imported", "=", False), ] ) @@ -102,7 +102,7 @@ def _get_modules_partially_installed(self): def _get_modules_with_changed_checksum(self): saved_checksums = self._get_saved_checksums() installed_modules = self.search( - ["&", ("state", "=", "installed"), ("name", "!=", "studio_customization")] + ["&", ("state", "=", "installed"), ("imported", "=", False)] ) return installed_modules.filtered( lambda r: r._get_checksum_dir() != saved_checksums.get(r.name), diff --git a/module_auto_update/tests/test_module.py b/module_auto_update/tests/test_module.py index 14e18fb0feb..6c1b6307893 100644 --- a/module_auto_update/tests/test_module.py +++ b/module_auto_update/tests/test_module.py @@ -80,6 +80,39 @@ def test_get_modules_with_changed_checksum(self): self.assertFalse(Imm._get_modules_with_changed_checksum()) +class TestImportedModules(TransactionCase): + def setUp(self): + super().setUp() + self.Imm = self.env["ir.module.module"] + # Modules imported into the database (e.g. Odoo Studio customizations) + # have no counterpart on the file system, so no checksum can be + # computed for them. + self.imported_module = self.Imm.create( + { + "name": "test_imported_module", + "state": "installed", + "imported": True, + } + ) + + def test_save_installed_checksums_skips_imported(self): + self.Imm._save_installed_checksums() + self.assertNotIn("test_imported_module", self.Imm._get_saved_checksums()) + + def test_changed_checksum_skips_imported(self): + # no checksum was ever saved for the imported module + self.Imm._save_checksums({}) + self.assertNotIn( + self.imported_module, self.Imm._get_modules_with_changed_checksum() + ) + + def test_partially_installed_skips_imported(self): + self.imported_module.state = "to upgrade" + self.assertNotIn( + self.imported_module, self.Imm._get_modules_partially_installed() + ) + + @odoo.tests.tagged("post_install", "-at_install") class TestModuleAfterInstall(TransactionCase): def setUp(self): From effda186e14915f7adb7aaf7cb42eeefef1c831b Mon Sep 17 00:00:00 2001 From: Sebastian Glasl Date: Tue, 1 Sep 2026 13:06:30 +0200 Subject: [PATCH 4/6] Fix failing test for OCB (base_import_module not installed) --- module_auto_update/tests/test_module.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/module_auto_update/tests/test_module.py b/module_auto_update/tests/test_module.py index 6c1b6307893..3e0f3cffe01 100644 --- a/module_auto_update/tests/test_module.py +++ b/module_auto_update/tests/test_module.py @@ -84,6 +84,11 @@ class TestImportedModules(TransactionCase): def setUp(self): super().setUp() self.Imm = self.env["ir.module.module"] + # Skip test if base_import_module is not installed (OCB) + if "imported" not in self.Imm._fields: + # the field comes from base_import_module, which is not a + # dependency of this module + self.skipTest("base_import_module is not installed") # Modules imported into the database (e.g. Odoo Studio customizations) # have no counterpart on the file system, so no checksum can be # computed for them. From e98846e4a0aa7d81b6f692b0db7137233aa5769b Mon Sep 17 00:00:00 2001 From: Sebastian Glasl Date: Tue, 1 Sep 2026 15:36:03 +0200 Subject: [PATCH 5/6] Remove redundant comment --- module_auto_update/tests/test_module.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/module_auto_update/tests/test_module.py b/module_auto_update/tests/test_module.py index 3e0f3cffe01..c18c195c7dd 100644 --- a/module_auto_update/tests/test_module.py +++ b/module_auto_update/tests/test_module.py @@ -86,8 +86,6 @@ def setUp(self): self.Imm = self.env["ir.module.module"] # Skip test if base_import_module is not installed (OCB) if "imported" not in self.Imm._fields: - # the field comes from base_import_module, which is not a - # dependency of this module self.skipTest("base_import_module is not installed") # Modules imported into the database (e.g. Odoo Studio customizations) # have no counterpart on the file system, so no checksum can be From b63dd1cfb3733f716066fc07795587e41fad22bf Mon Sep 17 00:00:00 2001 From: Sebastian Glasl Date: Tue, 1 Sep 2026 15:39:10 +0200 Subject: [PATCH 6/6] Fix module using the non-existing field 'imported' in environments like OCB, where 'base_import_module' might not be installed by default. --- module_auto_update/models/module.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/module_auto_update/models/module.py b/module_auto_update/models/module.py index a4aba2a94b6..2c714ca5e33 100644 --- a/module_auto_update/models/module.py +++ b/module_auto_update/models/module.py @@ -78,11 +78,27 @@ def _save_checksums(self, checksums): Icp.set_param(PARAM_INSTALLED_CHECKSUMS, json.dumps(checksums)) Icp.flush_model() + @api.model + def _not_imported_domain(self): + """Domain excluding modules that only exist in the database. + + Imported modules (Odoo Studio customizations, modules uploaded through + the web interface) have no counterpart on the file system, so no + checksum can be computed for them. + + The ``imported`` field is added by ``base_import_module``, which is not + a dependency of this module. When it is not installed, no module can be + imported in the first place, so no filtering is needed. + """ + if "imported" in self._fields: + return [("imported", "=", False)] + return [] + @api.model def _save_installed_checksums(self): checksums = {} installed_modules = self.search( - ["&", ("state", "=", "installed"), ("imported", "=", False)] + [("state", "=", "installed"), *self._not_imported_domain()] ) for module in installed_modules: checksums[module.name] = module._get_checksum_dir() @@ -92,9 +108,8 @@ def _save_installed_checksums(self): def _get_modules_partially_installed(self): return self.search( [ - "&", ("state", "in", ["to install", "to remove", "to upgrade"]), - ("imported", "=", False), + *self._not_imported_domain(), ] ) @@ -102,7 +117,7 @@ def _get_modules_partially_installed(self): def _get_modules_with_changed_checksum(self): saved_checksums = self._get_saved_checksums() installed_modules = self.search( - ["&", ("state", "=", "installed"), ("imported", "=", False)] + [("state", "=", "installed"), *self._not_imported_domain()] ) return installed_modules.filtered( lambda r: r._get_checksum_dir() != saved_checksums.get(r.name),