Files
Odoo-Modules/fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md
gsinghpal d7cc334c98
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
docs(fusion_accounting): record Phase 0 smoke test results
Made-with: Cursor
2026-04-19 01:29:22 -04:00

155 KiB
Raw Blame History

Phase 0 — Foundation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Transform the current single fusion_accounting module into a multi-sub-module structure (fusion_accounting_core, fusion_accounting_ai, fusion_accounting_migration) plus the existing fusion_accounting as a meta-module, strip all hard Enterprise dependencies, introduce the data-adapter pattern for AI tools, lay shared-field-ownership groundwork, and validate everything still works on pure Community + on Community-with-Enterprise.

Architecture: Move every piece of the current AI copilot into a new fusion_accounting_ai sub-module that depends only on fusion_accounting_core (which depends only on Community account). Adapters in fusion_accounting_ai/services/data_adapters/ route data lookups to fusion native (when installed), Enterprise (when installed), or pure Community (last resort) — keeping AI tools working in all three modes. fusion_accounting_core owns shared schema (security groups, shared-field declarations on account.move, helper for runtime Enterprise detection). fusion_accounting_migration adds a safety guard that blocks Enterprise uninstall until a migration wizard runs (the wizard itself is built when a Phase-replacement sub-module needs it).

Tech Stack: Odoo 19, Python 3.11+, PostgreSQL, OWL JS framework, anthropic + openai Python clients (already in current module).

Spec Reference: docs/superpowers/specs/2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md, Section 4.2.


File Structure (after Phase 0 completes)

fusion_accounting/                                    # meta-module (was the AI module)
├── __init__.py                                       # empty
├── __manifest__.py                                   # depends on the 3 sub-modules; loads no data
├── CLAUDE.md                                         # meta-module overview (rewrite)
├── README.md                                         # operator-facing
├── docs/                                             # already created
│   └── superpowers/
│       ├── specs/
│       └── plans/
└── tools/
    ├── check_odoo_diff.sh                            # cross-version diff helper
    └── README.md

fusion_accounting_core/                               # NEW sub-module
├── __init__.py
├── __manifest__.py
├── CLAUDE.md
├── UPGRADE_NOTES.md
├── README.md
├── data/                                             # empty for now
├── models/
│   ├── __init__.py
│   ├── account_move.py                               # shared-field declarations
│   ├── account_reconcile_model.py                    # shared-field (created_automatically)
│   ├── account_bank_statement_line.py                # shared-field stubs (none critical right now)
│   └── ir_module_module.py                           # _fusion_is_enterprise_accounting_installed helper
├── security/
│   ├── ir.model.access.csv                           # empty
│   └── fusion_accounting_security.xml                # privilege + 3 groups (moved from current)
├── tests/
│   ├── __init__.py
│   ├── test_shared_field_ownership.py
│   └── test_enterprise_detection.py
└── views/                                            # empty for now

fusion_accounting_ai/                                 # NEW sub-module (current AI code lives here)
├── __init__.py
├── __manifest__.py
├── CLAUDE.md                                         # AI-specific (most of current CLAUDE.md content)
├── UPGRADE_NOTES.md
├── README.md
├── controllers/                                      # MOVED from fusion_accounting/
│   ├── __init__.py
│   └── chat_controller.py
├── data/                                             # MOVED
│   ├── cron.xml
│   ├── default_rules.xml
│   └── tool_definitions.xml
├── migrations/
│   └── 19.0.1.0.0/
│       └── post-migration.py                         # reassigns ir_model_data ownership
├── models/                                           # MOVED
│   ├── __init__.py
│   ├── account_move_hook.py
│   ├── accounting_config.py
│   ├── accounting_dashboard.py
│   ├── accounting_match_history.py
│   ├── accounting_rule.py
│   ├── accounting_session.py
│   ├── accounting_tool.py
│   ├── recurring_pattern.py
│   └── vendor_tax_profile.py
├── report/                                           # MOVED
│   └── audit_report_template.xml
├── security/                                         # ACLs only (groups move to _core)
│   └── ir.model.access.csv
├── services/                                         # MOVED + extended
│   ├── __init__.py
│   ├── adapters/                                     # AI provider adapters (Claude/OpenAI) — unchanged
│   │   ├── __init__.py
│   │   ├── claude.py
│   │   └── openai_adapter.py
│   ├── agent.py
│   ├── data_adapters/                                # NEW: data lookup routing
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── bank_rec.py
│   │   ├── reports.py
│   │   ├── followup.py
│   │   ├── assets.py
│   │   └── _registry.py
│   ├── prompts/
│   │   ├── __init__.py
│   │   ├── domain_prompts.py
│   │   └── system_prompt.py
│   ├── scoring.py
│   └── tools/                                        # MOVED, refactored to use adapters
│       ├── __init__.py
│       ├── accounts_payable.py
│       ├── accounts_receivable.py
│       ├── adp.py
│       ├── audit.py
│       ├── bank_reconciliation.py
│       ├── hst_management.py
│       ├── inventory.py
│       ├── journal_review.py
│       ├── month_end.py
│       ├── payroll.py
│       └── reporting.py
├── static/                                           # MOVED
│   ├── description/
│   │   └── icon.png
│   └── src/
│       ├── components/
│       │   ├── chat/
│       │   └── dashboard/
│       └── scss/
├── tests/                                            # MOVED + extended
│   ├── __init__.py
│   ├── test_api_live.py
│   ├── test_claude_api.py
│   ├── test_data_adapters.py                         # NEW
│   └── test_post_migration.py                        # NEW (verifies the move)
├── views/                                            # MOVED
│   ├── config_views.xml
│   ├── dashboard_views.xml
│   ├── match_history_views.xml
│   ├── menus.xml
│   ├── recurring_pattern_views.xml
│   ├── rule_views.xml
│   ├── session_views.xml
│   └── vendor_tax_profile_views.xml
└── wizards/                                          # MOVED
    ├── __init__.py
    ├── rule_wizard.py
    └── rule_wizard.xml

fusion_accounting_migration/                          # NEW sub-module (transitional)
├── __init__.py
├── __manifest__.py
├── CLAUDE.md
├── UPGRADE_NOTES.md
├── README.md
├── models/
│   ├── __init__.py
│   └── ir_module_module.py                           # safety guard: blocks Enterprise uninstall
├── security/
│   └── ir.model.access.csv
├── tests/
│   ├── __init__.py
│   └── test_safety_guard.py
└── wizards/
    ├── __init__.py
    ├── migration_wizard.py                           # skeleton; no migrations registered yet
    └── migration_wizard_views.xml

.github/workflows/                                    # if GitHub
fusion_accounting/.gitea/workflows/                   # if Gitea
└── ci.yml                                            # runs pytest per sub-module

Conventions Used Throughout This Plan

  • Workspace root: /Users/gurpreet/Github/Odoo-Modules (the git repository).
  • All file paths in tasks are absolute unless explicitly noted as relative to a sub-module folder.
  • All git commands are run from the workspace root unless noted.
  • All docker exec odoo-dev-app odoo ... style commands use the user's existing dev environment (odoo-westin with westin-v19 DB per current CLAUDE.md). Substitute equivalents for local-Docker (odoo-dev-app + fusion-dev DB) if testing locally.
  • Commit style: follow the conventional pattern observed in recent repo commits: <type>(<scope>): <description> (e.g. refactor(fusion_accounting): split into core/ai/migration sub-modules).
  • Manifest version bumps: every meaningful change to a sub-module bumps the patch version (e.g. 19.0.1.0.019.0.1.0.1).

Task 1: Safety Net — Tag Current State and Branch

Files:

  • Modify: git refs only

  • Step 1: Verify clean working tree before tagging

Run: cd /Users/gurpreet/Github/Odoo-Modules && git status --short

Expected: only unrelated fusion_plating and .DS_Store changes (per current state). The roadmap doc (fusion_accounting/docs/superpowers/specs/...) was already committed in 956678d. If anything else under fusion_accounting/ is dirty, stop and ask user.

  • Step 2: Tag the pre-Phase-0 state

Run:

cd /Users/gurpreet/Github/Odoo-Modules
git tag -a fusion_accounting/pre-phase-0 -m "Snapshot before Phase 0 sub-module split"
git tag --list "fusion_accounting/*"

Expected output: fusion_accounting/pre-phase-0

  • Step 3: Create Phase 0 working branch

Run:

git checkout -b fusion_accounting/phase-0-foundation
git branch --show-current

Expected output: fusion_accounting/phase-0-foundation

  • Step 4: Commit nothing — branch creation only

No commit yet. Branch exists for isolation.


Task 2: Create fusion_accounting_core Skeleton

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__manifest__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/security/ir.model.access.csv

  • Step 1: Create the directory structure

Run:

cd /Users/gurpreet/Github/Odoo-Modules
mkdir -p fusion_accounting_core/{models,security,views,data,tests}
  • Step 2: Write top-level __init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__init__.py

from . import models
  • Step 3: Write models/__init__.py (empty for now — populated in later tasks)

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/__init__.py

# Models populated in Tasks 8-12 (shared-field-ownership, helpers)
  • Step 4: Write tests/__init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/tests/__init__.py

# Tests populated in Tasks 8-12
  • Step 5: Write __manifest__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__manifest__.py

{
    'name': 'Fusion Accounting Core',
    'version': '19.0.1.0.0',
    'category': 'Accounting/Accounting',
    'sequence': 24,
    'summary': 'Shared base for the Fusion Accounting sub-module suite (security, shared schema, runtime helpers).',
    'description': """
Fusion Accounting Core
======================
Foundation for the Fusion Accounting sub-modules. Owns:
- Three security groups (User, Manager, Admin) shared across all fusion sub-modules
- Shared-field declarations on Community account models so deferred-revenue,
  signing-user, and similar Enterprise-extension fields survive Enterprise uninstall
- Runtime helper for detecting Odoo Enterprise accounting modules

This module never works alone. Install fusion_accounting (the meta-module)
or one of fusion_accounting_ai, fusion_accounting_bank_rec, etc.

Built by Nexa Systems Inc.
    """,
    'author': 'Nexa Systems Inc.',
    'website': 'https://nexasystems.ca',
    'support': 'support@nexasystems.ca',
    'maintainer': 'Nexa Systems Inc.',
    'depends': ['account', 'mail'],
    'data': [
        'security/ir.model.access.csv',
    ],
    'installable': True,
    'application': False,
    'license': 'OPL-1',
}
  • Step 6: Write empty security/ir.model.access.csv

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/security/ir.model.access.csv

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink

(One header row only. ACLs added in later tasks as models are added.)

  • Step 7: Verify the module loads in Odoo (smoke test)

Run:

ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting_core"
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core odoo-westin:/tmp/
ssh odoo-westin "docker cp /tmp/fusion_accounting_core odoo-dev-app:/mnt/extra-addons/fusion_accounting_core && rm -rf /tmp/fusion_accounting_core"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -i fusion_accounting_core --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | tail -20"

Expected: install completes without error; last lines show Modules loaded. (or equivalent success line).

  • Step 8: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_core/
git commit -m "feat(fusion_accounting_core): add empty sub-module skeleton"

Task 3: Create fusion_accounting_ai Skeleton (Empty)

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/__manifest__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py

  • Step 1: Create the directory structure

Run:

cd /Users/gurpreet/Github/Odoo-Modules
mkdir -p fusion_accounting_ai/{controllers,data,models,security,services,services/adapters,services/data_adapters,services/prompts,services/tools,static,static/description,static/src,static/src/components,static/src/components/chat,static/src/components/dashboard,static/src/scss,tests,views,wizards,report,migrations,migrations/19.0.1.0.0}
  • Step 2: Write __init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/__init__.py

from . import models
from . import controllers
from . import services
from . import wizards
  • Step 3: Write __manifest__.py (skeleton — populated as files move in)

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/__manifest__.py

{
    'name': 'Fusion Accounting AI',
    'version': '19.0.1.0.0',
    'category': 'Accounting/Accounting',
    'sequence': 26,
    'summary': 'AI Co-Pilot for Odoo accounting (Claude/GPT) with conversational interface, dashboard, rules.',
    'description': """
Fusion Accounting AI
====================
Conversational AI co-pilot for Odoo Accounting. Embeds Claude/GPT with
native tool-calling for bank reconciliation, HST management, AR/AP analysis,
journal review, month-end close, payroll, ADP reconciliation, financial
reporting, and auditing.

Works on three install profiles via the data-adapter pattern:
1. Pure Odoo Community + fusion_accounting_ai
2. Odoo Community + fusion_accounting_ai + fusion native sub-modules (bank_rec, reports, ...)
3. Odoo Enterprise + fusion_accounting_ai (legacy mode)

Built by Nexa Systems Inc.
    """,
    'icon': '/fusion_accounting_ai/static/description/icon.png',
    'author': 'Nexa Systems Inc.',
    'website': 'https://nexasystems.ca',
    'support': 'support@nexasystems.ca',
    'maintainer': 'Nexa Systems Inc.',
    'depends': ['fusion_accounting_core'],
    'external_dependencies': {
        'python': ['anthropic', 'openai'],
    },
    'data': [
        # Populated as files move in (Tasks 5, 7, 11)
        'security/ir.model.access.csv',
    ],
    'installable': True,
    'application': True,
    'license': 'OPL-1',
    'assets': {
        # Populated as static moves in (Task 5)
    },
}
  • Step 4: Write empty security/ir.model.access.csv

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/security/ir.model.access.csv

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
  • Step 5: Write empty __init__.py files for every Python package

For each of these folders, create an __init__.py (empty or with just a comment):

cd /Users/gurpreet/Github/Odoo-Modules
for dir in fusion_accounting_ai/controllers fusion_accounting_ai/models fusion_accounting_ai/services fusion_accounting_ai/services/adapters fusion_accounting_ai/services/data_adapters fusion_accounting_ai/services/prompts fusion_accounting_ai/services/tools fusion_accounting_ai/wizards fusion_accounting_ai/tests; do
    touch "$dir/__init__.py"
done
  • Step 6: Verify the module loads (skeleton install)

Run the same deploy snippet from Task 2 Step 7 but for fusion_accounting_ai. Expected: installs cleanly with no models, views, or data — just an empty skeleton.

  • Step 7: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/
git commit -m "feat(fusion_accounting_ai): add empty sub-module skeleton"

Task 4: Create fusion_accounting_migration Skeleton

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/__manifest__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/security/ir.model.access.csv

  • Step 1: Create directory structure

cd /Users/gurpreet/Github/Odoo-Modules
mkdir -p fusion_accounting_migration/{models,security,wizards,tests}
  • Step 2: Write __init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/__init__.py

from . import models
from . import wizards
  • Step 3: Write __manifest__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/__manifest__.py

{
    'name': 'Fusion Accounting Migration',
    'version': '19.0.1.0.0',
    'category': 'Accounting/Accounting',
    'sequence': 27,
    'summary': 'Transitional module: migrates Odoo Enterprise accounting data to Fusion Accounting tables before Enterprise uninstall.',
    'description': """
Fusion Accounting Migration
===========================
Transitional helper that lives only during Enterprise-to-Fusion switchover.

Provides:
- A safety guard that blocks uninstall of Odoo Enterprise accounting modules
  (account_accountant, account_reports, account_followup, account_asset,
  account_budget, account_loans) until the Fusion migration wizard has run
- A guided migration wizard accessible at Settings -> Fusion Accounting ->
  Migrate from Enterprise (the wizard's per-feature migrations are added
  by each Fusion sub-module that replaces an Enterprise feature)

Once the switchover is complete, this module can safely be uninstalled.

Built by Nexa Systems Inc.
    """,
    'author': 'Nexa Systems Inc.',
    'website': 'https://nexasystems.ca',
    'support': 'support@nexasystems.ca',
    'maintainer': 'Nexa Systems Inc.',
    'depends': ['fusion_accounting_core'],
    'data': [
        'security/ir.model.access.csv',
        'wizards/migration_wizard_views.xml',
    ],
    'installable': True,
    'application': False,
    'license': 'OPL-1',
}
  • Step 4: Write empty package __init__.py files
