feat(fusion_accounting_ai): add post-migration to reassign ir_model_data ownership
Phase 0 Task 7. Pre-Phase-0 all AI code lived in module='fusion_accounting'; the code now lives in 'fusion_accounting_ai' but existing ir_model_data rows still record the old module name. This post-migration rewrites them. Handles duplicate-key conflicts by deleting old orphan rows when data-load has already created a new row under the same name in the new module. Idempotent: second run reassigns 0 rows. Made-with: Cursor
This commit is contained in:
97
fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py
Normal file
97
fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py
Normal file
@@ -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 <record id="model_..."/> 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 '<cron_name>_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,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_post_migration
|
||||
|
||||
34
fusion_accounting_ai/tests/test_post_migration.py
Normal file
34
fusion_accounting_ai/tests/test_post_migration.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user