3775 lines
155 KiB
Markdown
3775 lines
155 KiB
Markdown
# 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: `<type>(<scope>): <description>` (e.g. `refactor(fusion_accounting): split into core/ai/migration sub-modules`).
|
||
- **Manifest version bumps**: every meaningful change to a sub-module bumps the patch version (e.g. `19.0.1.0.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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<!-- Migration wizard view stub. Populated in Task 16. -->
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **Step 7: Verify install**
|
||
|
||
Same deploy snippet pattern as Task 2 Step 7, for `fusion_accounting_migration`. Expected: installs cleanly.
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```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 <method_name>_via_<mode> on self and call it.
|
||
|
||
E.g. method_name='list_unreconciled', mode=FUSION calls
|
||
self.list_unreconciled_via_fusion(*args, **kwargs).
|
||
"""
|
||
mode = self._select_mode()
|
||
attr = f"{method_name}_via_{mode.value}"
|
||
impl = getattr(self, attr, None)
|
||
if impl is None:
|
||
_logger.warning(
|
||
"DataAdapter %s has no implementation for %s in mode %s; "
|
||
"returning empty result",
|
||
type(self).__name__, method_name, mode.value,
|
||
)
|
||
return []
|
||
return impl(*args, **kwargs)
|
||
```
|
||
|
||
- [ ] **Step 4: Write the registry**
|
||
|
||
Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_ai/services/data_adapters/_registry.py`
|
||
|
||
```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 `<field name="model_id" ref="model_fusion_accounting_session"/>` etc., change to:
|
||
|
||
```xml
|
||
<field name="model_id" ref="fusion_accounting_ai.model_fusion_accounting_session"/>
|
||
```
|
||
|
||
(Apply the same change to `model_fusion_accounting_match_history`, `model_fusion_accounting_tool`, `model_fusion_accounting_rule`.)
|
||
|
||
This requires `fusion_accounting_core` to declare `fusion_accounting_ai` as a dependency for cross-module xml-id references in data files... BUT that creates a circular dep (`_ai` depends on `_core` already).
|
||
|
||
**Resolution:** instead of putting the record rules in `_core`, put them in `_ai` (where the models live). Only the GROUPS go in `_core`.
|
||
|
||
Revise the move:
|
||
|
||
```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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<!-- Module Category -->
|
||
<record id="module_category_fusion_accounting" model="ir.module.category">
|
||
<field name="name">Fusion Accounting</field>
|
||
<field name="sequence">25</field>
|
||
</record>
|
||
|
||
<!-- Groups Privilege -->
|
||
<record id="res_groups_privilege_fusion_accounting" model="res.groups.privilege">
|
||
<field name="name">Fusion Accounting</field>
|
||
<field name="category_id" ref="module_category_fusion_accounting"/>
|
||
</record>
|
||
|
||
<!-- User Group (Staff) -->
|
||
<record id="group_fusion_accounting_user" model="res.groups">
|
||
<field name="name">User</field>
|
||
<field name="sequence">10</field>
|
||
<field name="implied_ids" eval="[(4, ref('account.group_account_user'))]"/>
|
||
<field name="privilege_id" ref="res_groups_privilege_fusion_accounting"/>
|
||
</record>
|
||
|
||
<!-- Manager Group -->
|
||
<record id="group_fusion_accounting_manager" model="res.groups">
|
||
<field name="name">Manager</field>
|
||
<field name="sequence">20</field>
|
||
<field name="implied_ids" eval="[(4, ref('group_fusion_accounting_user'))]"/>
|
||
<field name="privilege_id" ref="res_groups_privilege_fusion_accounting"/>
|
||
</record>
|
||
|
||
<!-- Admin Group -->
|
||
<record id="group_fusion_accounting_admin" model="res.groups">
|
||
<field name="name">Administrator</field>
|
||
<field name="sequence">30</field>
|
||
<field name="implied_ids" eval="[(4, ref('group_fusion_accounting_manager'))]"/>
|
||
<field name="privilege_id" ref="res_groups_privilege_fusion_accounting"/>
|
||
</record>
|
||
|
||
<!-- Auto-assign: Accounting users get Fusion User; Advisers get Admin -->
|
||
<record id="account.group_account_user" model="res.groups">
|
||
<field name="implied_ids" eval="[(4, ref('group_fusion_accounting_user'))]"/>
|
||
</record>
|
||
<record id="account.group_account_manager" model="res.groups">
|
||
<field name="implied_ids" eval="[(4, ref('group_fusion_accounting_admin'))]"/>
|
||
</record>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **Step 4: Replace `_ai/security` files**
|
||
|
||
Move the old security.xml from `fusion_accounting/` to `fusion_accounting_ai/` and edit it down to just the record rules (groups removed):
|
||
|
||
```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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<!-- Per-user record rules (sessions visible only to the owning user; managers see all) -->
|
||
<record id="rule_fusion_session_user" model="ir.rule">
|
||
<field name="name">Fusion Session: Own Sessions</field>
|
||
<field name="model_id" ref="model_fusion_accounting_session"/>
|
||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||
<field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_user'))]"/>
|
||
</record>
|
||
|
||
<record id="rule_fusion_session_manager" model="ir.rule">
|
||
<field name="name">Fusion Session: All Sessions</field>
|
||
<field name="model_id" ref="model_fusion_accounting_session"/>
|
||
<field name="domain_force">[(1, '=', 1)]</field>
|
||
<field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_manager'))]"/>
|
||
</record>
|
||
|
||
<record id="rule_fusion_history_user" model="ir.rule">
|
||
<field name="name">Fusion History: Own History</field>
|
||
<field name="model_id" ref="model_fusion_accounting_match_history"/>
|
||
<field name="domain_force">[('session_id.user_id', '=', user.id)]</field>
|
||
<field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_user'))]"/>
|
||
</record>
|
||
|
||
<record id="rule_fusion_history_manager" model="ir.rule">
|
||
<field name="name">Fusion History: All History</field>
|
||
<field name="model_id" ref="model_fusion_accounting_match_history"/>
|
||
<field name="domain_force">[(1, '=', 1)]</field>
|
||
<field name="groups" eval="[(4, ref('fusion_accounting_core.group_fusion_accounting_manager'))]"/>
|
||
</record>
|
||
|
||
<!-- Multi-company rules -->
|
||
<record id="rule_fusion_tool_company" model="ir.rule">
|
||
<field name="name">Fusion Tool: Multi-Company</field>
|
||
<field name="model_id" ref="model_fusion_accounting_tool"/>
|
||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||
</record>
|
||
|
||
<record id="rule_fusion_rule_company" model="ir.rule">
|
||
<field name="name">Fusion Rule: Multi-Company</field>
|
||
<field name="model_id" ref="model_fusion_accounting_rule"/>
|
||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||
</record>
|
||
|
||
<record id="rule_fusion_history_company" model="ir.rule">
|
||
<field name="name">Fusion History: Multi-Company</field>
|
||
<field name="model_id" ref="model_fusion_accounting_match_history"/>
|
||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||
</record>
|
||
|
||
<!-- NEW (Phase 0): Multi-company rule on session itself
|
||
(per spec Section 4.2 + existing CLAUDE.md Known Issues) -->
|
||
<record id="rule_fusion_session_company" model="ir.rule">
|
||
<field name="name">Fusion Session: Multi-Company</field>
|
||
<field name="model_id" ref="model_fusion_accounting_session"/>
|
||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||
</record>
|
||
</odoo>
|
||
```
|
||
|
||
(Note: this includes the missing multi-company record rule on `fusion.accounting.session` per the spec.)
|
||
|
||
The rule above references a `company_id` field on `fusion.accounting.session`. Verify the model has that field — if not, also add it to the model.
|
||
|
||
- [ ] **Step 5: Verify `company_id` exists on `fusion.accounting.session`**
|
||
|
||
Run:
|
||
```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.<module_name>.completed
|
||
|
||
If the flag is False/unset and the module is currently installed, the guard
|
||
raises UserError pointing the user to Settings -> Fusion Accounting ->
|
||
Migrate from Enterprise.
|
||
|
||
The migration wizard sets the flag to True after a successful migration run
|
||
for that module.
|
||
"""
|
||
|
||
from odoo import _, api, models
|
||
from odoo.exceptions import UserError
|
||
|
||
|
||
GUARDED_MODULES = (
|
||
'account_accountant',
|
||
'account_reports',
|
||
'accountant',
|
||
'account_followup',
|
||
'account_asset',
|
||
'account_budget',
|
||
'account_loans',
|
||
)
|
||
|
||
|
||
class IrModuleModule(models.Model):
|
||
_inherit = "ir.module.module"
|
||
|
||
@api.model
|
||
def _fusion_check_uninstall_guard(self, module_names):
|
||
"""Verify it's safe to uninstall the given modules.
|
||
|
||
Returns True if all checks pass; raises UserError otherwise.
|
||
"""
|
||
Param = self.env['ir.config_parameter'].sudo()
|
||
for name in module_names:
|
||
if name not in GUARDED_MODULES:
|
||
continue
|
||
installed = self.sudo().search_count([
|
||
('name', '=', name), ('state', '=', 'installed'),
|
||
])
|
||
if not installed:
|
||
continue
|
||
flag_key = f'fusion_accounting.migration.{name}.completed'
|
||
if Param.get_param(flag_key, default='False').lower() != 'true':
|
||
raise UserError(_(
|
||
"Cannot uninstall %s: the Fusion Accounting migration "
|
||
"for this module has not run yet. Please open\n"
|
||
" Settings -> Fusion Accounting -> Migrate from Enterprise\n"
|
||
"and run the migration before uninstalling. Once the "
|
||
"migration has completed, the safety guard will allow "
|
||
"uninstall.\n\n"
|
||
"If you genuinely want to uninstall WITHOUT migrating "
|
||
"(data will be lost), set the parameter %s to True manually.",
|
||
name, flag_key,
|
||
))
|
||
return True
|
||
|
||
def button_immediate_uninstall(self):
|
||
"""Override to invoke the safety guard before allowing uninstall."""
|
||
self._fusion_check_uninstall_guard(self.mapped('name'))
|
||
return super().button_immediate_uninstall()
|
||
|
||
def module_uninstall(self):
|
||
"""Override the lower-level uninstall path too (CLI / API uninstall)."""
|
||
self._fusion_check_uninstall_guard(self.mapped('name'))
|
||
return super().module_uninstall()
|
||
```
|
||
|
||
- [ ] **Step 4: Update `models/__init__.py`**
|
||
|
||
Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/models/__init__.py`
|
||
|
||
```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.<name>.completed
|
||
config param to True.
|
||
"""
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'type': 'info',
|
||
'title': _("Nothing to migrate (yet)"),
|
||
'message': _(
|
||
"Phase 0 ships the migration framework but no per-feature "
|
||
"migrations are registered yet. Each fusion sub-module that "
|
||
"replaces an Enterprise feature (Phase 1+) will register its "
|
||
"own migration step here."
|
||
),
|
||
},
|
||
}
|
||
```
|
||
|
||
|
||
- [ ] **Step 6: Update `wizards/__init__.py`**
|
||
|
||
Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/wizards/__init__.py`
|
||
|
||
```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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
<record id="view_fusion_migration_wizard_form" model="ir.ui.view">
|
||
<field name="name">fusion.migration.wizard.form</field>
|
||
<field name="model">fusion.migration.wizard</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Migrate from Enterprise">
|
||
<sheet>
|
||
<group>
|
||
<field name="enterprise_modules_detected" readonly="1"/>
|
||
<field name="notes" readonly="1"/>
|
||
</group>
|
||
</sheet>
|
||
<footer>
|
||
<button name="action_run_migration" type="object" string="Run Migration" class="btn-primary"/>
|
||
<button special="cancel" string="Close" class="btn-secondary"/>
|
||
</footer>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="action_fusion_migration_wizard" model="ir.actions.act_window">
|
||
<field name="name">Migrate from Enterprise</field>
|
||
<field name="res_model">fusion.migration.wizard</field>
|
||
<field name="view_mode">form</field>
|
||
<field name="target">new</field>
|
||
</record>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **Step 8: Add ACL row for the wizard**
|
||
|
||
Edit `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_migration/security/ir.model.access.csv`:
|
||
|
||
```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 <module> <from_version> <to_version> [<output_md>]
|
||
#
|
||
# Example:
|
||
# tools/check_odoo_diff.sh account_accountant v19 v20 reports/v20_accountant_diff.md
|
||
|
||
set -euo pipefail
|
||
|
||
MODULE="${1:?Usage: check_odoo_diff.sh <module> <from_version> <to_version> [<output_md>]}"
|
||
FROM="${2:?from_version required (e.g. v19)}"
|
||
TO="${3:?to_version required (e.g. v20)}"
|
||
OUT="${4:-/dev/stdout}"
|
||
|
||
ROOT="${REPACKAGED_ODOO_ROOT:-/Users/gurpreet/Github/RePackaged-Odoo}"
|
||
FROM_DIR="$ROOT/accounting-$FROM/$MODULE"
|
||
TO_DIR="$ROOT/accounting-$TO/$MODULE"
|
||
|
||
if [ ! -d "$FROM_DIR" ]; then
|
||
echo "ERROR: $FROM_DIR does not exist. Snapshot v$FROM not yet present?" >&2
|
||
exit 1
|
||
fi
|
||
if [ ! -d "$TO_DIR" ]; then
|
||
echo "ERROR: $TO_DIR does not exist. Snapshot v$TO not yet present?" >&2
|
||
exit 1
|
||
fi
|
||
|
||
classify() {
|
||
local f="$1"
|
||
case "$f" in
|
||
*/views/*|*/static/src/components/*|*/report/*|*/wizard/*_views.xml|*/wizards/*_views.xml)
|
||
echo "[MIRROR]" ;;
|
||
*/models/*_engine.py|*/services/*)
|
||
echo "[ABSTRACT]" ;;
|
||
*/__manifest__.py)
|
||
echo "[MANIFEST]" ;;
|
||
*/tests/*)
|
||
echo "[TEST]" ;;
|
||
*)
|
||
echo "[REVIEW]" ;;
|
||
esac
|
||
}
|
||
|
||
{
|
||
echo "# Diff Report: $MODULE ($FROM -> $TO)"
|
||
echo ""
|
||
echo "Generated: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo ""
|
||
echo "## Changed Files (with classification suggestion)"
|
||
echo ""
|
||
diff -ruN --brief "$FROM_DIR" "$TO_DIR" | while read -r line; do
|
||
# parse "Files X and Y differ" or "Only in X: Y"
|
||
case "$line" in
|
||
"Files "*" and "*" differ")
|
||
file=$(echo "$line" | sed -E 's/^Files (.+) and .+ differ$/\1/' | sed "s|$FROM_DIR/||")
|
||
tag=$(classify "$file")
|
||
echo "- $tag \`$file\`"
|
||
;;
|
||
"Only in $TO_DIR"*)
|
||
file=$(echo "$line" | sed -E "s|Only in $TO_DIR(.*): (.+)|\1/\2|" | sed "s|^/||")
|
||
tag=$(classify "$file")
|
||
echo "- $tag NEW: \`$file\`"
|
||
;;
|
||
"Only in $FROM_DIR"*)
|
||
file=$(echo "$line" | sed -E "s|Only in $FROM_DIR(.*): (.+)|\1/\2|" | sed "s|^/||")
|
||
tag=$(classify "$file")
|
||
echo "- $tag REMOVED: \`$file\`"
|
||
;;
|
||
esac
|
||
done
|
||
echo ""
|
||
echo "## Full Diff (truncated to first 2000 lines)"
|
||
echo ""
|
||
echo '```diff'
|
||
diff -ruN "$FROM_DIR" "$TO_DIR" | head -2000
|
||
echo '```'
|
||
} > "$OUT"
|
||
|
||
echo "Diff report written to: $OUT" >&2
|
||
```
|
||
|
||
- [ ] **Step 3: Make it executable**
|
||
|
||
```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 <module> <from_version> <to_version> [<output_md>]
|
||
```
|
||
|
||
### 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-<version>/<module>` (default
|
||
root: `/Users/gurpreet/Github/RePackaged-Odoo`). Override the root with the
|
||
`REPACKAGED_ODOO_ROOT` env var.
|
||
```
|
||
|
||
- [ ] **Step 5: Smoke test the script**
|
||
|
||
```bash
|
||
cd /Users/gurpreet/Github/Odoo-Modules
|
||
ls /Users/gurpreet/Github/RePackaged-Odoo/
|
||
# Currently we only have one snapshot at "accounting/" not "accounting-v19/"
|
||
# So either rename the snapshot or just verify the script's error path works:
|
||
./fusion_accounting/tools/check_odoo_diff.sh account_accountant v19 v20 || echo "Expected error (no v20 snapshot yet)"
|
||
```
|
||
|
||
To prepare for V20 in the future:
|
||
|
||
```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 `<method>_via_fusion`, `<method>_via_enterprise`, `<method>_via_community`
|
||
- Adapter `_select_mode()` picks fusion if model loaded, else enterprise if module installed, else community
|
||
```
|
||
|
||
- [ ] **Step 5: Write `fusion_accounting_ai/UPGRADE_NOTES.md`**
|
||
|
||
```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.<module>.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.<module>.completed` to True in System Parameters.
|
||
```
|
||
|
||
- [ ] **Step 10: Rewrite `fusion_accounting/CLAUDE.md` as meta-module overview**
|
||
|
||
Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting/CLAUDE.md`
|
||
|
||
(Replace the entire file contents with:)
|
||
|
||
```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 <db> -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 <db> --no-http <<EOF
|
||
env['ir.module.module'].search([
|
||
('name', 'in', [
|
||
'fusion_accounting',
|
||
'fusion_accounting_ai',
|
||
'fusion_accounting_migration',
|
||
'fusion_accounting_core',
|
||
]),
|
||
('state', '=', 'installed'),
|
||
]).button_immediate_uninstall()
|
||
EOF
|
||
```
|
||
|
||
## Documentation
|
||
|
||
See `docs/superpowers/specs/` for the design and `docs/superpowers/plans/` for implementation plans.
|
||
```
|
||
|
||
- [ ] **Step 12: Commit all docs**
|
||
|
||
```bash
|
||
cd /Users/gurpreet/Github/Odoo-Modules
|
||
git add fusion_accounting_core/CLAUDE.md fusion_accounting_core/UPGRADE_NOTES.md fusion_accounting_core/README.md
|
||
git add fusion_accounting_ai/CLAUDE.md fusion_accounting_ai/UPGRADE_NOTES.md fusion_accounting_ai/README.md
|
||
git add fusion_accounting_migration/CLAUDE.md fusion_accounting_migration/UPGRADE_NOTES.md fusion_accounting_migration/README.md
|
||
git add fusion_accounting/CLAUDE.md fusion_accounting/README.md
|
||
git commit -m "docs(fusion_accounting): per-sub-module CLAUDE.md, UPGRADE_NOTES.md, README.md"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 21: CI Pipeline (Gitea or GitHub Actions)
|
||
|
||
**Files:**
|
||
- Create: `/Users/gurpreet/Github/Odoo-Modules/.gitea/workflows/fusion_accounting_ci.yml` (if Gitea)
|
||
OR `/Users/gurpreet/Github/Odoo-Modules/.github/workflows/fusion_accounting_ci.yml` (if GitHub)
|
||
|
||
- [ ] **Step 1: Determine which forge is in use**
|
||
|
||
Run:
|
||
```bash
|
||
cd /Users/gurpreet/Github/Odoo-Modules && git remote -v
|
||
```
|
||
|
||
If the remote contains `gitea` or a self-hosted gitea URL, use `.gitea/workflows/`. If it's `github.com`, use `.github/workflows/`. If neither/both, ask the user.
|
||
|
||
- [ ] **Step 2: Create the workflow directory**
|
||
|
||
```bash
|
||
# Adjust path based on Step 1 finding
|
||
mkdir -p /Users/gurpreet/Github/Odoo-Modules/.gitea/workflows
|
||
# OR
|
||
mkdir -p /Users/gurpreet/Github/Odoo-Modules/.github/workflows
|
||
```
|
||
|
||
- [ ] **Step 3: Write the workflow file**
|
||
|
||
Path (Gitea example): `/Users/gurpreet/Github/Odoo-Modules/.gitea/workflows/fusion_accounting_ci.yml`
|
||
|
||
```yaml
|
||
name: fusion_accounting CI
|
||
|
||
on:
|
||
push:
|
||
paths:
|
||
- 'fusion_accounting/**'
|
||
- 'fusion_accounting_core/**'
|
||
- 'fusion_accounting_ai/**'
|
||
- 'fusion_accounting_migration/**'
|
||
pull_request:
|
||
paths:
|
||
- 'fusion_accounting/**'
|
||
- 'fusion_accounting_core/**'
|
||
- 'fusion_accounting_ai/**'
|
||
- 'fusion_accounting_migration/**'
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
services:
|
||
postgres:
|
||
image: postgres:15
|
||
env:
|
||
POSTGRES_USER: odoo
|
||
POSTGRES_PASSWORD: odoo
|
||
POSTGRES_DB: postgres
|
||
ports: ['5432:5432']
|
||
options: --health-cmd pg_isready --health-interval 10s
|
||
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
sub_module:
|
||
- fusion_accounting_core
|
||
- fusion_accounting_ai
|
||
- fusion_accounting_migration
|
||
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Set up Python 3.11
|
||
uses: actions/setup-python@v4
|
||
with:
|
||
python-version: '3.11'
|
||
|
||
- name: Install Odoo 19 + dependencies
|
||
run: |
|
||
pip install --break-system-packages anthropic openai
|
||
# Install Odoo from source or pin a Docker image; adjust to local convention.
|
||
# Placeholder: this CI step needs to be aligned with how Nexa builds Odoo locally.
|
||
|
||
- name: Install fusion sub-modules
|
||
run: |
|
||
# Copy modules to addons path
|
||
mkdir -p /tmp/addons
|
||
cp -r fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration /tmp/addons/
|
||
|
||
- name: Initialize Odoo DB and install module
|
||
run: |
|
||
createdb -h localhost -U odoo fusion_test
|
||
odoo -d fusion_test --addons-path=/tmp/addons -i ${{ matrix.sub_module }} --stop-after-init --without-demo=all
|
||
|
||
- name: Run tests
|
||
run: |
|
||
odoo -d fusion_test --addons-path=/tmp/addons --test-tags='post_install' --stop-after-init -u ${{ matrix.sub_module }}
|
||
```
|
||
|
||
(Note: the Odoo 19 installation step is a placeholder. The actual install
|
||
mechanism depends on how Nexa builds Odoo locally — Docker image vs. source
|
||
checkout vs. pip-installed package. Adjust based on the existing
|
||
`docker exec odoo-dev-app odoo` pattern in `CLAUDE.md`.)
|
||
|
||
- [ ] **Step 4: Skip CI if not viable in this environment**
|
||
|
||
If setting up Odoo on a fresh runner is too involved for this Phase 0 (it
|
||
often requires Docker-in-Docker or a custom self-hosted runner), document
|
||
the deferral:
|
||
|
||
Path: `/Users/gurpreet/Github/Odoo-Modules/fusion_accounting/docs/superpowers/specs/2026-04-18-ci-deferred.md`
|
||
|
||
```markdown
|
||
# CI Deferred to Phase 1
|
||
|
||
The Phase 0 plan included setting up automated CI for fusion sub-modules.
|
||
Setting up Odoo 19 on a clean CI runner requires:
|
||
- Docker-in-Docker support OR a self-hosted runner with Odoo pre-installed
|
||
- A PostgreSQL service container
|
||
- Pinned Python dep set (anthropic, openai, plus Odoo's own deps)
|
||
|
||
This is non-trivial setup that benefits from being aligned with how Nexa
|
||
already runs Odoo locally (the `odoo-westin` Docker pattern). To avoid
|
||
half-doing this, CI setup is deferred to Phase 1, where it can be designed
|
||
together with the empirical-test infrastructure.
|
||
|
||
For Phase 0, tests are run manually by the engineer using:
|
||
|
||
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 \
|
||
--test-tags post_install --stop-after-init \
|
||
-c /etc/odoo/odoo.conf -u <sub_module>"
|
||
```
|
||
|
||
- [ ] **Step 5: Commit either the CI yaml or the deferral note**
|
||
|
||
```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 — <date>
|
||
|
||
- All test suites passed (X tests, 0 failures)
|
||
- AI chat panel responsive
|
||
- Bank rec data adapter returning correct shape
|
||
- Reports data adapter returning correct shape
|
||
- Migration wizard opens
|
||
- Safety guard fires on Enterprise uninstall attempt
|
||
- Multi-company record rule on session enforced
|
||
- All sub-modules show as installed in Apps page
|
||
|
||
Phase 0 complete. Ready for Phase 1 brainstorming.
|
||
```
|
||
|
||
(This is the only edit to the plan file itself, made at the end as a completion marker.)
|
||
|
||
- [ ] **Step 7: Tag the Phase 0 completion**
|
||
|
||
```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`.
|
||
|
||
---
|
||
|
||
## Phase 0 Smoke Test Results — 2026-04-18
|
||
|
||
Host: `odoo-westin` (container `odoo-dev-app`, DB `westin-v19`, Odoo 19, Enterprise installed alongside).
|
||
|
||
### Deploy
|
||
- Clean redeploy: removed and re-copied all four modules (`fusion_accounting`, `fusion_accounting_core`, `fusion_accounting_ai`, `fusion_accounting_migration`) into `/mnt/extra-addons/` on the container.
|
||
- Meta-module upgrade (`odoo -u fusion_accounting --stop-after-init --no-http`): exit 0, all four modules `installed` in `ir_module_module`. Only pre-existing unrelated warnings (studio, fusion_claims label collisions, docutils, `_sql_constraints` deprecations on third-party modules).
|
||
|
||
### Test suite results
|
||
- Command: `odoo --test-tags post_install --stop-after-init --no-http -u fusion_accounting_core,fusion_accounting_ai,fusion_accounting_migration`
|
||
- Exit code: **0**
|
||
- Per-test `Starting …` lines observed (odoo.tests INFO handler): **23 tests**
|
||
- `fusion_accounting_core` — 7 tests: `TestEnterpriseDetection` ×2, `TestSharedFieldOwnership` ×5
|
||
- `fusion_accounting_ai` — 14 tests: `TestDataAdapterBase` ×2, `TestBankRecAdapter` ×1, `TestReportsAdapter` ×4, `TestFollowupAdapter` ×4, `TestAssetsAdapter` ×1, `TestPostMigration` ×2
|
||
- `fusion_accounting_migration` — 2 tests: `TestSafetyGuard` ×2
|
||
- Result: **23 PASS, 0 FAIL, 0 ERROR, 0 SKIP**
|
||
- No `AssertionError` / `Traceback` / `FAILED` lines in the log.
|
||
- Odoo's `odoo.tests.stats` reports slightly higher per-module counts (ai: 26, core: 11, migration: 4) because Odoo also counts its own implicit per-module sanity checks (XML validation, etc.) beyond our explicit `TestCase` methods; all non-explicit tests also passed since exit code is 0 and no failure lines appear.
|
||
|
||
### Verification spot-checks
|
||
- **Migration wizard menu (6a)**: present — `ir_ui_menu` contains both `Fusion Accounting` (id 2802, root) and `Migrate from Enterprise` (id 2803, child of 2802). Ten total fusion menus registered across `fusion_accounting_ai` (8) and `fusion_accounting_migration` (2).
|
||
- **AI module actions (6b)**: 8 actions registered under `module='fusion_accounting_ai'` — `action_fusion_session`, `action_fusion_history`, `action_fusion_rule`, `action_fusion_dashboard`, `action_vendor_tax_profiles`, `action_recurring_patterns`, `action_fusion_rule_wizard`, `action_report_fusion_audit`.
|
||
- **Security groups (6c)**: three groups present in `fusion_accounting_core` — `Administrator`, `Manager`, `User`, each with `0` users (expected for a fresh install with no user assignments yet).
|
||
- **Shared-field columns on `account_move` (6d)**:
|
||
- `signing_user` (integer, FK to `res_users`) — physically present, owned by `fusion_accounting_core` ✓
|
||
- `payment_state_before_switch` (character varying) — physically present, owned by `fusion_accounting_core` ✓
|
||
- `deferred_move_ids` / `deferred_original_move_ids` — both present via m2m relation table `account_move_deferred_rel` with columns `original_move_id` / `deferred_move_id` (matches Enterprise's table name; test `test_deferred_relation_table_name_matches_enterprise` passes) ✓
|
||
- `deferred_entry_type` — exists in the ORM (`ir_model_fields.store='f'`) but no local column, because Enterprise's `account_asset` (installed on this DB: `account_accountant`, `account_asset`, `account_reports` all `installed`) currently owns the physical storage. This is the intended dual-ownership design from Task 17 — fusion_accounting_core declares a stub so the field survives Enterprise uninstall; the `TestSharedFieldOwnership.test_account_move_deferred_fields_exist` test passed and confirmed the field is in `Move._fields`.
|
||
|
||
### Deferred
|
||
- **Task 18** (empirical Enterprise-uninstall verification test): deferred pending environment provisioning decision. Requires a dedicated scratch DB where we can actually uninstall Enterprise without disturbing the productive westin-v19 tenant. Tracked in `fusion_accounting/docs/superpowers/plans/2026-04-18-ci-deferred.md` (or equivalent follow-up note). The shared-field design is validated in principle by Tasks 17+21 and the `TestSharedFieldOwnership` suite; Task 18 adds the "actually uninstall, confirm nothing collapses" live check.
|
||
|
||
### Phase 0 Status: **COMPLETE** (pending Task 18 empirical test)
|
||
|
||
Ready to proceed to Phase 1 (Bank Reconciliation) — brainstorming session + its own design doc + implementation plan.
|