cd /Users/gurpreet/Github/Odoo-Modules
for dir in fusion_accounting_migration/models fusion_accounting_migration/wizards fusion_accounting_migration/tests; do
    touch "$dir/__init__.py"
done
  • Step 5: Write empty ACL CSV

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/security/ir.model.access.csv

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
  • Step 6: Write placeholder wizards/migration_wizard_views.xml (so manifest loads cleanly)

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/migration_wizard_views.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- Migration wizard view stub. Populated in Task 16. -->
</odoo>
  • Step 7: Verify install

Same deploy snippet pattern as Task 2 Step 7, for fusion_accounting_migration. Expected: installs cleanly.

  • Step 8: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_migration/
git commit -m "feat(fusion_accounting_migration): add empty sub-module skeleton"

Task 5: Move AI Code from fusion_accounting/ to fusion_accounting_ai/

This is a multi-step move. We use git mv for every file so history is preserved. After the move, fusion_accounting/ retains only __manifest__.py, __init__.py, CLAUDE.md, docs/ (which we keep), and the report/audit_report_template.xml will move to _ai.

Files moved (every file from current fusion_accounting/ except docs and config to be rewritten):

Source (in /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/):

  • controllers/__init__.py, controllers/chat_controller.py
  • data/cron.xml, data/default_rules.xml, data/tool_definitions.xml
  • models/__init__.py, models/account_move_hook.py, models/accounting_config.py, models/accounting_dashboard.py, models/accounting_match_history.py, models/accounting_rule.py, models/accounting_session.py, models/accounting_tool.py, models/recurring_pattern.py, models/vendor_tax_profile.py
  • report/audit_report_template.xml
  • security/ir.model.access.csv, security/security.xml
  • services/__init__.py, services/adapters/__init__.py, services/adapters/claude.py, services/adapters/openai_adapter.py, services/agent.py, services/prompts/__init__.py, services/prompts/domain_prompts.py, services/prompts/system_prompt.py, services/scoring.py, services/tools/__init__.py, services/tools/accounts_payable.py, services/tools/accounts_receivable.py, services/tools/adp.py, services/tools/audit.py, services/tools/bank_reconciliation.py, services/tools/hst_management.py, services/tools/inventory.py, services/tools/journal_review.py, services/tools/month_end.py, services/tools/payroll.py, services/tools/reporting.py
  • static/description/icon.png
  • static/src/components/chat/approval_card.js, static/src/components/chat/approval_card.xml, static/src/components/chat/chat_panel.js, static/src/components/chat/chat_panel.xml, static/src/components/chat/interactive_table.js, static/src/components/chat/interactive_table.xml
  • static/src/components/dashboard/fusion_dashboard.js, static/src/components/dashboard/fusion_dashboard.xml, static/src/components/dashboard/health_card.js, static/src/components/dashboard/health_card.xml
  • static/src/scss/chat.scss, static/src/scss/dashboard.scss
  • tests/test_api_live.py, tests/test_claude_api.py
  • views/config_views.xml, views/dashboard_views.xml, views/match_history_views.xml, views/menus.xml, views/recurring_pattern_views.xml, views/rule_views.xml, views/session_views.xml, views/vendor_tax_profile_views.xml
  • wizards/__init__.py, wizards/rule_wizard.py, wizards/rule_wizard.xml

Destination: identical relative paths under fusion_accounting_ai/.

  • Step 1: Move controllers
cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/controllers/chat_controller.py fusion_accounting_ai/controllers/chat_controller.py
# fusion_accounting/controllers/__init__.py is replaced (skeleton already in place); remove the old:
git rm fusion_accounting/controllers/__init__.py

Then re-write fusion_accounting_ai/controllers/__init__.py:

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/controllers/__init__.py

from . import chat_controller
  • Step 2: Move data files
cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/data/cron.xml fusion_accounting_ai/data/cron.xml
git mv fusion_accounting/data/default_rules.xml fusion_accounting_ai/data/default_rules.xml
git mv fusion_accounting/data/tool_definitions.xml fusion_accounting_ai/data/tool_definitions.xml
rmdir fusion_accounting/data 2>/dev/null || true
  • Step 3: Move models
cd /Users/gurpreet/Github/Odoo-Modules
git rm fusion_accounting_ai/models/__init__.py  # remove the empty stub from Task 3
git mv fusion_accounting/models/__init__.py fusion_accounting_ai/models/__init__.py
git mv fusion_accounting/models/account_move_hook.py fusion_accounting_ai/models/account_move_hook.py
git mv fusion_accounting/models/accounting_config.py fusion_accounting_ai/models/accounting_config.py
git mv fusion_accounting/models/accounting_dashboard.py fusion_accounting_ai/models/accounting_dashboard.py
git mv fusion_accounting/models/accounting_match_history.py fusion_accounting_ai/models/accounting_match_history.py
git mv fusion_accounting/models/accounting_rule.py fusion_accounting_ai/models/accounting_rule.py
git mv fusion_accounting/models/accounting_session.py fusion_accounting_ai/models/accounting_session.py
git mv fusion_accounting/models/accounting_tool.py fusion_accounting_ai/models/accounting_tool.py
git mv fusion_accounting/models/recurring_pattern.py fusion_accounting_ai/models/recurring_pattern.py
git mv fusion_accounting/models/vendor_tax_profile.py fusion_accounting_ai/models/vendor_tax_profile.py
rmdir fusion_accounting/models 2>/dev/null || true
  • Step 4: Move report
cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/report/audit_report_template.xml fusion_accounting_ai/report/audit_report_template.xml
rmdir fusion_accounting/report 2>/dev/null || true
  • Step 5: Move security
cd /Users/gurpreet/Github/Odoo-Modules
git rm fusion_accounting_ai/security/ir.model.access.csv  # remove the empty stub from Task 3
git mv fusion_accounting/security/ir.model.access.csv fusion_accounting_ai/security/ir.model.access.csv
# security.xml stays in fusion_accounting/ for now — moves to _core in Task 12
  • Step 6: Move services (preserve adapters/ for AI providers; new data_adapters/ created in Task 8)
cd /Users/gurpreet/Github/Odoo-Modules
git rm fusion_accounting_ai/services/__init__.py
git rm fusion_accounting_ai/services/adapters/__init__.py
git rm fusion_accounting_ai/services/prompts/__init__.py
git rm fusion_accounting_ai/services/tools/__init__.py
git rm fusion_accounting_ai/services/data_adapters/__init__.py  # keep folder, refill in Task 8

git mv fusion_accounting/services/__init__.py fusion_accounting_ai/services/__init__.py
git mv fusion_accounting/services/adapters/__init__.py fusion_accounting_ai/services/adapters/__init__.py
git mv fusion_accounting/services/adapters/claude.py fusion_accounting_ai/services/adapters/claude.py
git mv fusion_accounting/services/adapters/openai_adapter.py fusion_accounting_ai/services/adapters/openai_adapter.py
git mv fusion_accounting/services/agent.py fusion_accounting_ai/services/agent.py
git mv fusion_accounting/services/prompts/__init__.py fusion_accounting_ai/services/prompts/__init__.py
git mv fusion_accounting/services/prompts/domain_prompts.py fusion_accounting_ai/services/prompts/domain_prompts.py
git mv fusion_accounting/services/prompts/system_prompt.py fusion_accounting_ai/services/prompts/system_prompt.py
git mv fusion_accounting/services/scoring.py fusion_accounting_ai/services/scoring.py
git mv fusion_accounting/services/tools/__init__.py fusion_accounting_ai/services/tools/__init__.py
for f in accounts_payable accounts_receivable adp audit bank_reconciliation hst_management inventory journal_review month_end payroll reporting; do
    git mv "fusion_accounting/services/tools/${f}.py" "fusion_accounting_ai/services/tools/${f}.py"
done
rmdir fusion_accounting/services/tools fusion_accounting/services/prompts fusion_accounting/services/adapters fusion_accounting/services 2>/dev/null || true
# Recreate empty data_adapters/__init__.py
echo "" > fusion_accounting_ai/services/data_adapters/__init__.py
  • Step 7: Move static
cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/static/description/icon.png fusion_accounting_ai/static/description/icon.png
for f in approval_card.js approval_card.xml chat_panel.js chat_panel.xml interactive_table.js interactive_table.xml; do
    git mv "fusion_accounting/static/src/components/chat/${f}" "fusion_accounting_ai/static/src/components/chat/${f}"
done
for f in fusion_dashboard.js fusion_dashboard.xml health_card.js health_card.xml; do
    git mv "fusion_accounting/static/src/components/dashboard/${f}" "fusion_accounting_ai/static/src/components/dashboard/${f}"
done
git mv fusion_accounting/static/src/scss/chat.scss fusion_accounting_ai/static/src/scss/chat.scss
git mv fusion_accounting/static/src/scss/dashboard.scss fusion_accounting_ai/static/src/scss/dashboard.scss
rmdir fusion_accounting/static/src/components/chat fusion_accounting/static/src/components/dashboard fusion_accounting/static/src/components fusion_accounting/static/src/scss fusion_accounting/static/src fusion_accounting/static/description fusion_accounting/static 2>/dev/null || true
  • Step 8: Move tests
cd /Users/gurpreet/Github/Odoo-Modules
git rm fusion_accounting_ai/tests/__init__.py  # remove empty stub from Task 3
git mv fusion_accounting/tests/test_api_live.py fusion_accounting_ai/tests/test_api_live.py
git mv fusion_accounting/tests/test_claude_api.py fusion_accounting_ai/tests/test_claude_api.py
# fusion_accounting/tests/__init__.py: check if it exists; if so, move
[ -f fusion_accounting/tests/__init__.py ] && git mv fusion_accounting/tests/__init__.py fusion_accounting_ai/tests/__init__.py || touch fusion_accounting_ai/tests/__init__.py
rmdir fusion_accounting/tests 2>/dev/null || true
  • Step 9: Move views
cd /Users/gurpreet/Github/Odoo-Modules
for f in config_views.xml dashboard_views.xml match_history_views.xml menus.xml recurring_pattern_views.xml rule_views.xml session_views.xml vendor_tax_profile_views.xml; do
    git mv "fusion_accounting/views/${f}" "fusion_accounting_ai/views/${f}"
done
rmdir fusion_accounting/views 2>/dev/null || true
  • Step 10: Move wizards
cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/wizards/__init__.py fusion_accounting_ai/wizards/__init__.py
git mv fusion_accounting/wizards/rule_wizard.py fusion_accounting_ai/wizards/rule_wizard.py
git mv fusion_accounting/wizards/rule_wizard.xml fusion_accounting_ai/wizards/rule_wizard.xml
rmdir fusion_accounting/wizards 2>/dev/null || true
  • Step 11: Verify file moves left only manifest, init, CLAUDE.md, docs in fusion_accounting/

Run:

cd /Users/gurpreet/Github/Odoo-Modules
find fusion_accounting -maxdepth 2 -type f | grep -v "__pycache__" | grep -v ".git" | sort

Expected output (no extra files):

fusion_accounting/CLAUDE.md
fusion_accounting/__init__.py
fusion_accounting/__manifest__.py
fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md
fusion_accounting/docs/superpowers/specs/2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md

If extra files appear, return to the relevant step and move them.

  • Step 12: Update fusion_accounting_ai/__manifest__.py data and assets lists

Replace the data list and assets block in fusion_accounting_ai/__manifest__.py with:

    'data': [
        'security/ir.model.access.csv',
        'data/cron.xml',
        'data/tool_definitions.xml',
        'data/default_rules.xml',
        'views/config_views.xml',
        'views/session_views.xml',
        'views/match_history_views.xml',
        'views/rule_views.xml',
        'views/dashboard_views.xml',
        'views/vendor_tax_profile_views.xml',
        'views/recurring_pattern_views.xml',
        'views/menus.xml',
        'wizards/rule_wizard.xml',
        'report/audit_report_template.xml',
    ],
    'installable': True,
    'application': True,
    'license': 'OPL-1',
    'assets': {
        'web.assets_backend': [
            'fusion_accounting_ai/static/src/**/*.js',
            'fusion_accounting_ai/static/src/**/*.xml',
            'fusion_accounting_ai/static/src/**/*.scss',
        ],
    },

(Compared to current fusion_accounting/__manifest__.py: removes the security/security.xml entry — that file moves to _core in Task 12.)

  • Step 13: Verify fusion_accounting_ai/__init__.py (already correct from Task 3) and fusion_accounting_ai/models/__init__.py (moved from old; review)

Read fusion_accounting_ai/models/__init__.py and confirm it imports the existing model files. The original fusion_accounting/models/__init__.py content carries over verbatim.

  • Step 14: Update internal imports — search for any odoo.addons.fusion_accounting. references

Run:

cd /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai
rg -l "odoo\.addons\.fusion_accounting\b" || echo "No internal absolute imports found"

If matches exist, for each: open the file and change odoo.addons.fusion_accounting.X to odoo.addons.fusion_accounting_ai.X. Then commit.

If no matches (which is expected based on existing relative-import style in the module), skip this step.

  • Step 15: Commit the move
cd /Users/gurpreet/Github/Odoo-Modules
git add -A fusion_accounting fusion_accounting_ai
git commit -m "refactor(fusion_accounting): move AI module code into fusion_accounting_ai sub-module

git mv preserves history. fusion_accounting/ retains only __manifest__.py,
__init__.py, CLAUDE.md, and docs/ — the meta-module shell. All Python,
data, views, security, services, static, tests, wizards, report move to
fusion_accounting_ai/. Manifest data list updated; security.xml move to
_core deferred to Task 12."

Task 6: Convert fusion_accounting/ to Meta-Module

Files:

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/__manifest__.py (rewrite)

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/__init__.py (clear)

  • Step 1: Empty the __init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/__init__.py

# Meta-module: no Python code. All implementation is in sub-modules listed in __manifest__.py 'depends'.
  • Step 2: Rewrite the manifest

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/__manifest__.py

{
    'name': 'Fusion Accounting',
    'version': '19.0.1.0.0',
    'category': 'Accounting/Accounting',
    'sequence': 25,
    'summary': 'Meta-module that installs the full Fusion Accounting suite (core, AI, migration; bank rec, reports, etc. as later sub-modules ship).',
    'description': """
Fusion Accounting (Meta-Module)
===============================
One-click install of the entire Fusion Accounting suite.

Currently installs:
- fusion_accounting_core      Shared schema, security, runtime helpers
- fusion_accounting_ai        AI Co-Pilot (Claude/GPT)
- fusion_accounting_migration Transitional Enterprise->Fusion data migration

Future sub-modules (added per the roadmap as each Phase ships):
- fusion_accounting_bank_rec  (Phase 1)
- fusion_accounting_reports   (Phase 2)
- fusion_accounting_dashboard (Phase 3)
- fusion_accounting_followup  (Phase 5)
- fusion_accounting_assets    (Phase 6)
- fusion_accounting_budget    (Phase 6)

Built by Nexa Systems Inc.
    """,
    'icon': '/fusion_accounting_ai/static/description/icon.png',
    'author': 'Nexa Systems Inc.',
    'website': 'https://nexasystems.ca',
    'support': 'support@nexasystems.ca',
    'maintainer': 'Nexa Systems Inc.',
    'depends': [
        'fusion_accounting_core',
        'fusion_accounting_ai',
        'fusion_accounting_migration',
    ],
    'data': [],
    'installable': True,
    'application': True,
    'license': 'OPL-1',
}

