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.<name>.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
This commit is contained in:
gsinghpal
2026-04-19 00:36:09 -04:00
parent 512467788b
commit db90b1ad5b
8 changed files with 197 additions and 1 deletions

View File

@@ -0,0 +1 @@
from . import migration_wizard

View File

@@ -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.<name>.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."
),
},
}

View File

@@ -1,4 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Migration wizard view stub. Populated in Task 17. -->
<record id="view_fusion_migration_wizard_form" model="ir.ui.view">
<field name="name">fusion.migration.wizard.form</field>
<field name="model">fusion.migration.wizard</field>
<field name="arch" type="xml">
<form string="Migrate from Enterprise">
<sheet>
<group>
<field name="enterprise_modules_detected" readonly="1"/>
<field name="notes" readonly="1"/>
</group>
</sheet>
<footer>
<button name="action_run_migration" type="object" string="Run Migration" class="btn-primary"/>
<button special="cancel" string="Close" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<record id="action_fusion_migration_wizard" model="ir.actions.act_window">
<field name="name">Migrate from Enterprise</field>
<field name="res_model">fusion.migration.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>