From 76c898aadf7a8bbaedd0b0ca0e1c4dcb0303681e Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 18 Apr 2026 21:13:07 -0400 Subject: [PATCH] docs(fusion_accounting): Phase 0 foundation implementation plan Detailed task-by-task plan for executing Phase 0 of the Enterprise Takeover Roadmap. 22 tasks covering: - Sub-module skeletons (_core, _ai, _migration) and meta-module conversion - Move all current AI module code into fusion_accounting_ai with git mv - ir_model_data ownership reassignment via post-migration script - Data adapter pattern (base + bank_rec + reports + followup + assets adapters) - Refactor of every AI tool to route through adapters (pilot in bank_rec, then survey + per-file) - Strip all hard Enterprise dependencies from manifests - Enterprise-detection helper and shared-field-ownership models in _core - Multi-company record rule on fusion.accounting.session (was a Known Issue) - Migration safety guard that blocks Enterprise uninstall until wizard runs - Migration wizard skeleton (per-feature migrations added by future phases) - tools/check_odoo_diff.sh for the annual upgrade ritual - Per-sub-module CLAUDE.md, UPGRADE_NOTES.md, README.md - CI pipeline (or deferral note if not yet viable) - Empirical Enterprise-uninstall verification test on a throwaway instance - End-to-end smoke test + completion tag Each task uses TDD where applicable (test fails, implement, test passes, commit) and concrete validation commands where TDD doesn't fit (file moves, config changes, manual smoke tests). Made-with: Cursor --- .../2026-04-18-phase-0-foundation-plan.md | 3736 +++++++++++++++++ 1 file changed, 3736 insertions(+) create mode 100644 fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md diff --git a/fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md b/fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md new file mode 100644 index 00000000..74526a77 --- /dev/null +++ b/fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md @@ -0,0 +1,3736 @@ +# 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`](../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: `(): ` (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.0` → `19.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: +```bash +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: +```bash +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: +```bash +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` + +```python +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` + +```python +# 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` + +```python +# Tests populated in Tasks 8-12 +``` + +- [ ] **Step 5: Write `__manifest__.py`** + +Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/__manifest__.py` + +```python +{ + '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` + +```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: +```bash +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** + +```bash +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: +```bash +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` + +```python +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` + +```python +{ + '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` + +```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): + +```bash +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** + +```bash +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** + +```bash +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` + +```python +from . import models +from . import wizards +``` + +- [ ] **Step 3: Write `__manifest__.py`** + +Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/__manifest__.py` + +```python +{ + '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** + +```bash +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` + +```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 + + + + +``` + +- [ ] **Step 7: Verify install** + +Same deploy snippet pattern as Task 2 Step 7, for `fusion_accounting_migration`. Expected: installs cleanly. + +- [ ] **Step 8: Commit** + +```bash +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** + +```bash +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` + +```python +from . import chat_controller +``` + +- [ ] **Step 2: Move data files** + +```bash +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** + +```bash +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** + +```bash +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** + +```bash +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)** + +```bash +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** + +```bash +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** + +```bash +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** + +```bash +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** + +```bash +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: +```bash +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: + +```python + '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: +```bash +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** + +```bash +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` + +```python +# 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` + +```python +{ + '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: +```bash +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** + +```bash +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` + +```python +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: +```bash +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` + +```python +"""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: +```bash +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** + +```bash +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** + +```bash +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** + +```bash +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` + +```python +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** + +```bash +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` + +```python +"""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 _via_ 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` + +```python +"""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` + +```python +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** + +```bash +# 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** + +```bash +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`: + +```python +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'`** + +```bash +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` + +```python +"""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: + +```python +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** + +```bash +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** + +```bash +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`: + +```python +@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` + +```python +"""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`: + +```python +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** + +```bash +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`: + +```python +@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` + +```python +"""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` + +```python +"""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`: + +```python +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** + +```bash +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: + +```python + '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: + +```bash +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** + +```bash +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): + +```python +from ..data_adapters import get_adapter +``` + +Then locate `get_unreconciled_bank_lines` and refactor: + +**Before (typical pattern, may differ — adapt to actual code):** +```python +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:** +```python +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** + +```bash +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** + +```bash +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: +```bash +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: +```python +def get_overdue_invoices(env, partner_id=None, days=30): + domain = [...] + moves = env['account.move'].search(domain) + return [{...} for m in moves] +``` + +After: +```python +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: +```bash +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: +```bash +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** + +```bash +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)** + +```bash +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` + +```python +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** + +```bash +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` + +```python +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` + +```python +from . import ir_module_module +``` + +- [ ] **Step 5: Run tests, verify pass** + +```bash +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** + +```bash +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` + +```python +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` + +```python +"""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` + +```python +"""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` + +```python +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** + +```bash +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** + +```bash +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 `` etc., change to: + +```xml + +``` + +(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: + +```bash +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 + + + + + Fusion Accounting + 25 + + + + + Fusion Accounting + + + + + + User + 10 + + + + + + + Manager + 20 + + + + + + + Administrator + 30 + + + + + + + + + + + + +``` + +- [ ] **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): + +```bash +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 + + + + + Fusion Session: Own Sessions + + [('user_id', '=', user.id)] + + + + + Fusion Session: All Sessions + + [(1, '=', 1)] + + + + + Fusion History: Own History + + [('session_id.user_id', '=', user.id)] + + + + + Fusion History: All History + + [(1, '=', 1)] + + + + + + Fusion Tool: Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + Fusion Rule: Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + Fusion History: Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + + Fusion Session: Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + +``` + +(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: +```bash +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): + +```python + 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: + +```python + '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'`: + +```python + '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): + +```python +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` + +```bash +mkdir -p /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_core/migrations/19.0.1.0.0 +``` + +```python +"""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** + +```bash +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** + +```bash +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` + +```python +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** + +```bash +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` + +```python +"""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..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` + +```python +from . import ir_module_module +``` + +- [ ] **Step 5: Write the wizard skeleton** + +Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/migration_wizard.py` + +```python +"""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..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` + +```python +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 + + + + fusion.migration.wizard.form + fusion.migration.wizard + +
+ + + + + + +
+
+
+
+
+ + + Migrate from Enterprise + fusion.migration.wizard + form + new + +
+``` + +- [ ] **Step 8: Add ACL row for the wizard** + +Edit `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/security/ir.model.access.csv`: + +```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** + +```bash +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** + +```bash +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** + +```bash +# 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** + +```bash +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** + +```bash +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** + +```bash +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` + +```bash +#!/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 [] +# +# 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 []}" +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** + +```bash +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` + +```markdown +# 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 [] +``` + +### Example + +```bash +# 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-/` (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: + +```bash +# 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** + +```bash +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` + +```markdown +# 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` + +```markdown +# 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` + +```markdown +# 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 `_via_fusion`, `_via_enterprise`, `_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`** + +```markdown +# 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`** + +```markdown +# 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`** + +```markdown +# 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..completed` + flag to True after running. +``` + +- [ ] **Step 8: Write `fusion_accounting_migration/UPGRADE_NOTES.md`** + +```markdown +# 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`** + +```markdown +# 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..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:) + +```markdown +# 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`** + +```markdown +# 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 <" +``` + +- [ ] **Step 5: Commit either the CI yaml or the deferral note** + +```bash +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** + +```bash +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** + +```bash +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: + +```markdown +## Phase 0 Smoke Test Results — + +- 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** + +```bash +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`.