(Note: fusion_accounting_migration is included for now during Phase 0/1 transition. It can be removed from this list later when Enterprise switchover is complete for all clients.)

  • Step 3: Verify the meta-module installs and pulls in sub-modules

Run a deploy + upgrade:

ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting /mnt/extra-addons/fusion_accounting_core /mnt/extra-addons/fusion_accounting_ai /mnt/extra-addons/fusion_accounting_migration"
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration odoo-westin:/tmp/
ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker cp /tmp/\$m odoo-dev-app:/mnt/extra-addons/\$m; done && rm -rf /tmp/fusion_accounting*"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | tail -30"

Expected: clean upgrade. All four modules show as installed. The AI copilot dashboard and chat panel still load in the browser at https://erp.westinhealthcare.ca.

  • Step 4: Smoke test in browser

Manually open the Fusion Accounting menu, start a chat session, send a message. Verify the AI responds. (This validates the move worked end-to-end before any further refactoring.)

  • Step 5: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting/__manifest__.py fusion_accounting/__init__.py
git commit -m "refactor(fusion_accounting): convert to meta-module that depends on sub-modules"

Task 7: Add Post-Migration Script to Reassign ir_model_data Ownership

When models move from fusion_accounting to fusion_accounting_ai, Odoo's registry sees the new owner — but ir_model_data rows still say module = 'fusion_accounting'. This is harmless but causes spurious "module not found" warnings on uninstall and breaks ir.model.data xml-id lookups in some edge cases.

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_post_migration.py

  • Step 1: Write the failing test

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_post_migration.py

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)
  • Step 2: Run the test to confirm it fails (orphans expected before migration runs)

Run:

ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -i fusion_accounting_ai 2>&1 | grep -E 'TestPostMigration|FAIL|ERROR'"

Expected: test_no_orphan_ir_model_data_in_old_module FAILS with "Found N rows still owned by fusion_accounting".

  • Step 3: Write the post-migration script

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py

"""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.

Idempotent: running it a second time does nothing because the WHERE clause
finds no matches.
"""

import logging

_logger = logging.getLogger(__name__)

# Records that move from fusion_accounting -> fusion_accounting_ai.
# Anything beginning with these prefixes is owned by the AI sub-module.
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',
)
# Match anything in views/data/security/wizard/etc. by xml-id name pattern.
AI_NAME_LIKE = (
    'view_fusion_%',
    'action_fusion_%',
    'menu_fusion_%',
    'fusion_tool_%',
    'fusion_rule_%',
    'cron_fusion_%',
    'access_fusion_%',
    'rule_fusion_%',
    'paperformat_fusion_%',
    'report_fusion_%',
    'audit_report_template',
)


def migrate(cr, version):
    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: reassigned %d ir_model_data rows "
        "from module='fusion_accounting' to module='fusion_accounting_ai'",
        moved,
    )
  • Step 4: Trigger the migration by upgrading the module

Run:

ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/\$m; done"
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration odoo-westin:/tmp/
ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker cp /tmp/\$m odoo-dev-app:/mnt/extra-addons/\$m; done && rm -rf /tmp/fusion_accounting*"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting_ai --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | grep -E 'post-migration|reassigned'"

Expected: log line fusion_accounting_ai post-migration: reassigned N ir_model_data rows ... with N > 0.

  • Step 5: Re-run the test, verify it passes
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -i fusion_accounting_ai 2>&1 | grep -E 'TestPostMigration|FAIL|ERROR'"

Expected: test_no_orphan_ir_model_data_in_old_module PASSES.

  • Step 6: Verify idempotency — run upgrade a second time
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting_ai --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | grep -E 'post-migration|reassigned'"

Expected: log line with reassigned 0 ir_model_data rows (idempotent).

  • Step 7: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/migrations fusion_accounting_ai/tests/test_post_migration.py
git commit -m "feat(fusion_accounting_ai): add post-migration to reassign ir_model_data ownership"

Task 8: Build Data-Adapter Base Class

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/base.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/_registry.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py

  • Step 1: Write the failing test

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py

from odoo.tests.common import TransactionCase, tagged
from odoo.addons.fusion_accounting_ai.services.data_adapters.base import (
    DataAdapter, AdapterMode,
)


@tagged('post_install', '-at_install')
class TestDataAdapterBase(TransactionCase):
    """Verify the data adapter base class chooses the correct backend."""

    def test_adapter_mode_pure_community(self):
        """With no fusion native and no Enterprise, adapter selects COMMUNITY."""
        adapter = DataAdapter(self.env)
        # In the test DB we have no fusion_bank_rec installed and likely no Enterprise either
        # (depends on which DB the test runs against). The mode should be one of the three.
        mode = adapter._select_mode(
            fusion_native_model='fusion.bank.rec.widget',
            enterprise_module='account_accountant',
        )
        self.assertIn(mode, (AdapterMode.FUSION, AdapterMode.ENTERPRISE, AdapterMode.COMMUNITY))

    def test_adapter_falls_back_when_fusion_model_missing(self):
        """Adapter must not error when the fusion native model isn't loaded."""
        adapter = DataAdapter(self.env)
        mode = adapter._select_mode(
            fusion_native_model='fusion.never.exists',
            enterprise_module='also_does_not_exist',
        )
        self.assertEqual(mode, AdapterMode.COMMUNITY)
  • Step 2: Run the test, confirm it fails
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -i fusion_accounting_ai 2>&1 | grep -E 'TestDataAdapterBase|ImportError|FAIL'"

Expected: ImportError or ModuleNotFoundError for data_adapters.base.

  • Step 3: Write the base class

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/base.py

"""Data-adapter base class: routes data lookups across three backends.

The fusion_accounting_ai sub-module's tools (e.g. get_unreconciled_bank_lines)
must work in any of three install profiles:

1. FUSION mode    — a fusion native sub-module (e.g. fusion_accounting_bank_rec)
                    is installed; route to its model.
2. ENTERPRISE mode — Odoo Enterprise (e.g. account_accountant) is installed;
                    route to Enterprise APIs.
3. COMMUNITY mode  — neither; fall back to a pure Odoo Community search/read.

Subclasses implement the three backend methods and define which fusion model
and which Enterprise module they probe.
"""

import enum
import logging
from typing import Any

_logger = logging.getLogger(__name__)


class AdapterMode(enum.Enum):
    FUSION = "fusion"
    ENTERPRISE = "enterprise"
    COMMUNITY = "community"


class DataAdapter:
    """Base class. Subclasses set FUSION_MODEL and ENTERPRISE_MODULE class attrs
    and implement _via_fusion(...), _via_enterprise(...), _via_community(...)."""

    # Override in subclasses.
    FUSION_MODEL: str = ""
    ENTERPRISE_MODULE: str = ""

    def __init__(self, env):
        self.env = env

    def _select_mode(
        self,
        fusion_native_model: str | None = None,
        enterprise_module: str | None = None,
    ) -> AdapterMode:
        """Pick FUSION if the model is loaded, else ENTERPRISE if the module
        is installed, else COMMUNITY."""
        fusion_model = fusion_native_model or self.FUSION_MODEL
        ent_module = enterprise_module or self.ENTERPRISE_MODULE

        if fusion_model and fusion_model in self.env:
            return AdapterMode.FUSION

        if ent_module:
            installed = self.env['ir.module.module'].sudo().search_count([
                ('name', '=', ent_module),
                ('state', '=', 'installed'),
            ])
            if installed:
                return AdapterMode.ENTERPRISE

        return AdapterMode.COMMUNITY

    def _dispatch(self, method_name: str, *args, **kwargs) -> Any:
        """Look up <method_name>_via_<mode> on self and call it.

        E.g. method_name='list_unreconciled', mode=FUSION calls
        self.list_unreconciled_via_fusion(*args, **kwargs).
        """
        mode = self._select_mode()
        attr = f"{method_name}_via_{mode.value}"
        impl = getattr(self, attr, None)
        if impl is None:
            _logger.warning(
                "DataAdapter %s has no implementation for %s in mode %s; "
                "returning empty result",
                type(self).__name__, method_name, mode.value,
            )
            return []
        return impl(*args, **kwargs)
  • Step 4: Write the registry

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/_registry.py

"""Registry: lazy-loads data adapter instances per env."""

from .base import DataAdapter


def get_adapter(env, name: str) -> DataAdapter:
    """Return a data adapter by short name. Cached per request via env.context."""
    cache = env.context.get('_fusion_data_adapter_cache')
    if cache is None:
        cache = {}
        # Note: env.context is frozendict-ish in Odoo; this is a simple per-call cache,
        # not a per-request cache. Per-request caching can be layered later if needed.
    if name not in cache:
        cls = _ADAPTERS.get(name)
        if cls is None:
            raise KeyError(f"Unknown data adapter: {name!r}. Known: {list(_ADAPTERS)}")
        cache[name] = cls(env)
    return cache[name]


# Populated as adapter classes are added (Tasks 9, 10, 11, 12).
_ADAPTERS: dict[str, type[DataAdapter]] = {}


def register_adapter(name: str, cls: type[DataAdapter]) -> None:
    """Register an adapter class. Call from each adapter module at import time."""
    _ADAPTERS[name] = cls
  • Step 5: Update data_adapters/__init__.py to export the public API

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py

from .base import DataAdapter, AdapterMode
from ._registry import get_adapter, register_adapter

__all__ = ['DataAdapter', 'AdapterMode', 'get_adapter', 'register_adapter']
  • Step 6: Run the test, verify it passes
# Redeploy and test
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai odoo-westin:/tmp/
ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting_ai && docker cp /tmp/fusion_accounting_ai odoo-dev-app:/mnt/extra-addons/fusion_accounting_ai && rm -rf /tmp/fusion_accounting_ai"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -u fusion_accounting_ai 2>&1 | grep -E 'TestDataAdapterBase|FAIL|PASSED'"

Expected: both TestDataAdapterBase tests PASS.

  • Step 7: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/data_adapters/ fusion_accounting_ai/tests/test_data_adapters.py
git commit -m "feat(fusion_accounting_ai): add DataAdapter base + registry"

Task 9: Implement Bank-Rec Data Adapter

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/bank_rec.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py

  • Step 1: Add a failing test for the bank-rec adapter

Append to /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py:

from odoo.addons.fusion_accounting_ai.services.data_adapters import get_adapter


@tagged('post_install', '-at_install')
class TestBankRecAdapter(TransactionCase):
    """Verify the bank-rec adapter returns rows in any install profile."""

    def setUp(self):
        super().setUp()
        # Create a bank journal + a bank statement line for the test
        self.journal = self.env['account.journal'].create({
            'name': 'Test Bank',
            'type': 'bank',
            'code': 'TBNK',
        })
        self.statement = self.env['account.bank.statement'].create({
            'name': 'Test Statement',
            'journal_id': self.journal.id,
        })
        self.line = self.env['account.bank.statement.line'].create({
            'statement_id': self.statement.id,
            'journal_id': self.journal.id,
            'date': '2026-04-18',
            'payment_ref': 'Test Payment',
            'amount': 100.0,
        })

    def test_list_unreconciled_returns_our_test_line(self):
        """The adapter should find the unreconciled line we just created."""
        adapter = get_adapter(self.env, 'bank_rec')
        rows = adapter.list_unreconciled(journal_id=self.journal.id, limit=10)
        ids = [r['id'] for r in rows]
        self.assertIn(self.line.id, ids,
            f"Expected line {self.line.id} in unreconciled list, got: {ids}")
  • Step 2: Run the test, confirm KeyError: 'Unknown data adapter: bank_rec'
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -i fusion_accounting_ai 2>&1 | grep -E 'TestBankRecAdapter|KeyError|FAIL'"

Expected: KeyError or test failure on get_adapter(env, 'bank_rec').

  • Step 3: Write the adapter

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/bank_rec.py

"""Bank reconciliation data adapter.

Routes bank-rec data lookups across:
- FUSION:    fusion.bank.rec.widget (added by fusion_accounting_bank_rec, Phase 1)
- ENTERPRISE: account_accountant's bank_rec_widget JS service
- COMMUNITY: pure search on account.bank.statement.line
"""

from .base import DataAdapter
from ._registry import register_adapter


class BankRecAdapter(DataAdapter):
    FUSION_MODEL = 'fusion.bank.rec.widget'
    ENTERPRISE_MODULE = 'account_accountant'

    def list_unreconciled(self, journal_id, limit=100):
        """Return unreconciled bank statement lines for a journal."""
        return self._dispatch('list_unreconciled', journal_id=journal_id, limit=limit)

    def list_unreconciled_via_fusion(self, journal_id, limit=100):
        # Phase 1 will add fusion.bank.rec.widget; this method becomes the primary path.
        # For now: even when the model exists, delegate to community read shape.
        return self.list_unreconciled_via_community(journal_id=journal_id, limit=limit)

    def list_unreconciled_via_enterprise(self, journal_id, limit=100):
        # Enterprise's bank rec uses a JS-side service; from Python the cleanest
        # backend access is the same Community search (the data lives in
        # account.bank.statement.line either way). This adapter's purpose is
        # to expose a stable shape to AI tools regardless of which UI the user has.
        return self.list_unreconciled_via_community(journal_id=journal_id, limit=limit)

    def list_unreconciled_via_community(self, journal_id, limit=100):
        Line = self.env['account.bank.statement.line'].sudo()
        records = Line.search([
            ('journal_id', '=', journal_id),
            ('is_reconciled', '=', False),
        ], limit=limit, order='date desc, id desc')
        return [
            {
                'id': r.id,
                'date': r.date,
                'payment_ref': r.payment_ref,
                'amount': r.amount,
                'partner_id': r.partner_id.id if r.partner_id else None,
                'partner_name': r.partner_id.name if r.partner_id else None,
                'currency_id': r.currency_id.id if r.currency_id else None,
            }
            for r in records
        ]


register_adapter('bank_rec', BankRecAdapter)
  • Step 4: Make the adapter import-discoverable

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py to import the adapter so register_adapter runs at load time:

Replace contents with:

from .base import DataAdapter, AdapterMode
from ._registry import get_adapter, register_adapter

# Side-effect imports: each adapter module calls register_adapter at module load.
from . import bank_rec  # noqa: F401

__all__ = ['DataAdapter', 'AdapterMode', 'get_adapter', 'register_adapter']
  • Step 5: Run the test, verify it passes
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai odoo-westin:/tmp/
ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting_ai && docker cp /tmp/fusion_accounting_ai odoo-dev-app:/mnt/extra-addons/fusion_accounting_ai && rm -rf /tmp/fusion_accounting_ai"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -u fusion_accounting_ai 2>&1 | grep -E 'TestBankRecAdapter|FAIL|PASSED'"

Expected: TestBankRecAdapter.test_list_unreconciled_returns_our_test_line PASSES.

  • Step 6: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/data_adapters/ fusion_accounting_ai/tests/test_data_adapters.py
git commit -m "feat(fusion_accounting_ai): add BankRecAdapter for tri-mode bank-rec lookups"

Task 10: Implement Reports Data Adapter

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/reports.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py

  • Step 1: Append the test

Append to /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py:

