diff --git a/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py b/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py
new file mode 100644
index 00000000..409d3ac8
--- /dev/null
+++ b/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py
@@ -0,0 +1,97 @@
+"""Reassign ir_model_data ownership from fusion_accounting to fusion_accounting_ai.
+
+Pre-Phase-0, all fusion code lived in module='fusion_accounting'. Post-Phase-0,
+fusion_accounting is the meta-module and the AI code lives in
+'fusion_accounting_ai'. Odoo loads the Python from the new location, but
+existing ir_model_data rows still record the old module name. This script
+rewrites them.
+
+Special case: if the data-load phase of this very upgrade already created a
+new row in module='fusion_accounting_ai' with the same `name` as an old
+orphan (because the orphan lived under the old module name when data-load
+looked for it, missed it, and re-created the record), the UPDATE below would
+violate the unique constraint on (module, name). For those conflicts we
+delete the old orphan — the newly-created row is the one that records and
+the runtime will actually use going forward.
+
+Idempotent: running it a second time does nothing because the WHERE clauses
+find no matches.
+"""
+
+import logging
+
+_logger = logging.getLogger(__name__)
+
+# Exact xml-id names (model_ prefix, one per fusion.* model) that belonged to
+# the AI module. Each corresponds to a auto-created
+# by Odoo when the model class loads.
+AI_MODEL_PREFIXES = (
+ 'model_fusion_accounting_session',
+ 'model_fusion_accounting_match_history',
+ 'model_fusion_accounting_rule',
+ 'model_fusion_accounting_tool',
+ 'model_fusion_accounting_dashboard',
+ 'model_fusion_accounting_recurring_pattern',
+ 'model_fusion_accounting_vendor_tax_profile',
+ 'model_fusion_accounting_rule_wizard',
+)
+
+# XML-id name patterns for views/data/security/wizard/etc. that belong to
+# the AI sub-module. These cover every xml-id the AI module declares in its
+# data files (cron.xml, default_rules.xml, tool_definitions.xml, views/*.xml,
+# wizards/*.xml, report/*.xml) plus the ACL entries in ir.model.access.csv.
+#
+# Patterns use SQL LIKE syntax; '%' matches anything. These are broad on
+# purpose: we want to catch every past and present xml-id declared by the AI
+# data files, including Odoo-auto-generated companions (e.g. ir.cron auto-
+# creates an ir.actions.server with xml-id '_ir_actions_server').
+AI_NAME_LIKE = (
+ 'view_fusion_%',
+ 'action_fusion_%',
+ 'menu_fusion_%',
+ 'fusion_tool_%',
+ 'fusion_rule_%',
+ 'cron_fusion_%',
+ 'seq_fusion_%',
+ 'access_fusion_%',
+ 'rule_fusion_%',
+ 'paperformat_fusion_%',
+ 'report_fusion_%',
+ 'audit_report_template',
+)
+
+
+def migrate(cr, version):
+ # Step 1: Delete orphan rows that conflict with an already-existing row in
+ # fusion_accounting_ai (data-load artifact). The new row is the survivor.
+ cr.execute("""
+ DELETE FROM ir_model_data AS old
+ WHERE old.module = 'fusion_accounting'
+ AND (old.name = ANY(%s) OR old.name LIKE ANY(%s))
+ AND EXISTS (
+ SELECT 1 FROM ir_model_data AS new
+ WHERE new.module = 'fusion_accounting_ai'
+ AND new.name = old.name
+ )
+ """, (list(AI_MODEL_PREFIXES), list(AI_NAME_LIKE)))
+ deleted_conflicts = cr.rowcount
+
+ # Step 2: Reassign the non-conflicting orphans.
+ cr.execute("""
+ UPDATE ir_model_data
+ SET module = 'fusion_accounting_ai'
+ WHERE module = 'fusion_accounting'
+ AND (
+ name = ANY(%s)
+ OR name LIKE ANY(%s)
+ )
+ """, (list(AI_MODEL_PREFIXES), list(AI_NAME_LIKE)))
+ moved = cr.rowcount
+
+ _logger.info(
+ "fusion_accounting_ai post-migration: deleted %d conflicting orphans, "
+ "reassigned %d ir_model_data rows from module='fusion_accounting' "
+ "to module='fusion_accounting_ai'",
+ deleted_conflicts,
+ moved,
+ )
diff --git a/fusion_accounting_ai/tests/__init__.py b/fusion_accounting_ai/tests/__init__.py
index e69de29b..839e3144 100644
--- a/fusion_accounting_ai/tests/__init__.py
+++ b/fusion_accounting_ai/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_post_migration
diff --git a/fusion_accounting_ai/tests/test_post_migration.py b/fusion_accounting_ai/tests/test_post_migration.py
new file mode 100644
index 00000000..e9a62709
--- /dev/null
+++ b/fusion_accounting_ai/tests/test_post_migration.py
@@ -0,0 +1,34 @@
+from odoo.tests.common import TransactionCase, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestPostMigration(TransactionCase):
+ """Verify ir_model_data ownership transferred from fusion_accounting to fusion_accounting_ai."""
+
+ def test_no_orphan_ir_model_data_in_old_module(self):
+ """No fusion-related model/view/data record should still claim module='fusion_accounting'.
+
+ After Phase 0, fusion_accounting is the meta-module and owns no records.
+ Every fusion.* model/view/data record should be owned by a sub-module
+ (fusion_accounting_ai, fusion_accounting_core, fusion_accounting_migration).
+ """
+ orphans = self.env['ir.model.data'].search([
+ ('module', '=', 'fusion_accounting'),
+ ('name', 'like', '%'),
+ ])
+ # The meta-module legitimately may own zero records. Anything found here
+ # is an orphan from the pre-Phase-0 layout.
+ self.assertFalse(
+ orphans,
+ f"Found {len(orphans)} ir_model_data rows still owned by fusion_accounting "
+ f"(should be owned by sub-modules). Examples: "
+ f"{[(r.module, r.name) for r in orphans[:5]]}"
+ )
+
+ def test_known_xml_ids_resolve_via_new_module(self):
+ """Spot-check that key xml-ids are reachable under the new module name."""
+ # Sessions model
+ ref = self.env.ref('fusion_accounting_ai.model_fusion_accounting_session', raise_if_not_found=False)
+ self.assertTrue(ref, "fusion_accounting_ai.model_fusion_accounting_session should resolve")
+ # Security group
+ # (this lives in _core after Task 12 — adapt assertion when Task 12 completes)