From 9619124626fd4bb2ba4eb761afb113fe915052ee Mon Sep 17 00:00:00 2001 From: shiny-code-bot Date: Tue, 29 Sep 2026 23:38:40 -0400 Subject: [PATCH] Upgrade a configured module list with its installed local dependencies Odoo's -u upgrades a module's dependents, not its dependencies, so a deploy that names the tenant's install list never loaded data changes in the local addons it depends on until the next restore. Add their installed local dependency closure, the same closure AUTO already uses. Refs #142 --- docker/scripts/run_odoo_data_workflows.py | 40 +++++++++++++++-------- tests/test_odoo_data_workflows.py | 9 +++++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docker/scripts/run_odoo_data_workflows.py b/docker/scripts/run_odoo_data_workflows.py index ac4f825..23ac582 100644 --- a/docker/scripts/run_odoo_data_workflows.py +++ b/docker/scripts/run_odoo_data_workflows.py @@ -1935,6 +1935,20 @@ def _project_addon_group_dirs() -> list[Path]: grouped_dirs.append(child_path) return grouped_dirs + def _local_dependency_closure(self, modules: Sequence[str], local_module_paths: dict[str, Path]) -> set[str]: + """Return the modules plus every local addon they depend on, directly or not.""" + closure = set(modules) + pending = list(modules) + while pending: + addon_path = local_module_paths.get(pending.pop()) + if not addon_path: + continue + for dependency_name in self._load_manifest_dependencies(addon_path): + if dependency_name in local_module_paths and dependency_name not in closure: + closure.add(dependency_name) + pending.append(dependency_name) + return closure + @staticmethod def _load_manifest_dependencies(addon_path: Path) -> list[str]: manifest_path = addon_path / "__manifest__.py" @@ -2053,19 +2067,7 @@ def update_addons(self, explicit_modules: Sequence[str] | None = None, reason: s _logger.info("ODOO_UPDATE_MODULES unset/AUTO and no installed local modules detected; skipping.") return mode_label = mods_env.upper() if mods_env else "AUTO" - desired_set = set(installed_local_modules) - pending = list(installed_local_modules) - while pending: - module_name = pending.pop() - addon_path = local_module_paths.get(module_name) - if not addon_path: - continue - for dependency_name in self._load_manifest_dependencies(addon_path): - if dependency_name not in local_modules: - continue - if dependency_name not in desired_set: - desired_set.add(dependency_name) - pending.append(dependency_name) + desired_set = self._local_dependency_closure(installed_local_modules, local_module_paths) missing_dependencies = sorted(name for name in desired_set if name not in installed_modules) if missing_dependencies: _logger.info( @@ -2087,6 +2089,18 @@ def update_addons(self, explicit_modules: Sequence[str] | None = None, reason: s if not desired: _logger.info("ODOO_UPDATE_MODULES is empty after parsing; skipping.") return + # Odoo's -u upgrades a module's dependents, not its dependencies, so a + # changed local dependency (a new cron, view or record) would otherwise + # load only on the next restore. + closure = self._local_dependency_closure(desired, self._resolve_local_module_paths()) + installed_modules = self._installed_modules() + installed_dependencies = sorted(name for name in closure - set(desired) if name in installed_modules) + if installed_dependencies: + _logger.info( + "ODOO_UPDATE_MODULES; also upgrading installed local dependencies: %s", + ", ".join(installed_dependencies), + ) + desired = [*desired, *installed_dependencies] modules_source_label = "ODOO_UPDATE_MODULES" if explicit_modules is not None: diff --git a/tests/test_odoo_data_workflows.py b/tests/test_odoo_data_workflows.py index dd0c48b..3f80891 100644 --- a/tests/test_odoo_data_workflows.py +++ b/tests/test_odoo_data_workflows.py @@ -1341,6 +1341,15 @@ def test_configured_module_list_is_used_without_auto_detection(self) -> None: self.assertEqual(list(apply_module_updates.call_args.args[0]), ["tenant_unused", "sale"]) self.assertIsNone(apply_module_updates.call_args.kwargs["local_module_paths"]) + def test_configured_module_list_also_upgrades_its_installed_local_dependencies(self) -> None: + # tenant_core depends on sale (core), tenant_helper and, through it, tenant_deep. + apply_module_updates = self._update( + installed_modules={"base", "sale", "tenant_core", "tenant_helper"}, update_modules="tenant_core" + ) + + apply_module_updates.assert_called_once() + self.assertEqual(list(apply_module_updates.call_args.args[0]), ["tenant_core", "tenant_helper"]) + def test_explicit_modules_override_configured_modules(self) -> None: apply_module_updates = self._update( installed_modules=set(), update_modules="tenant_unused", explicit_modules=["website", " "]