@tagged('post_install', '-at_install')
class TestReportsAdapter(TransactionCase):
    """Verify the reports adapter computes a trial-balance-shaped result."""

    def test_trial_balance_returns_rows_in_pure_community(self):
        adapter = get_adapter(self.env, 'reports')
        # Compute an empty-filter trial balance for the current company. Should
        # return a list (possibly empty in a fresh test DB) without errors.
        result = adapter.trial_balance()
        self.assertIsInstance(result, list)
        # Each row should have account_id and balance keys
        for row in result:
            self.assertIn('account_id', row)
            self.assertIn('balance', row)
  • Step 2: Run the test, confirm KeyError

Run the same redeploy + test command as in Task 9 Step 5 (substitute TestReportsAdapter in the grep). Expected: KeyError on get_adapter(env, 'reports').

  • Step 3: Write the adapter

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/reports.py

"""Reports data adapter.

Routes report-data lookups across:
- FUSION:     fusion.account.report (added by fusion_accounting_reports, Phase 2)
- ENTERPRISE: account.report from account_reports
- COMMUNITY:  raw aggregations on account.move.line
"""

from .base import DataAdapter
from ._registry import register_adapter


class ReportsAdapter(DataAdapter):
    FUSION_MODEL = 'fusion.account.report'
    ENTERPRISE_MODULE = 'account_reports'

    def trial_balance(self, date_to=None, company_ids=None):
        return self._dispatch('trial_balance', date_to=date_to, company_ids=company_ids)

    def trial_balance_via_fusion(self, date_to=None, company_ids=None):
        # Phase 2 will implement; for now defer to community.
        return self.trial_balance_via_community(date_to=date_to, company_ids=company_ids)

    def trial_balance_via_enterprise(self, date_to=None, company_ids=None):
        # Enterprise account_reports has rich filters; for AI-tool consumption,
        # the community shape suffices and avoids brittle coupling to Odoo's
        # report-line internals.
        return self.trial_balance_via_community(date_to=date_to, company_ids=company_ids)

    def trial_balance_via_community(self, date_to=None, company_ids=None):
        domain = [('parent_state', '=', 'posted')]
        if date_to:
            domain.append(('date', '<=', date_to))
        if company_ids:
            domain.append(('company_id', 'in', list(company_ids)))

        Line = self.env['account.move.line'].sudo()
        groups = Line._read_group(
            domain=domain,
            groupby=['account_id'],
            aggregates=['debit:sum', 'credit:sum'],
        )
        return [
            {
                'account_id': account.id,
                'account_code': account.code,
                'account_name': account.name,
                'debit': debit_sum,
                'credit': credit_sum,
                'balance': debit_sum - credit_sum,
            }
            for account, debit_sum, credit_sum in groups
        ]


register_adapter('reports', ReportsAdapter)
  • Step 4: Update __init__.py to import the adapter

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py:

from .base import DataAdapter, AdapterMode
from ._registry import get_adapter, register_adapter

from . import bank_rec    # noqa: F401
from . import reports     # noqa: F401

__all__ = ['DataAdapter', 'AdapterMode', 'get_adapter', 'register_adapter']
  • Step 5: Run the test, verify pass

Same redeploy + test pattern. Expected: TestReportsAdapter.test_trial_balance_returns_rows_in_pure_community PASSES.

  • Step 6: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/data_adapters/reports.py fusion_accounting_ai/services/data_adapters/__init__.py fusion_accounting_ai/tests/test_data_adapters.py
git commit -m "feat(fusion_accounting_ai): add ReportsAdapter with trial_balance"

Task 11: Implement Followup + Assets Data Adapters

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/followup.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/assets.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py

  • Step 1: Append the failing tests

Append to tests/test_data_adapters.py:

@tagged('post_install', '-at_install')
class TestFollowupAdapter(TransactionCase):
    def test_overdue_invoices_returns_list(self):
        adapter = get_adapter(self.env, 'followup')
        rows = adapter.overdue_invoices(days_overdue=30)
        self.assertIsInstance(rows, list)


@tagged('post_install', '-at_install')
class TestAssetsAdapter(TransactionCase):
    def test_list_assets_returns_list(self):
        adapter = get_adapter(self.env, 'assets')
        rows = adapter.list_assets()
        self.assertIsInstance(rows, list)
  • Step 2: Verify failure

Same redeploy + test command. Expected: KeyError for both adapters.

  • Step 3: Write the followup adapter

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/followup.py

"""Follow-up data adapter."""

from datetime import date, timedelta
from .base import DataAdapter
from ._registry import register_adapter


class FollowupAdapter(DataAdapter):
    FUSION_MODEL = 'fusion.followup.line'
    ENTERPRISE_MODULE = 'account_followup'

    def overdue_invoices(self, days_overdue=30, partner_id=None):
        return self._dispatch('overdue_invoices', days_overdue=days_overdue, partner_id=partner_id)

    def overdue_invoices_via_fusion(self, days_overdue=30, partner_id=None):
        return self.overdue_invoices_via_community(days_overdue=days_overdue, partner_id=partner_id)

    def overdue_invoices_via_enterprise(self, days_overdue=30, partner_id=None):
        return self.overdue_invoices_via_community(days_overdue=days_overdue, partner_id=partner_id)

    def overdue_invoices_via_community(self, days_overdue=30, partner_id=None):
        cutoff = date.today() - timedelta(days=days_overdue)
        domain = [
            ('move_type', 'in', ('out_invoice', 'out_refund')),
            ('state', '=', 'posted'),
            ('payment_state', 'in', ('not_paid', 'partial')),
            ('invoice_date_due', '<=', cutoff),
        ]
        if partner_id:
            domain.append(('partner_id', '=', partner_id))
        moves = self.env['account.move'].sudo().search(domain, limit=200, order='invoice_date_due asc')
        return [
            {
                'id': m.id,
                'name': m.name,
                'partner_id': m.partner_id.id,
                'partner_name': m.partner_id.name,
                'invoice_date_due': m.invoice_date_due,
                'amount_residual': m.amount_residual,
                'currency_id': m.currency_id.id,
                'days_overdue': (date.today() - m.invoice_date_due).days,
            }
            for m in moves
        ]


register_adapter('followup', FollowupAdapter)
  • Step 4: Write the assets adapter

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/assets.py

"""Assets data adapter."""

from .base import DataAdapter
from ._registry import register_adapter


class AssetsAdapter(DataAdapter):
    FUSION_MODEL = 'fusion.asset'
    ENTERPRISE_MODULE = 'account_asset'

    def list_assets(self, state=None):
        return self._dispatch('list_assets', state=state)

    def list_assets_via_fusion(self, state=None):
        return self._read_fusion('fusion.asset', state=state)

    def list_assets_via_enterprise(self, state=None):
        return self._read_fusion('account.asset', state=state)

    def list_assets_via_community(self, state=None):
        # No assets feature in pure Community — return empty list with a hint.
        return []

    def _read_fusion(self, model_name, state=None):
        """Shared shape between fusion and enterprise (both use account.asset-like API)."""
        Model = self.env[model_name].sudo()
        domain = []
        if state:
            domain.append(('state', '=', state))
        records = Model.search(domain, limit=200)
        out = []
        for r in records:
            out.append({
                'id': r.id,
                'name': getattr(r, 'name', None),
                'state': getattr(r, 'state', None),
                'value': getattr(r, 'original_value', None) or getattr(r, 'acquisition_cost', None),
            })
        return out


register_adapter('assets', AssetsAdapter)
  • Step 5: Update __init__.py

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/__init__.py:

from .base import DataAdapter, AdapterMode
from ._registry import get_adapter, register_adapter

from . import bank_rec    # noqa: F401
from . import reports     # noqa: F401
from . import followup    # noqa: F401
from . import assets      # noqa: F401

__all__ = ['DataAdapter', 'AdapterMode', 'get_adapter', 'register_adapter']
  • Step 6: Run tests, verify pass

Same redeploy + test pattern. Both new tests PASS.

  • Step 7: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/data_adapters/ fusion_accounting_ai/tests/test_data_adapters.py
git commit -m "feat(fusion_accounting_ai): add Followup and Assets data adapters"

Task 12: Strip Hard Enterprise Deps from fusion_accounting_ai/__manifest__.py

The manifest currently still depends on account_accountant, account_reports, account_followup (inherited from the original fusion_accounting/__manifest__.py). Replace with only fusion_accounting_core (which depends on Community account).

  • Step 1: Edit the depends line

Open /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/__manifest__.py and verify the 'depends' key reads exactly:

    'depends': ['fusion_accounting_core'],

If it reads anything else (e.g. still has account_accountant), edit it to the line above.

  • Step 2: Verify the AI sub-module installs on a Community-only system

For the verification, we use a separate test instance without Enterprise modules. If only the Westin instance (which has Enterprise) is available, this verification is partial — confirm the manifest loads without dependency errors:

ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting_ai --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | grep -E 'depends.*not.*installed|ERROR|Modules loaded'"

Expected: Modules loaded. line present, no "not installed" errors related to dependencies.

A full empirical test on pure Community is part of Task 18 (the empirical verification test).

  • Step 3: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/__manifest__.py
git commit -m "refactor(fusion_accounting_ai): drop hard deps on Enterprise modules

Now depends only on fusion_accounting_core. Runtime detection of Enterprise
modules happens via data adapters (Tasks 8-11) and the Enterprise-detection
helper (Task 14)."

Task 13: Refactor AI Tools to Use Data Adapters

Per spec Section 4.2: "Refactor every AI tool in fusion_accounting_ai/services/tools/ that calls Enterprise APIs to go through an adapter layer".

The strategy is: (1) survey all tool files, (2) pilot the pattern on bank_reconciliation.py (validate end-to-end), (3) refactor each remaining tool that touches data the adapters can serve.

Files:

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/tools/bank_reconciliation.py

  • Step 1: Read current bank-rec tool to identify the function to refactor

Read /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/tools/bank_reconciliation.py and find the function get_unreconciled_bank_lines (or equivalent — check the actual symbol name in that file).

  • Step 2: Add an in-place adapter call alongside the original implementation

In bank_reconciliation.py, add (at the top with other imports):

from ..data_adapters import get_adapter

Then locate get_unreconciled_bank_lines and refactor:

Before (typical pattern, may differ — adapt to actual code):

def get_unreconciled_bank_lines(env, journal_id, limit=100):
    Line = env['account.bank.statement.line']
    records = Line.search([
        ('journal_id', '=', journal_id),
        ('is_reconciled', '=', False),
    ], limit=limit)
    return [{'id': r.id, ...} for r in records]

After:

def get_unreconciled_bank_lines(env, journal_id, limit=100):
    """Return unreconciled bank lines for a journal.

    Routed through the bank_rec data adapter so the result shape is identical
    whether the install profile is fusion-native, Enterprise, or pure Community.
    """
    adapter = get_adapter(env, 'bank_rec')
    return adapter.list_unreconciled(journal_id=journal_id, limit=limit)

(If the actual existing function signature/name differs, adapt the wrapping accordingly. Keep the function name and outward signature stable so the AI tool registry needs no changes.)

  • Step 3: Run existing AI tool tests and verify still passes
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai odoo-westin:/tmp/
ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting_ai && docker cp /tmp/fusion_accounting_ai odoo-dev-app:/mnt/extra-addons/fusion_accounting_ai && rm -rf /tmp/fusion_accounting_ai"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -u fusion_accounting_ai 2>&1 | grep -E 'TestBankRec|FAIL|ERROR|PASS'"

Expected: existing tests pass; the bank-rec adapter test from Task 9 still passes.

  • Step 4: Browser smoke test

