Files
Odoo-Modules/fusion_accounting/CLAUDE.md
gsinghpal c66bdf5089 changes
2026-04-03 15:45:18 -04:00

249 lines
16 KiB
Markdown

# fusion_accounting — AI Accounting Co-Pilot
## What This Module Does
An AI agent (Claude/GPT with tool-calling) embedded in Odoo 19 Enterprise Accounting. Conversational interface backed by a dashboard for bank reconciliation, HST/GST management, AR/AP analysis, journal review, month-end close, payroll, inventory, ADP reconciliation, financial reporting, and auditing.
## Architecture
```
fusion_accounting/
├── models/ 7 files (5 new models + 2 inherits: account.move, res.config.settings)
├── services/
│ ├── agent.py AI orchestrator (prompt assembly, tool dispatch loop)
│ ├── adapters/ Claude + OpenAI adapters with native tool-calling
│ ├── tools/ 93 tool functions across 11 domain files
│ ├── prompts/ System prompt builder + 12 domain-specific prompts
│ └── scoring.py Confidence scoring + tier promotion logic
├── controllers/ 10 JSON-RPC endpoints
├── wizards/ Rule creation wizard
├── static/src/ OWL dashboard + chat panel + approval cards
├── views/ List/form/search views, menus, settings
├── security/ 3 groups (User/Manager/Admin), record rules, ACLs
├── data/ 88 tool definitions, 2 default rules, 2 crons, 1 sequence
├── tests/ API integration tests
└── report/ Audit report QWeb template
```
## Key Design Decisions
### AI Provider Integration
- Uses `fusion.api.service` (from fusion_api module) for API key resolution with fallback to `ir.config_parameter` — NO hard dependency on fusion_api
- Claude adapter: native `tool_use` blocks, extended thinking enabled (8K budget) for all Claude 4.x models
- OpenAI adapter: Chat Completions API with o-series reasoning model support (`developer` role, `max_completion_tokens`, `reasoning_effort`)
- API keys stored in `ir.config_parameter` with `fusion_accounting.` prefix
- API key fields in Settings use `password="True"` widget — labels include "(Fusion AI)" suffix to avoid conflicts with other modules' key fields
- **Provider pinning**: Sessions remember which provider was used. If the global provider changes mid-session, the session continues with its original provider to prevent cross-adapter message format contamination.
### Tool Tiering
- **Tier 1** (Free): Read-only, execute immediately — 60+ tools
- **Tier 2** (Auto-approved): Low-risk writes, logged — ~10 tools
- **Tier 3** (Requires approval): Financial writes, user must approve — ~15 tools
- Auto-promotion: Tier 3 → Tier 2 at 95% accuracy over 30+ decisions (atomic SQL counters on `fusion.accounting.rule._record_decision`)
- Tool descriptions include tier labels (e.g., `[Tier 3: Requires user approval]`) so the AI knows which tools need approval
- When a Tier 3 tool is encountered during the chat loop, the loop short-circuits: a final text response is forced so the AI can present approval cards to the user
### Tier 3 Approval Flow
- When a Tier 3 action is approved/rejected, the session's `message_ids_json` is updated to replace the `pending_approval` placeholder with the actual tool result — this prevents dangling `tool_use` blocks that would cause API errors on the next chat turn
- After approval, `scoring.check_promotions()` is called to check if any rules should be promoted
### Menu Location
- **Parent**: `accountant.menu_accounting` (NOT `account.menu_finance` — that's Community Edition only)
- Enterprise uses `accountant.menu_accounting` (ID 1663) as the visible menu root
- `account.menu_finance` (ID 180) exists but has NO visible children in Enterprise — it's the Community root
### Session Persistence
- Chat sessions stored in `fusion.accounting.session` with `message_ids_json` (JSON text field)
- On page load, chat panel calls `/session/latest` to restore the most recent active session
- Empty assistant messages (tool-call-only responses with no text) are filtered out by the controller
- "New Chat" button closes current session and creates a fresh one
- Session name (e.g., FAS/2026/00001) shown in the chat header
- **Session ownership**: Controllers verify the current user owns the session (managers can access any session)
### Rich Text Chat Output
- AI responses are rendered as rich HTML, not plain text
- Markdown-to-HTML conversion happens client-side in `chat_panel.js` via `mdToHtml()` function
- HTML is injected via `innerHTML` on `onMounted` + `onPatched` (NOT via OWL's `markup()` / `t-out` — those proved unreliable in Odoo 19)
- The `_renderRichMessages()` method finds `.fusion_rich_slot[data-idx]` divs and sets their innerHTML
- Supported: headers (# through #####), **bold**, *italic*, `code`, tables, bullet/numbered lists, horizontal rules, [links](url)
- System prompt instructs AI to use markdown formatting and include Odoo record links like `[INV/2026/00123](/odoo/accounting/123)`
### Interactive Tables (fusion-table)
- AI can return `fusion-table` fenced code blocks instead of Markdown tables for actionable results
- `mdToHtml()` detects these blocks, extracts JSON, and renders `FusionInteractiveTable` OWL components via `mount()`
- **Interactive mode**: checkbox column + data columns + AI Recommendation column (colour-coded badge) + Your Input column (text field per row) + bottom bulk action bar
- **Read-only mode**: styled table, no inputs/actions
- Actions: Apply Recommendations, Flag Selected, Create Rules, Dismiss Selected, Submit All Notes to AI
- Action button clicks format a `[TABLE_ACTION]` structured message and send it back through the chat endpoint
- The AI decides per-response whether to use interactive or Markdown tables based on whether the data is actionable
- Used for: `find_missing_itc_bills`, `find_duplicate_bills`, `get_overdue_invoices`, `find_draft_entries`, `get_unreconciled_bank_lines`, etc.
- NOT used for: `get_profit_loss`, `get_balance_sheet`, `get_trial_balance` (informational, read-only)
- All styles use Odoo CSS variables — dark/light mode handled automatically
### Dashboard Layout
- Health cards row at top (6 cards: Bank Recon, AR, AP, HST, Audit Score, Month-End)
- Below: side-by-side layout — "Needs Attention" panel (flex-grow) + Chat panel (720px fixed width)
- Chat panel is 720px (80% larger than original 400px design)
- Dashboard endpoint returns `needs_attention` and `recent_activity` JSON arrays alongside health card metrics
## Odoo 19 Gotchas (Learned the Hard Way)
### Search Views
- NO `string` attribute on `<search>` element
- NO `string` attribute on `<group>` element inside search views
- Group-by filters MUST have `domain="[]"` attribute
- Add `<separator/>` before `<group>` in search views
### OWL Client Actions
- Components registered as client actions receive props: `action`, `actionId`, `updateActionState`, `className`
- Must use `static props = ["*"]` (accept any) — NOT `static props = []` (accept none)
### OWL Rich HTML Rendering
- `markup()` from `@odoo/owl` + `t-out` is UNRELIABLE in Odoo 19 for rendering HTML in OWL components
- Use `onMounted` + `onPatched` hooks to find DOM elements and set `innerHTML` directly
- Pattern: render a placeholder `<div class="slot" t-att-data-idx="index"/>`, then in the hook find it and set `.innerHTML`
- Always use BOTH `onMounted` AND `onPatched``onPatched` alone misses the first render
### Cron Safe Eval
- NO `import` statements (forbidden opcode `IMPORT_NAME`)
- `datetime` module available as `datetime` (use `datetime.datetime.now()`, `datetime.timedelta()`)
- NO `from datetime import X` pattern
### read_group Deprecated
- `read_group()` is deprecated in Odoo 19 — use `_read_group()` instead
- Still works but throws DeprecationWarning
- Dashboard `accounting_dashboard.py` still uses `read_group()` — migrate to `_read_group()` when the new API is stable
### Config Parameter Values
- When changing a Selection field's options, the stored DB value in `ir_config_parameter` must match one of the new options or Settings page will crash with `ValueError: Wrong value`
- Fix: UPDATE the value in DB after changing selection options:
```sql
UPDATE ir_config_parameter SET value = 'new_value' WHERE key = 'fusion_accounting.field_name';
```
### Field Label Conflicts
- Odoo warns if two fields on the same model have the same `string` label
- Our `display_name_field` conflicted with built-in `display_name` — renamed string to "Tool Label"
- API key fields use "(Fusion AI)" suffix to avoid label conflicts with other modules
- Tool model uses `domain` (not `domain_name`) and `parameters_schema` (not `parameters`) as field names
### Group Assignment
- `implied_ids` on groups only applies to NEWLY added users, not existing ones
- After installing, manually add existing users to groups via SQL:
```sql
INSERT INTO res_groups_users_rel (gid, uid)
SELECT <group_id>, gu.uid FROM res_groups_users_rel gu
JOIN ir_model_data imd ON imd.res_id = gu.gid AND imd.model = 'res.groups'
WHERE imd.module = 'account' AND imd.name = 'group_account_manager'
ON CONFLICT DO NOTHING;
```
### TransientModel in Controllers
- Use `.new({...})` NOT `.create({...})` for TransientModels in controller endpoints
- `.create()` writes a DB row on every request; `.new()` is in-memory only
- Dashboard controller uses `.new()` to compute health metrics without DB writes
## Server Details
- **Server**: odoo-westin (192.168.1.40, SSH via `ssh odoo-westin`)
- **Container**: odoo-dev-app (Odoo), odoo-dev-db (PostgreSQL)
- **Database**: westin-v19
- **Module path**: `/mnt/extra-addons/fusion_accounting/`
- **Python deps**: anthropic (v0.88.0), openai (v2.30.0) — installed with `--break-system-packages`
- **URL**: erp.westinhealthcare.ca
## Deployment Commands
```bash
# Full deploy cycle (clean + copy + upgrade + restart)
ssh odoo-westin "docker exec -u 0 odoo-dev-app rm -rf /mnt/extra-addons/fusion_accounting"
scp -r "K:\Github\Odoo-Modules\fusion_accounting" odoo-westin:/tmp/fusion_accounting
ssh odoo-westin "docker cp /tmp/fusion_accounting odoo-dev-app:/mnt/extra-addons/fusion_accounting && rm -rf /tmp/fusion_accounting"
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_accounting --stop-after-init --http-port=8099 -c /etc/odoo/odoo.conf"
ssh odoo-westin "docker restart odoo-dev-app"
# Check logs
ssh odoo-westin "docker logs odoo-dev-app --tail 100"
# Quick DB queries
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19 -t -c \"<SQL>\""
# Check module state
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19 -t -c \"SELECT name, state, latest_version FROM ir_module_module WHERE name = 'fusion_accounting';\""
```
## Security Groups
| Group ID | XML ID | Name | Access |
|---|---|---|---|
| 564 | `group_fusion_accounting_user` | User | Dashboard, chat (read-only tools) |
| 565 | `group_fusion_accounting_manager` | Manager | + Approve/reject, Tier 2 tools, rules |
| 566 | `group_fusion_accounting_admin` | Administrator | + Config, all tools, rule admin |
Auto-assigned: `account.group_account_user` → User, `account.group_account_manager` → Admin
## Controller Endpoints
| Route | Auth | Purpose |
|---|---|---|
| `/fusion_accounting/session/create` | user | Create new chat session |
| `/fusion_accounting/session/close` | user (ownership check) | Close active session |
| `/fusion_accounting/session/latest` | user (own sessions only) | Load most recent active session + messages |
| `/fusion_accounting/session/history` | user (ownership check, managers see all) | Load specific session messages |
| `/fusion_accounting/chat` | user (ownership check) | Send message, get AI response |
| `/fusion_accounting/approve` | user + manager group check | Approve single Tier 3 action |
| `/fusion_accounting/reject` | user + manager group check | Reject single Tier 3 action |
| `/fusion_accounting/approve_all` | user + manager group check | Batch approve multiple actions |
| `/fusion_accounting/reject_all` | user + manager group check | Batch reject multiple actions |
| `/fusion_accounting/dashboard/data` | user | Get dashboard health card metrics + needs_attention + recent_activity |
Note: Approve/reject endpoints use `auth='user'` at the decorator level with an imperative `has_group()` check inside the handler (Odoo has no built-in `auth='manager'`).
## Models
| Model | Type | Location | Purpose |
|---|---|---|---|
| `fusion.accounting.session` | Model | models/ | Chat sessions with message JSON storage |
| `fusion.accounting.match.history` | Model | models/ | Every AI tool call + decision (approved/rejected/pending) |
| `fusion.accounting.rule` | Model | models/ | Fusion Rules engine with versioning and auto-promotion |
| `fusion.accounting.tool` | Model | models/ | Tool registry (82 tools seeded from XML) |
| `fusion.accounting.dashboard` | TransientModel | models/ | Computed health metrics (use `.new()` not `.create()`) |
| `res.config.settings` (inherit) | TransientModel | models/ | Settings page (API keys, thresholds, toggles) |
| `account.move` (inherit) | Model | models/ | Post-action audit hook |
| `fusion.accounting.agent` | AbstractModel | services/ | AI orchestrator |
| `fusion.accounting.adapter.claude` | AbstractModel | services/ | Claude tool-calling adapter |
| `fusion.accounting.adapter.openai` | AbstractModel | services/ | OpenAI tool-calling adapter |
| `fusion.accounting.scoring` | AbstractModel | services/ | Confidence scoring |
| `fusion.accounting.rule.wizard` | TransientModel | wizards/ | Quick-create rule from chat suggestion |
## AI Models Available
**Claude** (default: claude-sonnet-4-6):
- claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5
- claude-sonnet-4-5, claude-opus-4-5, claude-sonnet-4-0, claude-opus-4-0
**OpenAI** (default: gpt-5.4-mini):
- gpt-5.4, gpt-5.4-mini, gpt-5.4-nano
- o3, o4-mini
- gpt-4o, gpt-4o-mini (legacy)
## Theme / Styling Rules
- NO hardcoded colours — use CSS variables (`var(--o-border-color)`, `var(--bs-body-color-rgb)`) and Bootstrap utility classes
- Must work in both light and dark mode
- Box shadows: use `rgba(var(--bs-body-color-rgb), 0.1)` not `rgba(0,0,0,0.1)`
- AI messages use `var(--o-view-background-color)` background + `var(--o-border-color)` border
- Links use `var(--o-action-color)` for theme awareness
### HST Filing Workflow (4-Phase AI-Driven)
- Phase 1: AI runs all HST reports (tax report, missing ITCs, compliance audit, HST balance)
- Phase 2: AI sweeps ALL bank accounts for unreconciled expense payments
- Phase 3: Per-line processing — check for existing bills, check history for coding patterns, ask about HST, create bills, register payments
- Phase 4: Re-run reports to verify updated HST position
- New tools added: `search_partners` (Tier 1), `find_similar_bank_lines` (Tier 1), `get_bank_line_details` (Tier 1), `create_vendor_bill` (Tier 3), `register_bill_payment` (Tier 3), `create_expense_entry` (Tier 3)
- Two paths for recording expenses: (a) formal vendor bill + payment, or (b) direct GL entry in MISC journal with optional HST split
- The `create_expense_entry` tool posts directly to the Miscellaneous Operations journal — debit expense + debit HST ITC (2006) + credit bank
- Domain prompt (`hst_management` in domain_prompts.py) includes bank journal IDs and the full 4-phase workflow instructions
## Known Issues / Future Work
- `read_group()` deprecation warnings in `accounting_dashboard.py` — migrate to `_read_group()` when the new API format is stable
- `generate_t4`, `generate_roe` are stubs pointing to fusion_payroll (by design — Phase 2)
- `get_payroll_schedule`, `verify_source_deductions`, `verify_payroll_deductions` are stubs (Phase 2 — fusion_payroll integration)
- `answer_financial_question` is a stub (returns message to use other tools instead)
- Batch approval "Approve All" / "Reject All" buttons are in the chat panel but not yet in the match history list view
- "Needs Attention" panel shows placeholder text in the dashboard — the data is computed and returned by the API but the frontend rendering needs to be connected
- Consider switching OpenAI adapter from Chat Completions API to Responses API for better tool handling with newer models
- `o1` model does not support tool calling — no guard in place (o3/o4-mini do support it)
- Multi-company record rule missing on `fusion.accounting.session` — add if multi-company usage is needed