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)