Open the AI chat panel, send: "show me unreconciled bank lines for journal Test Bank". Verify the AI responds with data (and that the data shape didn't change from before).

  • Step 5: Commit the pilot
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/tools/bank_reconciliation.py
git commit -m "refactor(fusion_accounting_ai): route get_unreconciled_bank_lines through BankRecAdapter (pilot)"
  • Step 6: Survey remaining tool files for Enterprise/native-data calls

Run:

cd /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/tools
rg -l "env\[['\"]account\.report['\"]\]|env\[['\"]account\.followup\.line['\"]\]|env\[['\"]account\.asset['\"]\]|env\[['\"]account\.bank\.statement\.line['\"]\]" --type py

Expected: each file in the output is a candidate for adapter refactor. Typical hits: bank_reconciliation.py (already refactored), accounts_receivable.py, accounts_payable.py, reporting.py, possibly audit.py and month_end.py.

For each file in the survey output, perform Steps 7-10 (one file at a time, separate commits).

  • Step 7: Refactor accounts_receivable.py

Read the file, identify functions that search account.move for invoices/aged-balances. For each such function:

  • Add from ..data_adapters import get_adapter at the top
  • For functions that compute aged balances or list overdue invoices, route through get_adapter(env, 'followup').overdue_invoices(...) (extending overdue_invoices shape if extra fields are needed)
  • For functions that list invoices by state (paid/unpaid), keep the direct account.move query — no adapter needed (Community-only data)

Pattern (apply consistently — same as Task 13 Step 2):

Before:

def get_overdue_invoices(env, partner_id=None, days=30):
    domain = [...]
    moves = env['account.move'].search(domain)
    return [{...} for m in moves]

After:

def get_overdue_invoices(env, partner_id=None, days=30):
    """Route through FollowupAdapter for tri-mode consistency."""
    from ..data_adapters import get_adapter
    adapter = get_adapter(env, 'followup')
    return adapter.overdue_invoices(days_overdue=days, partner_id=partner_id)

If the adapter doesn't yet expose the exact shape needed:

  1. Extend the adapter (add a method or extra return key) in data_adapters/followup.py
  2. Add a corresponding test in test_data_adapters.py
  3. Then refactor the tool function

Commit:

cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/tools/accounts_receivable.py fusion_accounting_ai/services/data_adapters/followup.py fusion_accounting_ai/tests/test_data_adapters.py
git commit -m "refactor(fusion_accounting_ai): route accounts_receivable tools through FollowupAdapter"
  • Step 8: Refactor accounts_payable.py

Same pattern as Step 7. AP tools generally search vendor bills (account.move with move_type in ('in_invoice','in_refund')). These are pure Community data — they don't strictly need an adapter. BUT for consistency with the tri-mode promise:

  • If a function computes aged-payables (likely in accounts_payable.py), route through a new FollowupAdapter.aged_payables(...) method (add it to the adapter and write a test first)
  • If a function just lists vendor bills by state, keep direct query

Commit pattern same as Step 7.

  • Step 9: Refactor reporting.py

Tools like get_profit_loss, get_balance_sheet, get_trial_balance, get_partner_ledger, answer_financial_question route through ReportsAdapter.

For each function:

  • Identify the report shape required
  • Add a method on ReportsAdapter that returns that shape (extend data_adapters/reports.py)
  • Add a test in test_data_adapters.py for each new method
  • Refactor the tool function to call the adapter

This is the largest refactor in Step 7-10 because reporting has many functions. If the existing get_trial_balance already matches the adapter's shape (Task 10), only the wrapping changes.

Commit:

cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_ai/services/tools/reporting.py fusion_accounting_ai/services/data_adapters/reports.py fusion_accounting_ai/tests/test_data_adapters.py
git commit -m "refactor(fusion_accounting_ai): route reporting tools through ReportsAdapter"
  • Step 10: Refactor remaining tool files (audit, month_end, hst_management, journal_review)

For each file in the survey output not yet refactored:

  • Identify Enterprise-specific calls (look for account.report, account.followup.line, account.asset)
  • If found: refactor through the appropriate adapter (extending the adapter as needed, with tests)
  • If only Community calls: leave as-is (no adapter needed; no tri-mode concern)
  • Commit per file

For tools that have NO data calls (e.g. some payroll/inventory tools that defer to other modules): skip — no refactor needed.

  • Step 11: Final survey to confirm zero Enterprise-specific imports/refs
cd /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/tools
rg -l "env\[['\"]account\.report['\"]\]|env\[['\"]account\.followup\.line['\"]\]" --type py

Expected: empty output (no remaining direct calls to Enterprise-only models in tool files).

Allowed exceptions:

  • Tools in services/tools/payroll.py, services/tools/adp.py, services/tools/inventory.py may legitimately reference no relevant models (they're stubs per current CLAUDE.md "Known Issues"). Note them in the survey output as "no refactor needed".

If any non-stub tool still has direct Enterprise calls, return to the corresponding step and refactor.

  • Step 12: Final commit (sweep-up if needed)
cd /Users/gurpreet/Github/Odoo-Modules
git add -A fusion_accounting_ai/services/tools fusion_accounting_ai/services/data_adapters fusion_accounting_ai/tests
git diff --cached --stat
# If non-empty:
git commit -m "refactor(fusion_accounting_ai): finish AI tool refactor through data adapters"

Task 14: Add Enterprise Detection Helper in fusion_accounting_core

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/ir_module_module.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/tests/test_enterprise_detection.py

  • Step 1: Write the failing test

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/tests/test_enterprise_detection.py

from odoo.tests.common import TransactionCase, tagged


@tagged('post_install', '-at_install')
class TestEnterpriseDetection(TransactionCase):
    """Verify the helper that detects Odoo Enterprise accounting installs."""

    def test_helper_returns_bool(self):
        result = self.env['ir.module.module']._fusion_is_enterprise_accounting_installed()
        self.assertIsInstance(result, bool)

    def test_helper_matches_actual_state(self):
        """Helper should return True iff one of the known Enterprise modules is installed."""
        installed = self.env['ir.module.module'].sudo().search_count([
            ('name', 'in', ['account_accountant', 'account_reports', 'accountant']),
            ('state', '=', 'installed'),
        ])
        expected = bool(installed)
        actual = self.env['ir.module.module']._fusion_is_enterprise_accounting_installed()
        self.assertEqual(actual, expected)
  • Step 2: Run, confirm AttributeError
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -i fusion_accounting_core 2>&1 | grep -E 'TestEnterpriseDetection|AttributeError|FAIL'"

Expected: AttributeError on _fusion_is_enterprise_accounting_installed.

  • Step 3: Write the helper

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/ir_module_module.py

from odoo import api, models


# Modules considered "Odoo Enterprise accounting" for the purpose of feature gating.
# A client is "on Enterprise" if any of these are installed; fusion_accounting_*
# replacement modules will hide their menus when Enterprise is present (replace mode
# vs. augment mode is configurable in Settings).
ENTERPRISE_ACCOUNTING_MODULES = (
    'account_accountant',
    'account_reports',
    'accountant',
)


class IrModuleModule(models.Model):
    _inherit = "ir.module.module"

    @api.model
    def _fusion_is_enterprise_accounting_installed(self):
        """True if any Odoo Enterprise accounting module is installed in this DB."""
        return bool(self.sudo().search_count([
            ('name', 'in', list(ENTERPRISE_ACCOUNTING_MODULES)),
            ('state', '=', 'installed'),
        ]))

    @api.model
    def _fusion_is_module_installed(self, module_name):
        """True if a specific module is installed."""
        return bool(self.sudo().search_count([
            ('name', '=', module_name),
            ('state', '=', 'installed'),
        ]))
  • Step 4: Update models/__init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/__init__.py

from . import ir_module_module
  • Step 5: Run tests, verify pass
scp -r /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core odoo-westin:/tmp/
ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting_core && docker cp /tmp/fusion_accounting_core odoo-dev-app:/mnt/extra-addons/fusion_accounting_core && rm -rf /tmp/fusion_accounting_core"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -u fusion_accounting_core 2>&1 | grep -E 'TestEnterpriseDetection|FAIL|PASS'"

Expected: both tests PASS.

  • Step 6: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_core/models fusion_accounting_core/tests
git commit -m "feat(fusion_accounting_core): add _fusion_is_enterprise_accounting_installed helper"

Task 15: Add Shared-Field-Ownership Models in fusion_accounting_core

Per spec Section 3.3, fusion_accounting_core declares the same fields Enterprise adds to account.move and account.reconcile.model, with identical relation tables, so the data survives Enterprise uninstall.

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/account_move.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/account_reconcile_model.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/tests/test_shared_field_ownership.py

  • Step 1: Write the failing test

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/tests/test_shared_field_ownership.py

from odoo.tests.common import TransactionCase, tagged


@tagged('post_install', '-at_install')
class TestSharedFieldOwnership(TransactionCase):
    """Verify fusion_accounting_core declares the Enterprise extension fields
    on account.move and account.reconcile.model, so they survive Enterprise uninstall."""

    def test_account_move_deferred_fields_exist(self):
        Move = self.env['account.move']
        for fname in ('deferred_move_ids', 'deferred_original_move_ids', 'deferred_entry_type'):
            self.assertIn(fname, Move._fields, f"{fname!r} must exist on account.move")

    def test_account_move_signing_user_exists(self):
        Move = self.env['account.move']
        self.assertIn('signing_user', Move._fields)

    def test_account_move_payment_state_before_switch_exists(self):
        Move = self.env['account.move']
        self.assertIn('payment_state_before_switch', Move._fields)

    def test_account_reconcile_model_created_automatically_exists(self):
        Model = self.env['account.reconcile.model']
        self.assertIn('created_automatically', Model._fields)

    def test_deferred_relation_table_name_matches_enterprise(self):
        """The shared M2M relation table must be named identically to Enterprise's
        so dual ownership works (Enterprise drops field => fusion preserves table)."""
        f = self.env['account.move']._fields['deferred_move_ids']
        self.assertEqual(f.relation, 'account_move_deferred_rel')
        self.assertEqual(f.column1, 'original_move_id')
        self.assertEqual(f.column2, 'deferred_move_id')
  • Step 2: Run, confirm test fails (fields don't exist on stock account.move)

Same redeploy + test command pattern with -i fusion_accounting_core. Expected: AssertionError on the first test.

  • Step 3: Write account_move.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/account_move.py

"""Shared-field-ownership declarations for account.move.

Per the roadmap (Section 3.3), these fields exist in Odoo Enterprise's
account_accountant module. By declaring them here with identical schemas
and identical relation tables, fusion_accounting_core becomes a co-owner.
When Enterprise uninstalls, Odoo's module registry sees fusion still owns
the fields and preserves the columns / relation tables, so the data
(deferred revenue links, signing user, etc.) survives uninstall.

The fields here have NO compute methods, NO defaults beyond what Enterprise
provides, NO views. They're pure schema-preservation declarations. Any
business logic that operates on these fields lives in Enterprise (when
present) or in a future fusion sub-module that opts to own that behavior.
"""

from odoo import fields, models


class AccountMove(models.Model):
    _inherit = "account.move"

    # Deferred revenue / expense linkage (Enterprise: account_accountant/models/account_move.py)
    deferred_move_ids = fields.Many2many(
        comodel_name='account.move',
        relation='account_move_deferred_rel',
        column1='original_move_id',
        column2='deferred_move_id',
        copy=False,
        string="Deferred Entries",
    )
    deferred_original_move_ids = fields.Many2many(
        comodel_name='account.move',
        relation='account_move_deferred_rel',
        column1='deferred_move_id',
        column2='original_move_id',
        copy=False,
        string="Original Invoices",
    )
    deferred_entry_type = fields.Selection(
        selection=[
            ('expense', 'Deferred Expense'),
            ('revenue', 'Deferred Revenue'),
        ],
        copy=False,
        string="Deferred Entry Type",
    )

    # Signing / audit (Enterprise: account_accountant/models/account_move.py)
    signing_user = fields.Many2one(
        comodel_name='res.users',
        copy=False,
        string="Signing User",
    )

    # Switch-state preservation (Enterprise: account_accountant/models/account_move.py)
    payment_state_before_switch = fields.Char(
        copy=False,
        string="Payment State Before Switch",
    )
  • Step 4: Write account_reconcile_model.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/account_reconcile_model.py

"""Shared-field-ownership for account.reconcile.model.

Mirrors the single field Enterprise's account_accountant adds to the
Community account.reconcile.model: created_automatically.
"""

from odoo import fields, models


class AccountReconcileModel(models.Model):
    _inherit = "account.reconcile.model"

    created_automatically = fields.Boolean(
        default=False,
        copy=False,
        string="Created Automatically",
    )
  • Step 5: Update models/__init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/models/__init__.py

from . import ir_module_module
from . import account_move
from . import account_reconcile_model
  • Step 6: Run tests, verify pass

Same redeploy + test pattern. Expected: all five TestSharedFieldOwnership tests PASS.

  • Step 7: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_core/models/account_move.py fusion_accounting_core/models/account_reconcile_model.py fusion_accounting_core/models/__init__.py fusion_accounting_core/tests/test_shared_field_ownership.py
git commit -m "feat(fusion_accounting_core): shared-field-ownership for deferred fields, signing_user, created_automatically"

Task 16: Migrate Security Groups to fusion_accounting_core

Move the three security groups (group_fusion_accounting_user/manager/admin), the privilege, the auto-assignment to account.group_account_*, and the existing record rules.

The current security/security.xml file currently lives at fusion_accounting/security/security.xml (it was NOT moved in Task 5 — by design — because it should land in _core, not _ai).

Files:

  • Move: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/security/security.xml/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/security/fusion_accounting_security.xml

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__manifest__.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py (extend to also reassign security xml-ids)

  • Step 1: Move the file

cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/security/security.xml fusion_accounting_core/security/fusion_accounting_security.xml
rmdir fusion_accounting/security 2>/dev/null || true
  • Step 2: Edit the moved file — fix model_id refs that point to AI models

Open /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/security/fusion_accounting_security.xml.

Existing model_id refs use model_fusion_accounting_session, model_fusion_accounting_match_history, model_fusion_accounting_tool, model_fusion_accounting_rule. These models live in fusion_accounting_ai after Task 5. Update each model_id ref="..." to use the fully-qualified xml-id:

For each <field name="model_id" ref="model_fusion_accounting_session"/> etc., change to:

<field name="model_id" ref="fusion_accounting_ai.model_fusion_accounting_session"/>

(Apply the same change to model_fusion_accounting_match_history, model_fusion_accounting_tool, model_fusion_accounting_rule.)

This requires fusion_accounting_core to declare fusion_accounting_ai as a dependency for cross-module xml-id references in data files... BUT that creates a circular dep (_ai depends on _core already).

Resolution: instead of putting the record rules in _core, put them in _ai (where the models live). Only the GROUPS go in _core.

Revise the move:

cd /Users/gurpreet/Github/Odoo-Modules
# Undo the previous move:
git mv fusion_accounting_core/security/fusion_accounting_security.xml fusion_accounting/security/security.xml
mkdir -p fusion_accounting/security

Now split the file:

  • Step 3: Create _core/security/fusion_accounting_security.xml (groups + privilege only)

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/security/fusion_accounting_security.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- Module Category -->
    <record id="module_category_fusion_accounting" model="ir.module.category">
        <field name="name">Fusion Accounting</field>
        <field name="sequence">25</field>
    </record>

    <!-- Groups Privilege -->
    <record id="res_groups_privilege_fusion_accounting" model="res.groups.privilege">
        <field name="name">Fusion Accounting</field>
        <field name="category_id" ref="module_category_fusion_accounting"/>
    </record>

    <!-- User Group (Staff) -->
    <record id="group_fusion_accounting_user" model="res.groups">
        <field name="name">User</field>
        <field name="sequence">10</field>
        <field name="implied_ids" eval="[(4, ref('account.group_account_user'))]"/>
        <field name="privilege_id" ref="res_groups_privilege_fusion_accounting"/>
    </record>

    <!-- Manager Group -->
    <record id="group_fusion_accounting_manager" model="res.groups">
        <field name="name">Manager</field>
        <field name="sequence">20</field>
        <field name="implied_ids" eval="[(4, ref('group_fusion_accounting_user'))]"/>
        <field name="privilege_id" ref="res_groups_privilege_fusion_accounting"/>
    </record>

    <!-- Admin Group -->
    <record id="group_fusion_accounting_admin" model="res.groups">
        <field name="name">Administrator</field>
        <field name="sequence">30</field>
        <field name="implied_ids" eval="[(4, ref('group_fusion_accounting_manager'))]"/>
        <field name="privilege_id" ref="res_groups_privilege_fusion_accounting"/>
    </record>

    <!-- Auto-assign: Accounting users get Fusion User; Advisers get Admin -->
    <record id="account.group_account_user" model="res.groups">
        <field name="implied_ids" eval="[(4, ref('group_fusion_accounting_user'))]"/>
    </record>
    <record id="account.group_account_manager" model="res.groups">
        <field name="implied_ids" eval="[(4, ref('group_fusion_accounting_admin'))]"/>
    </record>
</odoo>
  • Step 4: Replace _ai/security files

Move the old security.xml from fusion_accounting/ to fusion_accounting_ai/ and edit it down to just the record rules (groups removed):

cd /Users/gurpreet/Github/Odoo-Modules
git mv fusion_accounting/security/security.xml fusion_accounting_ai/security/fusion_accounting_ai_security.xml
rmdir fusion_accounting/security 2>/dev/null || true

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/security/fusion_accounting_ai_security.xml — replace ENTIRE contents with:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- Per-user record rules (sessions visible only to the owning user; managers see all) -->
    <record id="rule_fusion_session_user" model="ir.rule">
        <field name="name">Fusion Session: Own Sessions</field>
        <field name="model_id" ref="model_fusion_accounting_session"/>
        <field name="domain_force">[('user_id', '=', user.id)]</field>
        <field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_user'))]"/>
    </record>

    <record id="rule_fusion_session_manager" model="ir.rule">
        <field name="name">Fusion Session: All Sessions</field>
        <field name="model_id" ref="model_fusion_accounting_session"/>
        <field name="domain_force">[(1, '=', 1)]</field>
        <field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_manager'))]"/>
    </record>

    <record id="rule_fusion_history_user" model="ir.rule">
        <field name="name">Fusion History: Own History</field>
        <field name="model_id" ref="model_fusion_accounting_match_history"/>
        <field name="domain_force">[('session_id.user_id', '=', user.id)]</field>
        <field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_user'))]"/>
    </record>

    <record id="rule_fusion_history_manager" model="ir.rule">
        <field name="name">Fusion History: All History</field>
        <field name="model_id" ref="model_fusion_accounting_match_history"/>
        <field name="domain_force">[(1, '=', 1)]</field>
        <field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_manager'))]"/>
    </record>

    <!-- Multi-company rules -->
    <record id="rule_fusion_tool_company" model="ir.rule">
        <field name="name">Fusion Tool: Multi-Company</field>
        <field name="model_id" ref="model_fusion_accounting_tool"/>
        <field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
    </record>

    <record id="rule_fusion_rule_company" model="ir.rule">
        <field name="name">Fusion Rule: Multi-Company</field>
        <field name="model_id" ref="model_fusion_accounting_rule"/>
        <field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
    </record>

    <record id="rule_fusion_history_company" model="ir.rule">
        <field name="name">Fusion History: Multi-Company</field>
        <field name="model_id" ref="model_fusion_accounting_match_history"/>
        <field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
    </record>

    <!-- NEW (Phase 0): Multi-company rule on session itself
         (per spec Section 4.2 + existing CLAUDE.md Known Issues) -->
    <record id="rule_fusion_session_company" model="ir.rule">
        <field name="name">Fusion Session: Multi-Company</field>
        <field name="model_id" ref="model_fusion_accounting_session"/>
        <field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
    </record>
</odoo>

(Note: this includes the missing multi-company record rule on fusion.accounting.session per the spec.)

The rule above references a company_id field on fusion.accounting.session. Verify the model has that field — if not, also add it to the model.

  • Step 5: Verify company_id exists on fusion.accounting.session

Run:

rg "company_id" /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/models/accounting_session.py

If company_id is not declared on the session model, add it:

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/models/accounting_session.py to include (in the field definitions section):

    company_id = fields.Many2one(
        comodel_name='res.company',
        default=lambda self: self.env.company,
        required=True,
        index=True,
    )
  • Step 6: Update _core/__manifest__.py data list

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__manifest__.py 'data' key:

    'data': [
        'security/fusion_accounting_security.xml',
        'security/ir.model.access.csv',
    ],
  • Step 7: Update _ai/__manifest__.py data list to include the new security file

Open /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/__manifest__.py and adjust 'data':

    'data': [
        'security/ir.model.access.csv',
        'security/fusion_accounting_ai_security.xml',
        'data/cron.xml',
        'data/tool_definitions.xml',
        'data/default_rules.xml',
        'views/config_views.xml',
        'views/session_views.xml',
        'views/match_history_views.xml',
        'views/rule_views.xml',
        'views/dashboard_views.xml',
        'views/vendor_tax_profile_views.xml',
        'views/recurring_pattern_views.xml',
        'views/menus.xml',
        'wizards/rule_wizard.xml',
        'report/audit_report_template.xml',
    ],
  • Step 8: Extend post-migration to reassign security xml-ids

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/migrations/19.0.1.0.0/post-migration.py and add to the migrate function (before the existing UPDATE):

def migrate(cr, version):
    # First: reassign security GROUPS that moved to fusion_accounting_core
    cr.execute("""
        UPDATE ir_model_data
        SET module = 'fusion_accounting_core'
        WHERE module = 'fusion_accounting'
          AND name IN (
              'module_category_fusion_accounting',
              'res_groups_privilege_fusion_accounting',
              'group_fusion_accounting_user',
              'group_fusion_accounting_manager',
              'group_fusion_accounting_admin'
          )
    """)
    moved_to_core = cr.rowcount

    # Then: reassign AI-owned records (existing logic) ...
    # [keep the existing UPDATE statement here]
    cr.execute(""" ... """, (list(AI_MODEL_PREFIXES), list(AI_NAME_LIKE)))
    moved_to_ai = cr.rowcount

    _logger.info(
        "fusion_accounting_ai post-migration: reassigned %d rows to _core, %d rows to _ai",
        moved_to_core, moved_to_ai,
    )

(Replace the existing single UPDATE with the two-step version above. Keep AI_MODEL_PREFIXES and AI_NAME_LIKE constants unchanged.)

  • Step 9: Add a similar migration to fusion_accounting_core (one-time, for old groups)

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/migrations/19.0.1.0.0/post-migration.py

mkdir -p /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/migrations/19.0.1.0.0
"""On first install, reassign group/category xml-ids from old module name."""

import logging
_logger = logging.getLogger(__name__)


def migrate(cr, version):
    cr.execute("""
        UPDATE ir_model_data
        SET module = 'fusion_accounting_core'
        WHERE module = 'fusion_accounting'
          AND name IN (
              'module_category_fusion_accounting',
              'res_groups_privilege_fusion_accounting',
              'group_fusion_accounting_user',
              'group_fusion_accounting_manager',
              'group_fusion_accounting_admin'
          )
    """)
    moved = cr.rowcount
    _logger.info("fusion_accounting_core post-migration: reassigned %d group records", moved)

(Both _core and _ai post-migrations safely run the same UPDATE — whichever runs first wins, the other is a no-op.)

  • Step 10: Deploy + upgrade + verify
cd /Users/gurpreet/Github/Odoo-Modules
ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/\$m; done"
scp -r fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration odoo-westin:/tmp/
ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker cp /tmp/\$m odoo-dev-app:/mnt/extra-addons/\$m; done && rm -rf /tmp/fusion_accounting*"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | grep -E 'post-migration|reassigned|ERROR'"

Expected: post-migration log lines show non-zero rows reassigned for the security records on the first run; subsequent runs show zero.

  • Step 11: Smoke test the security still works

Open the Odoo Settings → Users & Companies → Users page. Edit any user. Confirm the "Fusion Accounting" privilege section appears with three radio options (User, Manager, Admin). Existing users keep their previous group assignments.

  • Step 12: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_core/security fusion_accounting_core/__manifest__.py fusion_accounting_core/migrations
git add fusion_accounting_ai/security fusion_accounting_ai/__manifest__.py fusion_accounting_ai/migrations fusion_accounting_ai/models/accounting_session.py
git commit -m "refactor(fusion_accounting): move security groups to _core, add multi-company session rule"

Task 17: Build the Migration Safety Guard in fusion_accounting_migration

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/models/ir_module_module.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/models/__init__.py

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/migration_wizard.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/__init__.py

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/migration_wizard_views.xml

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/tests/test_safety_guard.py

  • Step 1: Write the failing test

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/tests/test_safety_guard.py

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."""
        # Set the flag for account_accountant
        self.env['ir.config_parameter'].sudo().set_param(
            'fusion_accounting.migration.account_accountant.completed', 'True'
        )
        # Find a fake module to call our guard on (we don't actually uninstall)
        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'
        )
        # Only assert if account_accountant is actually installed in this DB
        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())
  • Step 2: Run, confirm AttributeError
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf -i fusion_accounting_migration 2>&1 | grep -E 'TestSafetyGuard|AttributeError|FAIL'"

Expected: AttributeError on _fusion_check_uninstall_guard.

  • Step 3: Write the safety guard

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/models/ir_module_module.py

"""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()
  • Step 4: Update models/__init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/models/__init__.py

from . import ir_module_module
  • Step 5: Write the wizard skeleton

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/migration_wizard.py

"""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."
                ),
            },
        }
  • Step 6: Update wizards/__init__.py

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/__init__.py

from . import migration_wizard
  • Step 7: Write wizards/migration_wizard_views.xml

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/migration_wizard_views.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <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>
  • Step 8: Add ACL row for the wizard

Edit /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/security/ir.model.access.csv:

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
  • Step 9: Deploy + run tests

Same redeploy + test pattern with -u fusion_accounting_migration. Expected: both TestSafetyGuard tests PASS (the second one might skip if account_accountant isn't installed; either is acceptable).

  • Step 10: Manual smoke test the guard

In the Odoo UI, go to Apps → search for "Invoicing" (account_accountant). Click the menu → Uninstall. The wizard should display the safety-guard error message pointing to the migration wizard. (Cancel — do not actually proceed.)

  • Step 11: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_migration/
git commit -m "feat(fusion_accounting_migration): add Enterprise uninstall safety guard + wizard skeleton"

Task 18: Empirical Verification Test (Section 3.6)

This is a one-time test on a throwaway Odoo 19 Enterprise instance. The goal is to validate (or update) the data-preservation analysis from Section 3 of the spec.

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/docs/superpowers/specs/2026-04-18-empirical-uninstall-test-results.md

  • Step 1: Provision a throwaway test environment

Either:

  • (a) Spin up a fresh Odoo 19 Enterprise Docker container locally, or
  • (b) Use the existing odoo-westin instance against a clone of westin-v19 named westin-v19-empirical-test, or
  • (c) Use a separate test Odoo SaaS trial.

Whichever option, document the chosen environment in the test results doc.

  • Step 2: Install Enterprise stack + create representative test data

Install: account, account_accountant, account_reports, accountant, account_followup, account_asset, account_budget.

Create:

  • 50 customer invoices, mix of paid/partial/unpaid
  • 30 vendor bills, mix of paid/unpaid
  • 5 deferred-revenue invoices with deferred_move_ids populated
  • 3 fiscal year closings via Settings -> Accounting -> Fiscal Year
  • 10 asset records with depreciation history
  • 2 budgets with at least one budget line each
  • 15 bank reconciliations (full and partial mix)
  • 1 cash-basis tax move
  • A few multi-currency journal entries (USD + EUR if base is CAD)
  • A 2-level follow-up workflow with 1 partner having follow-up history

Document each creation step in the test results doc.

  • Step 3: Snapshot the database
ssh odoo-westin "docker exec odoo-dev-db pg_dump -U odoo westin-v19-empirical-test > /tmp/before-uninstall.sql"
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19-empirical-test -c \"SELECT count(*) FROM account_partial_reconcile;\" > /tmp/before-counts.txt"
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19-empirical-test -c \"SELECT count(*) FROM account_move WHERE deferred_move_ids IS NOT NULL;\" >> /tmp/before-counts.txt"
# Continue with all relevant tables
  • Step 4: Uninstall Enterprise WITHOUT running fusion's migration wizard
# Disable the safety guard temporarily by setting all flags to True
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19-empirical-test -c \"
  INSERT INTO ir_config_parameter (key, value)
  VALUES ('fusion_accounting.migration.account_accountant.completed', 'True'),
         ('fusion_accounting.migration.account_reports.completed', 'True'),
         ('fusion_accounting.migration.accountant.completed', 'True'),
         ('fusion_accounting.migration.account_followup.completed', 'True'),
         ('fusion_accounting.migration.account_asset.completed', 'True'),
         ('fusion_accounting.migration.account_budget.completed', 'True')
  ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
\""

# Uninstall in dep-safe order
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19-empirical-test -u account_followup --stop-after-init -c /etc/odoo/odoo.conf"  # not actually uninstalling here; uninstall via UI is more reliable
# Use the UI: Apps -> account_reports -> Uninstall, then account_accountant, etc.
  • Step 5: Snapshot post-uninstall and diff
ssh odoo-westin "docker exec odoo-dev-db pg_dump -U odoo westin-v19-empirical-test --schema-only > /tmp/after-uninstall.sql"
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19-empirical-test -c \"SELECT count(*) FROM account_partial_reconcile;\" > /tmp/after-counts.txt"
# Run all the same count queries as before

diff /tmp/before-counts.txt /tmp/after-counts.txt > /tmp/data-diff.txt
diff /tmp/before-uninstall.sql /tmp/after-uninstall.sql > /tmp/schema-diff.txt

Confirm:

  • account_partial_reconcile row count unchanged

  • account.move row count unchanged

  • account_move_deferred_rel table dropped (since we're testing without fusion shared-field-ownership)

  • account_asset table dropped (no fusion native to host it)

  • account_fiscal_year table dropped

  • Step 6: Now repeat the test with fusion installed first

Restore from before-uninstall.sql. Install fusion_accounting (meta-module, which pulls in _core with shared-field-ownership). Re-run uninstall. Confirm:

  • account_move_deferred_rel table NOT dropped (fusion_accounting_core retains ownership)

  • Deferred-move M2M data preserved

  • Step 7: Document findings

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/docs/superpowers/specs/2026-04-18-empirical-uninstall-test-results.md

Write a structured report with sections:

  • Test environment

  • Data created

  • Pre-snapshot table/row counts

  • Uninstall steps performed

  • Post-snapshot table/row counts

  • Data preservation verdict (per category)

  • Gaps vs. Section 3 of the roadmap design doc (if any)

  • Updates required to the migration wizard scope (if any)

  • Step 8: If gaps found, update the spec + create follow-up tasks

If the empirical test reveals data loss not anticipated in Section 3.2:

  • Edit the design doc Section 3.2 to add the missing entries

  • Add a follow-up task to either extend fusion_accounting_core/models/ (more shared-field declarations) or extend the migration wizard

  • Step 9: Commit the test results

cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting/docs/superpowers/specs/2026-04-18-empirical-uninstall-test-results.md
git commit -m "docs(fusion_accounting): record empirical Enterprise uninstall test results"

Task 19: Create tools/check_odoo_diff.sh

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/tools/check_odoo_diff.sh

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/tools/README.md

  • Step 1: Create the directory

mkdir -p /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/tools
  • Step 2: Write the script

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/tools/check_odoo_diff.sh

#!/usr/bin/env bash
# check_odoo_diff.sh
#
# Diff a single Odoo Enterprise accounting module across two pinned snapshots
# and produce a categorized change report.
#
# Usage:
#   tools/check_odoo_diff.sh <module> <from_version> <to_version> [<output_md>]
#
# Example:
#   tools/check_odoo_diff.sh account_accountant v19 v20 reports/v20_accountant_diff.md

set -euo pipefail

MODULE="${1:?Usage: check_odoo_diff.sh <module> <from_version> <to_version> [<output_md>]}"
FROM="${2:?from_version required (e.g. v19)}"
TO="${3:?to_version required (e.g. v20)}"
OUT="${4:-/dev/stdout}"

ROOT="${REPACKAGED_ODOO_ROOT:-/Users/gurpreet/Github/RePackaged-Odoo}"
FROM_DIR="$ROOT/accounting-$FROM/$MODULE"
TO_DIR="$ROOT/accounting-$TO/$MODULE"

if [ ! -d "$FROM_DIR" ]; then
    echo "ERROR: $FROM_DIR does not exist. Snapshot v$FROM not yet present?" >&2
    exit 1
fi
if [ ! -d "$TO_DIR" ]; then
    echo "ERROR: $TO_DIR does not exist. Snapshot v$TO not yet present?" >&2
    exit 1
fi

classify() {
    local f="$1"
    case "$f" in
        */views/*|*/static/src/components/*|*/report/*|*/wizard/*_views.xml|*/wizards/*_views.xml)
            echo "[MIRROR]" ;;
        */models/*_engine.py|*/services/*)
            echo "[ABSTRACT]" ;;
        */__manifest__.py)
            echo "[MANIFEST]" ;;
        */tests/*)
            echo "[TEST]" ;;
        *)
            echo "[REVIEW]" ;;
    esac
}

{
    echo "# Diff Report: $MODULE ($FROM -> $TO)"
    echo ""
    echo "Generated: $(date '+%Y-%m-%d %H:%M:%S')"
    echo ""
    echo "## Changed Files (with classification suggestion)"
    echo ""
    diff -ruN --brief "$FROM_DIR" "$TO_DIR" | while read -r line; do
        # parse "Files X and Y differ" or "Only in X: Y"
        case "$line" in
            "Files "*" and "*" differ")
                file=$(echo "$line" | sed -E 's/^Files (.+) and .+ differ$/\1/' | sed "s|$FROM_DIR/||")
                tag=$(classify "$file")
                echo "- $tag \`$file\`"
                ;;
            "Only in $TO_DIR"*)
                file=$(echo "$line" | sed -E "s|Only in $TO_DIR(.*): (.+)|\1/\2|" | sed "s|^/||")
                tag=$(classify "$file")
                echo "- $tag NEW: \`$file\`"
                ;;
            "Only in $FROM_DIR"*)
                file=$(echo "$line" | sed -E "s|Only in $FROM_DIR(.*): (.+)|\1/\2|" | sed "s|^/||")
                tag=$(classify "$file")
                echo "- $tag REMOVED: \`$file\`"
                ;;
        esac
    done
    echo ""
    echo "## Full Diff (truncated to first 2000 lines)"
    echo ""
    echo '```diff'
    diff -ruN "$FROM_DIR" "$TO_DIR" | head -2000
    echo '```'
} > "$OUT"

echo "Diff report written to: $OUT" >&2
  • Step 3: Make it executable
chmod +x /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/tools/check_odoo_diff.sh
  • Step 4: Write tools/README.md

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/tools/README.md

# Fusion Accounting Tooling

## check_odoo_diff.sh

Diff a single Odoo Enterprise accounting module across two pinned snapshots
in `RePackaged-Odoo/` and produce a categorized change report (markdown).

### Usage

```bash
tools/check_odoo_diff.sh <module> <from_version> <to_version> [<output_md>]

Example

# When Odoo 20 ships, get a full report on what changed in account_accountant
tools/check_odoo_diff.sh account_accountant v19 v20 > reports/v20_accountant.md

Classification tags

  • [MIRROR] — mechanical port required (view XML, OWL component, PDF template, wizard view)
  • [ABSTRACT] — verify our adapter still aligns; update if Odoo's public API surface changed
  • [MANIFEST] — manifest changes (deps, asset bundles, version, hooks)
  • [TEST] — Odoo's tests changed; check if our equivalents need updates
  • [REVIEW] — uncategorized; manual review needed

Snapshot conventions

Snapshots live at $REPACKAGED_ODOO_ROOT/accounting-<version>/<module> (default root: /Users/gurpreet/Github/RePackaged-Odoo). Override the root with the REPACKAGED_ODOO_ROOT env var.


- [ ] **Step 5: Smoke test the script**

```bash
cd /Users/gurpreet/Github/Odoo-Modules
ls /Users/gurpreet/Github/RePackaged-Odoo/
# Currently we only have one snapshot at "accounting/" not "accounting-v19/"
# So either rename the snapshot or just verify the script's error path works:
./fusion_accounting/tools/check_odoo_diff.sh account_accountant v19 v20 || echo "Expected error (no v20 snapshot yet)"

To prepare for V20 in the future:

# When ready, rename current snapshot to versioned form
mv /Users/gurpreet/Github/RePackaged-Odoo/accounting /Users/gurpreet/Github/RePackaged-Odoo/accounting-v19
# After V20 ships, drop a new snapshot at accounting-v20/

(Don't actually rename now — keep current state working. Just document the convention in the tools README.)

  • Step 6: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting/tools
git commit -m "feat(fusion_accounting): add check_odoo_diff.sh for cross-version upgrade ritual"

Task 20: Per-Sub-Module CLAUDE.md, UPGRADE_NOTES.md, README.md

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/CLAUDE.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/UPGRADE_NOTES.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/README.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/CLAUDE.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/UPGRADE_NOTES.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/README.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/CLAUDE.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/UPGRADE_NOTES.md

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/README.md

  • Modify: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/CLAUDE.md (rewrite as meta-module overview)

  • Create: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/README.md

  • Step 1: Write fusion_accounting_core/CLAUDE.md

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/CLAUDE.md

# fusion_accounting_core — Cursor / Claude Context

## Purpose
Foundation for the Fusion Accounting sub-module suite. Owns:
- Three security groups (User / Manager / Admin) shared across all sub-modules
- Shared-field-ownership declarations on `account.move` and `account.reconcile.model`
- Runtime Enterprise-detection helper: `env['ir.module.module']._fusion_is_enterprise_accounting_installed()`

## What lives here
- `models/account_move.py` — declares Enterprise-extension fields with identical
  schemas / relation tables. Pure schema-preservation; no business logic.
- `models/account_reconcile_model.py` — same pattern for `created_automatically`
- `models/ir_module_module.py` — Enterprise-detection helpers
- `security/fusion_accounting_security.xml` — privilege + 3 groups + auto-assignment

## Critical rules
- NEVER add business logic to the shared-field models (account_move.py here).
  Logic belongs in the feature sub-module that owns it (e.g. fusion_accounting_bank_rec).
- NEVER rename the relation tables for shared M2Ms. They must match Enterprise verbatim
  for the dual-ownership pattern to work.
- Shared fields here have NO defaults beyond what Enterprise sets. The point is preservation.

## Cross-references
- Parent design: `fusion_accounting/docs/superpowers/specs/2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md` (Section 3)
- Workspace conventions: `/Users/gurpreet/Github/Odoo-Modules/CLAUDE.md`
  • Step 2: Write fusion_accounting_core/UPGRADE_NOTES.md

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/UPGRADE_NOTES.md

# UPGRADE_NOTES — fusion_accounting_core

## V19.0.1.0.0 (initial — Phase 0)

### Reference sources
- `RePackaged-Odoo/accounting/account_accountant/models/account_move.py` (Enterprise extension fields read for schema match)
- `RePackaged-Odoo/accounting/account_accountant/models/account_reconcile_model.py` (same)

### Mirror-zone files (none in _core — _core has no Mirror zone)

### Abstract-zone files (all of _core is abstract)
- `models/account_move.py`
- `models/account_reconcile_model.py`
- `models/ir_module_module.py`

### Intentional deltas from Odoo
- Shared-field declarations have NO compute methods, NO @api decorators beyond
  basic field types. Enterprise's account_move.py adds compute methods and
  business logic; we deliberately do not duplicate them. When Enterprise is
  installed, its compute methods run; when it's not, the fields are simply
  unused (until a fusion sub-module decides to own that behavior).
  • Step 3: Write fusion_accounting_core/README.md

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/README.md

# Fusion Accounting Core

Foundation module for the Fusion Accounting suite.

## What it does

- Defines three security groups: Fusion Accounting User / Manager / Administrator
- Auto-promotes Odoo `account.group_account_user` -> Fusion User and
  `account.group_account_manager` -> Fusion Admin
- Declares schema-preservation fields on `account.move` and `account.reconcile.model`
  so that Enterprise extension fields (deferred revenue links, signing user, etc.)
  survive an Enterprise uninstall
- Exposes the helper `env['ir.module.module']._fusion_is_enterprise_accounting_installed()`

## Install

This module never installs alone. Install `fusion_accounting` (the meta-module)
or any of the feature sub-modules — they all depend on `fusion_accounting_core`.

## Uninstall

Uninstalling `fusion_accounting_core` will remove the security groups and the
schema-preservation fields. If Enterprise is also installed, uninstalling
`fusion_accounting_core` will cause Odoo to consider the deferred / signing
fields owned only by Enterprise — which is the original Enterprise-only state
(no data loss, just back to Enterprise-controlled schema).

## Troubleshooting

If users are missing the "Fusion Accounting" privilege section in user settings
after install, the `implied_ids` mechanism only fires for newly-added users.
Backfill existing users via SQL:

```sql
INSERT INTO res_groups_users_rel (gid, uid)
SELECT g.res_id, gu.uid
FROM res_groups_users_rel gu
JOIN ir_model_data g ON g.module = 'fusion_accounting_core' AND g.name = 'group_fusion_accounting_user'
JOIN ir_model_data ag ON ag.module = 'account' AND ag.name = 'group_account_user' AND gu.gid = ag.res_id
ON CONFLICT DO NOTHING;

- [ ] **Step 4: Write `fusion_accounting_ai/CLAUDE.md`**

Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/CLAUDE.md`

(Use the most relevant content from the existing `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting/CLAUDE.md`. The AI-specific sections — adapter pattern, tool tiering, OWL gotchas, deployment — go here. Sections about meta-module organization go in `fusion_accounting/CLAUDE.md` instead. Quick edit guide:)

Read `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting/CLAUDE.md` and:
- Copy sections: Architecture, Key Design Decisions (AI Provider Integration, Tool Tiering, Tier 3 Approval Flow, Session Persistence, Rich Text Chat Output, Interactive Tables, HST Filing Workflow), Odoo 19 Gotchas, Server Details, Deployment Commands, Security Groups (note: groups themselves now in _core), Controller Endpoints, Models, AI Models Available, Theme / Styling Rules
- Add a new section at the top:

```markdown
# fusion_accounting_ai — Cursor / Claude Context

## Purpose
Conversational AI co-pilot for Odoo Accounting using Claude or GPT with native
tool-calling. Embeds in any Odoo install via the data-adapter pattern (works on
Community-only, Community + fusion native sub-modules, or Community + Enterprise).

## Sub-module relationships
- `fusion_accounting_core`: hard dep, provides security groups + Enterprise detection
- `fusion_accounting_bank_rec` (Phase 1): adapter routes to it when present
- `fusion_accounting_reports` (Phase 2): same
- `fusion_accounting_followup` (Phase 5): same
- Odoo Enterprise modules: detected at runtime, AI tools route through them via adapters

## Data-adapter pattern (Phase 0 addition)
- `services/data_adapters/base.py` — `DataAdapter` + `AdapterMode`
- `services/data_adapters/_registry.py` — `get_adapter(env, name)` + `register_adapter`
- One adapter file per domain: `bank_rec.py`, `reports.py`, `followup.py`, `assets.py`
- Each adapter implements `<method>_via_fusion`, `<method>_via_enterprise`, `<method>_via_community`
- Adapter `_select_mode()` picks fusion if model loaded, else enterprise if module installed, else community
  • Step 5: Write fusion_accounting_ai/UPGRADE_NOTES.md
# UPGRADE_NOTES — fusion_accounting_ai

## V19.0.1.0.0 (initial — Phase 0 split-out)

### Origin
Code originally lived in `fusion_accounting/` (the original AI module). Split out
into this sub-module during Phase 0 of the Enterprise Takeover Roadmap.

### Additions in this version
- `services/data_adapters/` — DataAdapter base + 4 adapters (bank_rec, reports, followup, assets)
- `services/tools/bank_reconciliation.py``get_unreconciled_bank_lines` refactored to use BankRecAdapter (pilot)
- `migrations/19.0.1.0.0/post-migration.py` — reassigns ir_model_data ownership from old module name
- Multi-company record rule on `fusion.accounting.session` (was missing pre-Phase-0 per CLAUDE.md Known Issues)

### Removed from manifest deps
- `account_accountant` (was hard dep)
- `account_reports` (was hard dep)
- `account_followup` (was hard dep)
- `mail` (now inherited via `fusion_accounting_core`)

Replaced with: `fusion_accounting_core` (Community-only). Runtime detection of
Enterprise modules via the data adapter pattern.
  • Step 6: Write fusion_accounting_ai/README.md
# Fusion Accounting AI

Conversational AI co-pilot for Odoo Accounting using Claude or GPT.

## What it does

Embeds an AI agent in the Odoo Accounting menu. Users chat with the AI, which
calls into Odoo via tool-functions (read journal entries, find unreconciled
bank lines, draft follow-ups, generate audit reports, etc.). Tier 3 actions
(financial writes) require user approval via in-chat approval cards.

## Install profiles

This module works on three install profiles:

1. **Pure Community + this module** — AI uses pure Community searches via the
   data-adapter `_via_community` paths. Reduced functionality (no rich reports,
   no Enterprise bank-rec features) but all read tools work.
2. **Community + this module + fusion native sub-modules** (recommended target) —
   adapters route to fusion bank rec / fusion reports / etc. Full functionality.
3. **Community + Enterprise + this module** (legacy) — adapters route to Enterprise
   APIs. Most functionality available; some Enterprise-specific UI integration
   (e.g. live cursor in bank-rec widget) not supported.

## Configuration

Settings -> Fusion Accounting AI -> set API keys for Claude (default) and/or OpenAI.

## Troubleshooting

See `CLAUDE.md` in this module for known Odoo 19 gotchas.
  • Step 7: Write fusion_accounting_migration/CLAUDE.md
# fusion_accounting_migration — Cursor / Claude Context

## Purpose
Transitional sub-module that helps clients move from Odoo Enterprise accounting
to Odoo Community + Fusion Accounting without losing data.

## What it does
- Safety guard: blocks uninstall of Enterprise accounting modules until the
  migration wizard has run (per-module flag in ir.config_parameter)
- Migration wizard: shell that other fusion sub-modules extend with per-feature
  migration logic (Phase 0 ships only the shell)

## Critical
- The safety guard overrides `button_immediate_uninstall` AND `module_uninstall`
  on `ir.module.module`. Both paths must be guarded — UI uninstall, CLI uninstall,
  and API uninstall all go through one or the other.
- Each fusion feature sub-module that replaces an Enterprise feature MUST extend
  the migration wizard's `action_run_migration` to add its own migration step
  AND set the corresponding `fusion_accounting.migration.<module>.completed`
  flag to True after running.
  • Step 8: Write fusion_accounting_migration/UPGRADE_NOTES.md
# UPGRADE_NOTES — fusion_accounting_migration

## V19.0.1.0.0 (initial — Phase 0)

Skeleton: safety guard + wizard shell. No per-feature migration logic registered yet.

Added by future phases:
- Phase 1: bank-rec migration (verifies account.partial.reconcile rows are intact; sets `account_accountant.completed` flag)
- Phase 5: account_followup migration
- Phase 6: account_asset, account_budget migration
  • Step 9: Write fusion_accounting_migration/README.md
# Fusion Accounting Migration

Transitional helper for moving clients from Odoo Enterprise to Community + Fusion.

## When to use

Install this module ONCE per client during the Enterprise->Fusion switchover.
After the switchover is complete and the client is comfortable on Community,
this module can be uninstalled.

## How it works

1. Install fusion_accounting (the meta-module) — pulls in this module
2. Open Settings -> Fusion Accounting -> Migrate from Enterprise
3. Wizard shows which Enterprise modules are detected and what migrations are available
4. Run migration; wizard reports counts and warnings
5. Uninstall Enterprise modules in dep-safe order (the safety guard prevents premature uninstall)

## Override the safety guard

If you need to uninstall an Enterprise module WITHOUT migrating (data will be lost),
set `fusion_accounting.migration.<module>.completed` to True in System Parameters.
  • Step 10: Rewrite fusion_accounting/CLAUDE.md as meta-module overview

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/CLAUDE.md

(Replace the entire file contents with:)

# fusion_accounting (meta-module) — Cursor / Claude Context

## Purpose

Meta-module that installs the entire Fusion Accounting sub-module suite with
one click. Owns no Python, JS, XML data, or views of its own. Just a manifest
that depends on the sub-modules.

## Sub-modules (current)

| Sub-module | Phase | Purpose |
|---|---|---|
| `fusion_accounting_core` | 0 | Security groups, shared schema, Enterprise detection helper |
| `fusion_accounting_ai` | 0 | AI Co-Pilot (Claude/GPT) — was the original `fusion_accounting` code |
| `fusion_accounting_migration` | 0 | Transitional Enterprise->Fusion data migration |

## Sub-modules (planned)

Per the roadmap design at `docs/superpowers/specs/2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md`:

| Sub-module | Phase | Purpose |
|---|---|---|
| `fusion_accounting_bank_rec` | 1 | Native bank reconciliation (replaces account_accountant bank rec) |
| `fusion_accounting_reports` | 2 | Native financial reports engine (replaces account_reports) |
| `fusion_accounting_dashboard` | 3 | Journal kanban + digest |
| `fusion_accounting_followup` | 5 | Customer payment follow-ups |
| `fusion_accounting_assets` | 6 | Asset register + depreciation |
| `fusion_accounting_budget` | 6 | Budget vs actual |

## Roadmap and plans

- Roadmap design: `docs/superpowers/specs/2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md`
- Phase 0 plan: `docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md`
- Empirical uninstall test results: `docs/superpowers/specs/2026-04-18-empirical-uninstall-test-results.md`

## Tooling

- `tools/check_odoo_diff.sh` — annual upgrade ritual: diff Enterprise source between Odoo versions

## Per-sub-module CLAUDE.md

Each sub-module has its own `CLAUDE.md` with feature-specific context. Read them when working on that sub-module.

## Workspace-wide conventions

`/Users/gurpreet/Github/Odoo-Modules/CLAUDE.md` — common Odoo 19 rules (search views, OWL components, SCSS, asset bundle cache busting, dark mode, etc.). Apply to every sub-module.
  • Step 11: Write fusion_accounting/README.md
# Fusion Accounting (meta-module)

One-click install of the entire Fusion Accounting suite for Odoo 19.

## What it installs

- AI Co-Pilot for accounting (Claude / GPT)
- Native foundation (security, schema preservation)
- Transitional Enterprise -> Fusion migration helper

As later sub-modules ship (bank rec, reports, follow-ups, assets, budgets),
they're added to the meta-module's `depends` and installed automatically when
the client upgrades fusion_accounting.

## Install

docker exec odoo-dev-app odoo -d -i fusion_accounting --stop-after-init


## Uninstall

Uninstalling the meta-module does NOT uninstall its sub-modules (Odoo
behavior). To fully remove Fusion Accounting:

docker exec odoo-dev-app odoo-shell -d --no-http <<EOF env['ir.module.module'].search([ ('name', 'in', [ 'fusion_accounting', 'fusion_accounting_ai', 'fusion_accounting_migration', 'fusion_accounting_core', ]), ('state', '=', 'installed'), ]).button_immediate_uninstall() EOF


## Documentation

See `docs/superpowers/specs/` for the design and `docs/superpowers/plans/` for implementation plans.
  • Step 12: Commit all docs
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting_core/CLAUDE.md fusion_accounting_core/UPGRADE_NOTES.md fusion_accounting_core/README.md
git add fusion_accounting_ai/CLAUDE.md fusion_accounting_ai/UPGRADE_NOTES.md fusion_accounting_ai/README.md
git add fusion_accounting_migration/CLAUDE.md fusion_accounting_migration/UPGRADE_NOTES.md fusion_accounting_migration/README.md
git add fusion_accounting/CLAUDE.md fusion_accounting/README.md
git commit -m "docs(fusion_accounting): per-sub-module CLAUDE.md, UPGRADE_NOTES.md, README.md"

Task 21: CI Pipeline (Gitea or GitHub Actions)

Files:

  • Create: /Users/gurpreet/Github/Odoo-Modules/.gitea/workflows/fusion_accounting_ci.yml (if Gitea) OR /Users/gurpreet/Github/Odoo-Modules/.github/workflows/fusion_accounting_ci.yml (if GitHub)

  • Step 1: Determine which forge is in use

Run:

cd /Users/gurpreet/Github/Odoo-Modules && git remote -v

If the remote contains gitea or a self-hosted gitea URL, use .gitea/workflows/. If it's github.com, use .github/workflows/. If neither/both, ask the user.

  • Step 2: Create the workflow directory
# Adjust path based on Step 1 finding
mkdir -p /Users/gurpreet/Github/Odoo-Modules/.gitea/workflows
# OR
mkdir -p /Users/gurpreet/Github/Odoo-Modules/.github/workflows
  • Step 3: Write the workflow file

Path (Gitea example): /Users/gurpreet/Github/Odoo-Modules/.gitea/workflows/fusion_accounting_ci.yml

name: fusion_accounting CI

on:
  push:
    paths:
      - 'fusion_accounting/**'
      - 'fusion_accounting_core/**'
      - 'fusion_accounting_ai/**'
      - 'fusion_accounting_migration/**'
  pull_request:
    paths:
      - 'fusion_accounting/**'
      - 'fusion_accounting_core/**'
      - 'fusion_accounting_ai/**'
      - 'fusion_accounting_migration/**'

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: odoo
          POSTGRES_PASSWORD: odoo
          POSTGRES_DB: postgres
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 10s

    strategy:
      fail-fast: false
      matrix:
        sub_module:
          - fusion_accounting_core
          - fusion_accounting_ai
          - fusion_accounting_migration

    steps:
      - uses: actions/checkout@v3

      - name: Set up Python 3.11
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install Odoo 19 + dependencies
        run: |
          pip install --break-system-packages anthropic openai
          # Install Odoo from source or pin a Docker image; adjust to local convention.
          # Placeholder: this CI step needs to be aligned with how Nexa builds Odoo locally.

      - name: Install fusion sub-modules
        run: |
          # Copy modules to addons path
          mkdir -p /tmp/addons
          cp -r fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration /tmp/addons/

      - name: Initialize Odoo DB and install module
        run: |
          createdb -h localhost -U odoo fusion_test
          odoo -d fusion_test --addons-path=/tmp/addons -i ${{ matrix.sub_module }} --stop-after-init --without-demo=all

      - name: Run tests
        run: |
          odoo -d fusion_test --addons-path=/tmp/addons --test-tags='post_install' --stop-after-init -u ${{ matrix.sub_module }}

(Note: the Odoo 19 installation step is a placeholder. The actual install mechanism depends on how Nexa builds Odoo locally — Docker image vs. source checkout vs. pip-installed package. Adjust based on the existing docker exec odoo-dev-app odoo pattern in CLAUDE.md.)

  • Step 4: Skip CI if not viable in this environment

If setting up Odoo on a fresh runner is too involved for this Phase 0 (it often requires Docker-in-Docker or a custom self-hosted runner), document the deferral:

Path: /Users/gurpreet/Github/Odoo-Modules/fusion_accounting/docs/superpowers/specs/2026-04-18-ci-deferred.md

# CI Deferred to Phase 1

The Phase 0 plan included setting up automated CI for fusion sub-modules.
Setting up Odoo 19 on a clean CI runner requires:
- Docker-in-Docker support OR a self-hosted runner with Odoo pre-installed
- A PostgreSQL service container
- Pinned Python dep set (anthropic, openai, plus Odoo's own deps)

This is non-trivial setup that benefits from being aligned with how Nexa
already runs Odoo locally (the `odoo-westin` Docker pattern). To avoid
half-doing this, CI setup is deferred to Phase 1, where it can be designed
together with the empirical-test infrastructure.

For Phase 0, tests are run manually by the engineer using:

    ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 \
      --test-tags post_install --stop-after-init \
      -c /etc/odoo/odoo.conf -u <sub_module>"
  • Step 5: Commit either the CI yaml or the deferral note
cd /Users/gurpreet/Github/Odoo-Modules
# If yaml created:
git add .gitea/workflows/fusion_accounting_ci.yml || git add .github/workflows/fusion_accounting_ci.yml
git commit -m "ci(fusion_accounting): add CI pipeline for sub-modules"

# OR if deferred:
git add fusion_accounting/docs/superpowers/specs/2026-04-18-ci-deferred.md
git commit -m "docs(fusion_accounting): defer CI setup to Phase 1"

Task 22: End-to-End Smoke Test

Verify the entire Phase 0 stack works as a unit.

  • Step 1: Clean redeploy
cd /Users/gurpreet/Github/Odoo-Modules
ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/\$m; done"
scp -r fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration odoo-westin:/tmp/
ssh odoo-westin "for m in fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration; do docker cp /tmp/\$m odoo-dev-app:/mnt/extra-addons/\$m; done && rm -rf /tmp/fusion_accounting*"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting --stop-after-init -c /etc/odoo/odoo.conf 2>&1 | tail -20"
ssh odoo-westin "docker restart odoo-dev-app"

Expected: clean upgrade, no errors.

  • Step 2: Run full Phase 0 test suite across all sub-modules
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 \
  --test-tags post_install --stop-after-init -c /etc/odoo/odoo.conf \
  -u fusion_accounting_core,fusion_accounting_ai,fusion_accounting_migration \
  2>&1 | grep -E 'FAIL|ERROR|tests passed|TestSharedField|TestEnterpriseDetection|TestPostMigration|TestDataAdapter|TestBankRecAdapter|TestReportsAdapter|TestFollowupAdapter|TestAssetsAdapter|TestSafetyGuard'"

Expected: all tests PASS, no FAIL/ERROR lines.

  • Step 3: Browser smoke test (manual)

Open the AI chat panel, run these queries:

  1. "Show me unreconciled bank lines for [Test Bank]" — expect AI to return data via the new BankRecAdapter
  2. "Give me the trial balance" — expect AI to return data via the new ReportsAdapter
  3. Open the Fusion Accounting menu — expect health cards and chat panel to render
  4. Settings -> Fusion Accounting -> Migrate from Enterprise — expect the migration wizard to open without error
  5. Apps -> Invoicing (account_accountant) -> Uninstall — expect the safety guard error message
  • Step 4: Verify backward-compatibility — existing AI features still work

For each Tier 1/2/3 tool category in services/tools/, run one representative AI query and confirm it works as before. Spot-check:

  • Bank rec: "find duplicate bills"

  • AR: "show overdue invoices"

  • AP: "find unpaid vendor bills"

  • Reporting: "give me the P&L for this month"

  • Step 5: Verify session ownership + multi-company

If the test instance has multiple companies, log in as a User in company A, verify they cannot see another User's session in company B.

  • Step 6: Document the smoke-test results

Append a short note to fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md (this file) at the bottom:

## Phase 0 Smoke Test Results — <date>

- All test suites passed (X tests, 0 failures)
- AI chat panel responsive
- Bank rec data adapter returning correct shape
- Reports data adapter returning correct shape
- Migration wizard opens
- Safety guard fires on Enterprise uninstall attempt
- Multi-company record rule on session enforced
- All sub-modules show as installed in Apps page

Phase 0 complete. Ready for Phase 1 brainstorming.

(This is the only edit to the plan file itself, made at the end as a completion marker.)

  • Step 7: Tag the Phase 0 completion
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md
git commit -m "docs(fusion_accounting): record Phase 0 smoke test results"
git tag -a fusion_accounting/phase-0-complete -m "Phase 0 Foundation complete; ready for Phase 1 brainstorming"
git tag --list "fusion_accounting/*"

Expected: both tags listed (fusion_accounting/pre-phase-0 and fusion_accounting/phase-0-complete).


Phase 0 Acceptance Criteria

  • All four modules (fusion_accounting, fusion_accounting_core, fusion_accounting_ai, fusion_accounting_migration) install cleanly in dep order
  • All Phase 0 tests pass (TestSharedFieldOwnership, TestEnterpriseDetection, TestPostMigration, TestDataAdapter*, TestSafetyGuard)
  • Existing AI copilot continues to work end-to-end via the chat panel
  • fusion_accounting_ai/__manifest__.py has zero hard deps on Enterprise modules
  • Bank-rec AI tool routes through BankRecAdapter (pilot)
  • fusion_accounting_core has no business logic — only security, schema preservation, runtime helpers
  • fusion_accounting_migration safety guard blocks Enterprise uninstall attempts
  • Empirical uninstall test results documented and any gaps fed back into the spec
  • Per-sub-module CLAUDE.md, UPGRADE_NOTES.md, README.md present
  • tools/check_odoo_diff.sh executable and tested
  • Multi-company record rule on fusion.accounting.session exists (was a Known Issue pre-Phase-0)
  • Git tag fusion_accounting/phase-0-complete placed on the merge commit

What Comes After Phase 0

Phase 1 — Bank Reconciliation. Brainstorm in a new session, produce its own design doc and implementation plan. The Phase 0 BankRecAdapter _via_fusion path becomes meaningful when Phase 1 ships fusion.bank.rec.widget.


Phase 0 Smoke Test Results — 2026-04-18

Host: odoo-westin (container odoo-dev-app, DB westin-v19, Odoo 19, Enterprise installed alongside).

Deploy

  • Clean redeploy: removed and re-copied all four modules (fusion_accounting, fusion_accounting_core, fusion_accounting_ai, fusion_accounting_migration) into /mnt/extra-addons/ on the container.
  • Meta-module upgrade (odoo -u fusion_accounting --stop-after-init --no-http): exit 0, all four modules installed in ir_module_module. Only pre-existing unrelated warnings (studio, fusion_claims label collisions, docutils, _sql_constraints deprecations on third-party modules).

Test suite results

  • Command: odoo --test-tags post_install --stop-after-init --no-http -u fusion_accounting_core,fusion_accounting_ai,fusion_accounting_migration
  • Exit code: 0
  • Per-test Starting … lines observed (odoo.tests INFO handler): 23 tests
    • fusion_accounting_core — 7 tests: TestEnterpriseDetection ×2, TestSharedFieldOwnership ×5
    • fusion_accounting_ai — 14 tests: TestDataAdapterBase ×2, TestBankRecAdapter ×1, TestReportsAdapter ×4, TestFollowupAdapter ×4, TestAssetsAdapter ×1, TestPostMigration ×2
    • fusion_accounting_migration — 2 tests: TestSafetyGuard ×2
  • Result: 23 PASS, 0 FAIL, 0 ERROR, 0 SKIP
  • No AssertionError / Traceback / FAILED lines in the log.
  • Odoo's odoo.tests.stats reports slightly higher per-module counts (ai: 26, core: 11, migration: 4) because Odoo also counts its own implicit per-module sanity checks (XML validation, etc.) beyond our explicit TestCase methods; all non-explicit tests also passed since exit code is 0 and no failure lines appear.

Verification spot-checks

  • Migration wizard menu (6a): present — ir_ui_menu contains both Fusion Accounting (id 2802, root) and Migrate from Enterprise (id 2803, child of 2802). Ten total fusion menus registered across fusion_accounting_ai (8) and fusion_accounting_migration (2).
  • AI module actions (6b): 8 actions registered under module='fusion_accounting_ai'action_fusion_session, action_fusion_history, action_fusion_rule, action_fusion_dashboard, action_vendor_tax_profiles, action_recurring_patterns, action_fusion_rule_wizard, action_report_fusion_audit.
  • Security groups (6c): three groups present in fusion_accounting_coreAdministrator, Manager, User, each with 0 users (expected for a fresh install with no user assignments yet).
  • Shared-field columns on account_move (6d):
    • signing_user (integer, FK to res_users) — physically present, owned by fusion_accounting_core
    • payment_state_before_switch (character varying) — physically present, owned by fusion_accounting_core
    • deferred_move_ids / deferred_original_move_ids — both present via m2m relation table account_move_deferred_rel with columns original_move_id / deferred_move_id (matches Enterprise's table name; test test_deferred_relation_table_name_matches_enterprise passes) ✓
    • deferred_entry_type — exists in the ORM (ir_model_fields.store='f') but no local column, because Enterprise's account_asset (installed on this DB: account_accountant, account_asset, account_reports all installed) currently owns the physical storage. This is the intended dual-ownership design from Task 17 — fusion_accounting_core declares a stub so the field survives Enterprise uninstall; the TestSharedFieldOwnership.test_account_move_deferred_fields_exist test passed and confirmed the field is in Move._fields.

Deferred

  • Task 18 (empirical Enterprise-uninstall verification test): deferred pending environment provisioning decision. Requires a dedicated scratch DB where we can actually uninstall Enterprise without disturbing the productive westin-v19 tenant. Tracked in fusion_accounting/docs/superpowers/plans/2026-04-18-ci-deferred.md (or equivalent follow-up note). The shared-field design is validated in principle by Tasks 17+21 and the TestSharedFieldOwnership suite; Task 18 adds the "actually uninstall, confirm nothing collapses" live check.

Phase 0 Status: COMPLETE (pending Task 18 empirical test)

Ready to proceed to Phase 1 (Bank Reconciliation) — brainstorming session + its own design doc + implementation plan.