From db90b1ad5bd220f872fee3a578efe0bbf802c099 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 00:36:09 -0400 Subject: [PATCH] feat(fusion_accounting_migration): add Enterprise uninstall safety guard + wizard skeleton Phase 0 Task 17. Installs a safety guard on ir.module.module that blocks uninstall of Odoo Enterprise accounting modules (account_accountant, account_reports, accountant, account_followup, account_asset, account_budget, account_loans) until the per-module migration flag fusion_accounting.migration..completed is set to True. Guard covers both button_immediate_uninstall (UI) and module_uninstall (CLI/API) paths, raising UserError with a pointer to the migration wizard and an escape hatch config parameter. Also ships a TransientModel fusion.migration.wizard as a shell: it detects installed Enterprise modules via GUARDED_MODULES and exposes action_run_migration for sub-modules to extend in later phases. No per-feature migrations are registered yet -- Phase 1+ sub-modules will hook in their own steps. Tests: TestSafetyGuard x2 pass (blocked-when-pending verified with account_accountant installed; not-blocked-when-completed verified by setting the flag). Made-with: Cursor --- .../models/__init__.py | 1 + .../models/ir_module_module.py | 72 +++++++++++++++++++ .../security/ir.model.access.csv | 1 + fusion_accounting_migration/tests/__init__.py | 1 + .../tests/test_safety_guard.py | 31 ++++++++ .../wizards/__init__.py | 1 + .../wizards/migration_wizard.py | 65 +++++++++++++++++ .../wizards/migration_wizard_views.xml | 26 ++++++- 8 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_migration/models/ir_module_module.py create mode 100644 fusion_accounting_migration/tests/test_safety_guard.py create mode 100644 fusion_accounting_migration/wizards/migration_wizard.py diff --git a/fusion_accounting_migration/models/__init__.py b/fusion_accounting_migration/models/__init__.py index e69de29b..8e2d2d1b 100644 --- a/fusion_accounting_migration/models/__init__.py +++ b/fusion_accounting_migration/models/__init__.py @@ -0,0 +1 @@ +from . import ir_module_module diff --git a/fusion_accounting_migration/models/ir_module_module.py b/fusion_accounting_migration/models/ir_module_module.py new file mode 100644 index 00000000..b5fccddf --- /dev/null +++ b/fusion_accounting_migration/models/ir_module_module.py @@ -0,0 +1,72 @@ +"""Safety guard: blocks Odoo Enterprise accounting uninstall until migration runs. + +For each Enterprise accounting module the user attempts to uninstall, the +guard checks an ir.config_parameter flag named: + + fusion_accounting.migration..completed + +If the flag is False/unset and the module is currently installed, the guard +raises UserError pointing the user to Settings -> Fusion Accounting -> +Migrate from Enterprise. + +The migration wizard sets the flag to True after a successful migration run +for that module. +""" + +from odoo import _, api, models +from odoo.exceptions import UserError + + +GUARDED_MODULES = ( + 'account_accountant', + 'account_reports', + 'accountant', + 'account_followup', + 'account_asset', + 'account_budget', + 'account_loans', +) + + +class IrModuleModule(models.Model): + _inherit = "ir.module.module" + + @api.model + def _fusion_check_uninstall_guard(self, module_names): + """Verify it's safe to uninstall the given modules. + + Returns True if all checks pass; raises UserError otherwise. + """ + Param = self.env['ir.config_parameter'].sudo() + for name in module_names: + if name not in GUARDED_MODULES: + continue + installed = self.sudo().search_count([ + ('name', '=', name), ('state', '=', 'installed'), + ]) + if not installed: + continue + flag_key = f'fusion_accounting.migration.{name}.completed' + if Param.get_param(flag_key, default='False').lower() != 'true': + raise UserError(_( + "Cannot uninstall %s: the Fusion Accounting migration " + "for this module has not run yet. Please open\n" + " Settings -> Fusion Accounting -> Migrate from Enterprise\n" + "and run the migration before uninstalling. Once the " + "migration has completed, the safety guard will allow " + "uninstall.\n\n" + "If you genuinely want to uninstall WITHOUT migrating " + "(data will be lost), set the parameter %s to True manually.", + name, flag_key, + )) + return True + + def button_immediate_uninstall(self): + """Override to invoke the safety guard before allowing uninstall.""" + self._fusion_check_uninstall_guard(self.mapped('name')) + return super().button_immediate_uninstall() + + def module_uninstall(self): + """Override the lower-level uninstall path too (CLI / API uninstall).""" + self._fusion_check_uninstall_guard(self.mapped('name')) + return super().module_uninstall() diff --git a/fusion_accounting_migration/security/ir.model.access.csv b/fusion_accounting_migration/security/ir.model.access.csv index 97dd8b91..420f6bb6 100644 --- a/fusion_accounting_migration/security/ir.model.access.csv +++ b/fusion_accounting_migration/security/ir.model.access.csv @@ -1 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fusion_migration_wizard_admin,fusion.migration.wizard admin,model_fusion_migration_wizard,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 diff --git a/fusion_accounting_migration/tests/__init__.py b/fusion_accounting_migration/tests/__init__.py index e69de29b..184d2b90 100644 --- a/fusion_accounting_migration/tests/__init__.py +++ b/fusion_accounting_migration/tests/__init__.py @@ -0,0 +1 @@ +from . import test_safety_guard diff --git a/fusion_accounting_migration/tests/test_safety_guard.py b/fusion_accounting_migration/tests/test_safety_guard.py new file mode 100644 index 00000000..67532c33 --- /dev/null +++ b/fusion_accounting_migration/tests/test_safety_guard.py @@ -0,0 +1,31 @@ +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class TestSafetyGuard(TransactionCase): + """Verify the safety guard blocks Enterprise uninstall when migration hasn't run.""" + + def test_uninstall_not_blocked_when_migration_completed(self): + """If the per-module migration flag is set, uninstall is allowed.""" + self.env['ir.config_parameter'].sudo().set_param( + 'fusion_accounting.migration.account_accountant.completed', 'True' + ) + guard = self.env['ir.module.module']._fusion_check_uninstall_guard(['account_accountant']) + self.assertTrue(guard, "Guard should pass when migration flag is set") + + def test_uninstall_blocked_when_migration_pending(self): + """If account_accountant is installed and migration not run, raise.""" + self.env['ir.config_parameter'].sudo().set_param( + 'fusion_accounting.migration.account_accountant.completed', 'False' + ) + Module = self.env['ir.module.module'].sudo() + installed = Module.search_count([ + ('name', '=', 'account_accountant'), + ('state', '=', 'installed'), + ]) + if not installed: + self.skipTest("account_accountant not installed in this DB") + with self.assertRaises(UserError) as ctx: + Module._fusion_check_uninstall_guard(['account_accountant']) + self.assertIn('migration', str(ctx.exception).lower()) diff --git a/fusion_accounting_migration/wizards/__init__.py b/fusion_accounting_migration/wizards/__init__.py index e69de29b..0e67a0b3 100644 --- a/fusion_accounting_migration/wizards/__init__.py +++ b/fusion_accounting_migration/wizards/__init__.py @@ -0,0 +1 @@ +from . import migration_wizard diff --git a/fusion_accounting_migration/wizards/migration_wizard.py b/fusion_accounting_migration/wizards/migration_wizard.py new file mode 100644 index 00000000..cdde55da --- /dev/null +++ b/fusion_accounting_migration/wizards/migration_wizard.py @@ -0,0 +1,65 @@ +"""Migration wizard skeleton. + +Per-feature migration logic (account.asset -> fusion.asset, etc.) is added +by each fusion sub-module that replaces an Enterprise feature, by extending +this wizard via _inherit. + +Phase 0 ships the wizard with no migrations registered. Phase 1 will add +the bank-rec verification check. Phase 6 will add asset migration, etc. +""" + +from odoo import _, api, fields, models + + +class FusionMigrationWizard(models.TransientModel): + _name = "fusion.migration.wizard" + _description = "Migrate from Odoo Enterprise to Fusion Accounting" + + enterprise_modules_detected = fields.Char( + compute='_compute_detected', + string="Enterprise Modules Detected", + ) + notes = fields.Text(default=lambda self: self._default_notes()) + + def _default_notes(self): + return _( + "This wizard migrates data from Odoo Enterprise accounting modules " + "to Fusion Accounting tables. Run before uninstalling Enterprise. " + "After a successful run, each migrated module is marked complete " + "and the Enterprise uninstall safety guard will allow uninstall.\n\n" + "Phase 0 of the roadmap ships this wizard as a shell. As Phase 1, " + "Phase 5, Phase 6, etc. ship, each adds its own migration step here." + ) + + @api.depends_context('uid') + def _compute_detected(self): + Mod = self.env['ir.module.module'].sudo() + from ..models.ir_module_module import GUARDED_MODULES + installed = Mod.search([ + ('name', 'in', list(GUARDED_MODULES)), + ('state', '=', 'installed'), + ]) + for w in self: + w.enterprise_modules_detected = ', '.join(installed.mapped('name')) or _("None") + + def action_run_migration(self): + """Stub: Phase 0 has no migrations to run. + + Sub-modules extend this method to perform their per-module migration, + then set the corresponding fusion_accounting.migration..completed + config param to True. + """ + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'info', + 'title': _("Nothing to migrate (yet)"), + 'message': _( + "Phase 0 ships the migration framework but no per-feature " + "migrations are registered yet. Each fusion sub-module that " + "replaces an Enterprise feature (Phase 1+) will register its " + "own migration step here." + ), + }, + } diff --git a/fusion_accounting_migration/wizards/migration_wizard_views.xml b/fusion_accounting_migration/wizards/migration_wizard_views.xml index ccd0b26f..dd27259b 100644 --- a/fusion_accounting_migration/wizards/migration_wizard_views.xml +++ b/fusion_accounting_migration/wizards/migration_wizard_views.xml @@ -1,4 +1,28 @@ - + + fusion.migration.wizard.form + fusion.migration.wizard + +
+ + + + + + +
+
+
+
+
+ + + Migrate from Enterprise + fusion.migration.wizard + form + new +