From 4536c5bc402a34213ebaa5f5bf401196724efd64 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 31 Jul 2026 23:25:43 -0400 Subject: [PATCH] [IMP] migration: per-database uninstall list, with a stated reason MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Which modules must be uninstalled before a version bump depends on the data of one specific database, so that list does not belong to a shared versioned file. - Read private/odoo/migration//uninstall_module_list_odooXX0_to_odooYY0.txt first, then the shared versioned defaults under script/odoo/migration/, and merge them (duplicates dropped). private/ mirrors script/, the convention already used by script/todo/todo.json -> private/todo/todo_override.json. - Fix the parser. It was « f.readline().split() »: only the FIRST line was kept, so a multi-line list was silently truncated, and a comma-separated list turned into one bogus module name. On a file starting with a comment it returned the words of that comment as module names. It now reads every line and accepts commas, several names per line, blank lines and comments. - Support a « # reason » justification per module, printed before uninstalling; a module with no reason is flagged. Removing a module must stay reviewable. - Ignore private/odoo/ in git: these lists describe one database. Co-Authored-By: Claude Opus 4.8 (1M context) --- private/.gitignore | 5 ++ script/odoo/migration/README.base.md | 41 ++++++++++++++ script/odoo/migration/README.fr.md | 25 +++++++- script/odoo/migration/README.md | 23 ++++++++ script/todo/todo_upgrade.py | 85 ++++++++++++++++++++++++---- 5 files changed, 167 insertions(+), 12 deletions(-) diff --git a/private/.gitignore b/private/.gitignore index 5e440ed..12cb007 100644 --- a/private/.gitignore +++ b/private/.gitignore @@ -1 +1,6 @@ *.kdbx + +# Per-database migration lists (modules to uninstall/install before a version +# bump). They describe one specific database, never a shared default, so they +# must not be versioned. Shared defaults belong to script/odoo/migration/. +odoo/ diff --git a/script/odoo/migration/README.base.md b/script/odoo/migration/README.base.md index 7856f03..a29e31f 100644 --- a/script/odoo/migration/README.base.md +++ b/script/odoo/migration/README.base.md @@ -21,5 +21,46 @@ source ./.venv.odoo15.0_python3.8.20/bin/activate && cat ./script/odoo/migration Check [uninstall_module_list_odoo140_to_odoo150.txt](uninstall_module_list_odoo140_to_odoo150.txt) +## Module lists to uninstall + +Before a version bump, the migration uninstalls the modules listed in +`uninstall_module_list_odoo_to_odoo.txt`. Two locations are read, the +private one first, and the results are merged (duplicates dropped): + +1. `private/odoo/migration//uninstall_module_list_odooXX0_to_odooYY0.txt` + — specific to ONE database, not versioned. Which modules must be dropped + depends on the data, so this is where nearly every entry belongs. +2. `script/odoo/migration/uninstall_module_list_odooXX0_to_odooYY0.txt` + — shared defaults, versioned, valid for every database. + +Syntax: one module per line, with a justification after `#`. Commas and several +names per line are accepted; blank lines and full-line comments are ignored. +A module without a stated reason is flagged at runtime: removing a module is a +decision someone must be able to review later. + Consultez [uninstall_module_list_odoo140_to_odoo150.txt](uninstall_module_list_odoo140_to_odoo150.txt) + +## Listes de modules à désinstaller + +Avant une montée de version, la migration désinstalle les modules listés dans +`uninstall_module_list_odoo_to_odoo.txt`. Deux emplacements sont +lus, le privé d'abord, puis fusionnés (doublons éliminés) : + +1. `private/odoo/migration//uninstall_module_list_odooXX0_to_odooYY0.txt` + — propre à UNE base de données, non versionné. Les modules à supprimer + dépendent des données : c'est ici que va la quasi-totalité des entrées. +2. `script/odoo/migration/uninstall_module_list_odooXX0_to_odooYY0.txt` + — valeurs par défaut partagées, versionnées, valables pour toute base. + +Syntaxe : un module par ligne, avec une justification après `#`. Les virgules et +plusieurs noms par ligne sont acceptés ; lignes vides et commentaires pleine +ligne sont ignorés. Un module sans raison est signalé à l'exécution : supprimer +un module est une décision qui doit pouvoir être relue plus tard. + + +``` +queue_job # blocks 12->13, trigger queue_job_notify +mgmtsystem_hazard # not ported to 13.0 +web_syncer # dropped upstream +``` diff --git a/script/odoo/migration/README.fr.md b/script/odoo/migration/README.fr.md index 19ca316..35961eb 100644 --- a/script/odoo/migration/README.fr.md +++ b/script/odoo/migration/README.fr.md @@ -7,4 +7,27 @@ Exécutez ce script lors de la migration de base de données. Exemple : source ./.venv.odoo15.0_python3.8.20/bin/activate && cat ./script/odoo/migration/fix_migration_odoo140_to_odoo150.py | ./odoo15.0/odoo/odoo-bin shell -d DATABASE ``` -Consultez [uninstall_module_list_odoo140_to_odoo150.txt](uninstall_module_list_odoo140_to_odoo150.txt) \ No newline at end of file +Consultez [uninstall_module_list_odoo140_to_odoo150.txt](uninstall_module_list_odoo140_to_odoo150.txt) + +## Listes de modules à désinstaller + +Avant une montée de version, la migration désinstalle les modules listés dans +`uninstall_module_list_odoo_to_odoo.txt`. Deux emplacements sont +lus, le privé d'abord, puis fusionnés (doublons éliminés) : + +1. `private/odoo/migration//uninstall_module_list_odooXX0_to_odooYY0.txt` + — propre à UNE base de données, non versionné. Les modules à supprimer + dépendent des données : c'est ici que va la quasi-totalité des entrées. +2. `script/odoo/migration/uninstall_module_list_odooXX0_to_odooYY0.txt` + — valeurs par défaut partagées, versionnées, valables pour toute base. + +Syntaxe : un module par ligne, avec une justification après `#`. Les virgules et +plusieurs noms par ligne sont acceptés ; lignes vides et commentaires pleine +ligne sont ignorés. Un module sans raison est signalé à l'exécution : supprimer +un module est une décision qui doit pouvoir être relue plus tard. + +``` +queue_job # blocks 12->13, trigger queue_job_notify +mgmtsystem_hazard # not ported to 13.0 +web_syncer # dropped upstream +``` \ No newline at end of file diff --git a/script/odoo/migration/README.md b/script/odoo/migration/README.md index 62bf9c7..9002c6c 100644 --- a/script/odoo/migration/README.md +++ b/script/odoo/migration/README.md @@ -8,3 +8,26 @@ source ./.venv.odoo15.0_python3.8.20/bin/activate && cat ./script/odoo/migration ``` Check [uninstall_module_list_odoo140_to_odoo150.txt](uninstall_module_list_odoo140_to_odoo150.txt) + +## Module lists to uninstall + +Before a version bump, the migration uninstalls the modules listed in +`uninstall_module_list_odoo_to_odoo.txt`. Two locations are read, the +private one first, and the results are merged (duplicates dropped): + +1. `private/odoo/migration//uninstall_module_list_odooXX0_to_odooYY0.txt` + — specific to ONE database, not versioned. Which modules must be dropped + depends on the data, so this is where nearly every entry belongs. +2. `script/odoo/migration/uninstall_module_list_odooXX0_to_odooYY0.txt` + — shared defaults, versioned, valid for every database. + +Syntax: one module per line, with a justification after `#`. Commas and several +names per line are accepted; blank lines and full-line comments are ignored. +A module without a stated reason is flagged at runtime: removing a module is a +decision someone must be able to review later. + +``` +queue_job # blocks 12->13, trigger queue_job_notify +mgmtsystem_hazard # not ported to 13.0 +web_syncer # dropped upstream +``` \ No newline at end of file diff --git a/script/todo/todo_upgrade.py b/script/todo/todo_upgrade.py index b34ea9a..fd50ef6 100755 --- a/script/todo/todo_upgrade.py +++ b/script/todo/todo_upgrade.py @@ -44,6 +44,12 @@ FILENAME_ODOO_VERSION = ".odoo-version" LOCAL_MANIFEST = os.path.join( ".repo", "local_manifests", "erplibre_manifest.xml" ) +# Module lists for a version bump. The shared, versioned defaults live under +# script/; the per-database lists live under private/ (mirroring script/, like +# script/todo/todo.json -> private/todo/todo_override.json). Which modules must +# be dropped depends on the database, so that choice is never versioned. +PATH_MIGRATION_GLOBAL = os.path.join("script", "odoo", "migration") +PATH_MIGRATION_PRIVATE = os.path.join("private", "odoo", "migration") class TodoUpgrade: @@ -828,18 +834,20 @@ class TodoUpgrade: ) if not is_state_4_reach_open_upgrade: - lst_module_to_uninstall = [] - uninstall_module_list_file = os.path.join( - "script", - "odoo", - "migration", - f"uninstall_module_list_odoo{start_version * 10}_to_odoo{(start_version + 1) * 10}.txt", + lst_module_to_uninstall, lst_uninstall_reason = ( + self.read_uninstall_module_list(start_version, database_name) ) - if os.path.exists(uninstall_module_list_file): - with open(uninstall_module_list_file, "r") as f: - lst_module_to_uninstall = [ - a.strip() for a in f.readline().split() - ] + if lst_uninstall_reason: + # Show WHY each module goes away: which modules must be dropped + # depends on the database, and a removal without a stated reason + # is a decision nobody can review later. + print("✨ Modules to uninstall before migration :") + for name, reason, origin in lst_uninstall_reason: + print( + f" - {name}" + + (f" — {reason}" if reason else " — ⚠️ no reason given") + + f" [{origin}]" + ) if config_state_1_uninstall_module: lst_module_to_uninstall = ( @@ -1802,6 +1810,61 @@ class TodoUpgrade: } return dct_module + @staticmethod + def parse_module_list_file(file_path): + """Read a module list file, return [(module, reason), ...]. + + Accepted syntax, one module per line with an optional justification: + + queue_job # blocks 12->13, trigger queue_job_notify + mgmtsystem_hazard # not ported to 13.0 + + Commas and several names per line are also accepted, so a list copied + from a command line works as-is. Blank lines and full-line comments are + ignored. + + The previous parser was « f.readline().split() »: it kept only the FIRST + line, so a multi-line list was silently truncated, and a comma-separated + list collapsed into one bogus module name. + """ + lst_module = [] + with open(file_path, "r", encoding="utf-8") as f: + for line in f: + content, _, reason = line.partition("#") + for module_name in content.replace(",", " ").split(): + lst_module.append((module_name, reason.strip())) + return lst_module + + def read_uninstall_module_list(self, start_version, database_name): + """Modules to uninstall before migrating start_version -> next. + + Reads the per-database private list first, then the shared versioned + defaults; duplicates are dropped, keeping the first occurrence. + + Returns (lst_module, lst_detail) where lst_detail carries + (module, reason, origin_file) so the caller can justify each removal. + """ + file_name = ( + f"uninstall_module_list_odoo{start_version * 10}" + f"_to_odoo{(start_version + 1) * 10}.txt" + ) + lst_path = [ + os.path.join(PATH_MIGRATION_PRIVATE, database_name, file_name), + os.path.join(PATH_MIGRATION_GLOBAL, file_name), + ] + + lst_module = [] + lst_detail = [] + for file_path in lst_path: + if not os.path.exists(file_path): + continue + for module_name, reason in self.parse_module_list_file(file_path): + if module_name in lst_module: + continue + lst_module.append(module_name) + lst_detail.append((module_name, reason, file_path)) + return lst_module, lst_detail + def uninstall_from_database( self, lst_module_to_uninstall, database_name, actual_version ):