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:
@@ -0,0 +1 @@
|
|||||||
|
from . import ir_module_module
|
||||||
|
|||||||
72
fusion_accounting_migration/models/ir_module_module.py
Normal file
72
fusion_accounting_migration/models/ir_module_module.py
Normal file
@@ -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.<module_name>.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()
|
||||||
@@ -1 +1,2 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
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
|
||||||
|
|||||||
|
@@ -0,0 +1 @@
|
|||||||
|
from . import test_safety_guard
|
||||||
|
|||||||
31
fusion_accounting_migration/tests/test_safety_guard.py
Normal file
31
fusion_accounting_migration/tests/test_safety_guard.py
Normal file
@@ -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())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import migration_wizard
|
||||||
|
|||||||
65
fusion_accounting_migration/wizards/migration_wizard.py
Normal file
65
fusion_accounting_migration/wizards/migration_wizard.py
Normal 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."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,4 +1,28 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<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>
|
</odoo>
|
||||||
|
|||||||
Reference in New Issue
Block a user