# 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 `` element - NO `string` attribute on `` element inside search views - Group-by filters MUST have `domain="[]"` attribute - Add `` before `` 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 `
`, 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 , 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 \"\"" # 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