273 lines
17 KiB
Markdown
273 lines
17 KiB
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
|
|
|
|
---
|
|
|
|
## Architecture
|
|
```
|
|
fusion_accounting_ai/
|
|
├── 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
|
|
│ ├── data_adapters/ Tri-mode domain routers (fusion / enterprise / community)
|
|
│ ├── 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/ ACLs + record rules (groups themselves live in fusion_accounting_core)
|
|
├── 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
|
|
|
|
### 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
|
|
|
|
## 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_ai/`
|
|
- **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_ai"
|
|
scp -r "K:\Github\Odoo-Modules\fusion_accounting_ai" odoo-westin:/tmp/fusion_accounting_ai
|
|
ssh odoo-westin "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 -u fusion_accounting_ai --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_ai';\""
|
|
```
|
|
|
|
## Security Groups
|
|
(The three groups themselves are now defined in `fusion_accounting_core`. This
|
|
module's `security/ir.model.access.csv` grants access on AI-specific models
|
|
using those group XML-ids.)
|
|
|
|
| XML ID (in fusion_accounting_core) | Name | Access in AI module |
|
|
|---|---|---|
|
|
| `group_fusion_accounting_user` | User | Dashboard, chat (read-only tools) |
|
|
| `group_fusion_accounting_manager` | Manager | + Approve/reject, Tier 2 tools, rules |
|
|
| `group_fusion_accounting_admin` | Administrator | + Config, all tools, rule admin |
|
|
|
|
Auto-assigned (configured in _core): `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
|
|
|
|
## 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 on `fusion.accounting.session` — added in Phase 0 split-out (see UPGRADE_NOTES.md)
|