6.5 KiB
fusion_accounting_reports — Cursor / Claude Context
Purpose
AI-augmented financial reports — a Fusion-native replacement for Odoo
Enterprise's account_reports module. Phase 2 of the fusion_accounting
roadmap.
CORE scope:
- Income Statement (P&L)
- Balance Sheet
- Trial Balance
- General Ledger (with drill-down)
AI augmentation:
- Anomaly detection (variance vs prior period)
- AI commentary (LLM-generated narrative)
Architecture
Hybrid: the engine (fusion.report.engine, AbstractModel) is the SINGLE
read surface for reports. Per-report definitions are stored as fusion.report
records with JSON line_specs so non-developers can tweak the layouts.
Public engine API (5 methods):
compute_pnl(period, *, comparison='none', company_id=None)compute_balance_sheet(date_to, *, comparison='none', company_id=None)compute_trial_balance(period, *, company_id=None)compute_gl(period, *, account_ids=None, company_id=None)drill_down(*, account_id, period, company_id=None)
Pure-Python services in services/ (no Odoo imports — independently
unit-testable):
date_periods—Perioddataclass + comparison-period mathaccount_hierarchy— chart-of-accounts tree walktotaling— debit/credit/balance roll-upscurrency_conversion— multi-currency conversion viares.currency.rateline_resolver— JSONline_specs→ rendered rowsdrill_down_resolver— line → underlying journal itemsanomaly_detection— variance vs prior period (z-score + abs/pct gates)commentary_generator— LLM narrative with templated fallbackcommentary_prompt— provider-agnostic system + user prompt
Persisted models in models/:
fusion.report— definition with JSONline_specsfusion.report.commentary— LLM-output cache (one per period+mode)fusion.report.anomaly— flagged variancesfusion.account.balance.mv— pre-aggregated materialized viewfusion.report.engine— AbstractModel (the API)fusion.reports.cron— cron handlers (commentary refresh, MV refresh)fusion.xlsx.export.wizard— TransientModel (XLSX export)fusion.period.picker.wizard— TransientModel (UX entry-point)fusion.migration.wizard(inherits) — adds_reports_bootstrap_step
Controller: controllers/reports_controller.py exposes 8 JSON-RPC endpoints
under /fusion/reports/*. All read paths route through the engine.
OWL frontend: static/src/
scss/— variables, base styles, dark-mode overridesservices/reports_service.js— central reactive state + RPC wrappersviews/report_viewer/— top-level OWL view + view-registry adaptercomponents/report_table/— generic financial-table renderercomponents/drill_down_dialog/— modal for journal-item listingcomponents/period_filter/— date-range + comparison pickercomponents/ai_commentary_panel/— LLM commentary surfacecomponents/anomaly_strip/— variance summary bannertours/reports_tours.js— 5 OWL tour smoke tests
Coexistence
When account_reports is installed, the Reports menu hides via
fusion_accounting_core.group_fusion_show_when_enterprise_absent
(a computed group). The engine + AI tools (commentary, anomaly detection)
remain available for the chat regardless.
Conventions
-
V19 deprecations to avoid:
_sql_constraints(usemodels.Constraint),@api.depends('id')(raisesNotImplementedError),@route(type='json')(usetype='jsonrpc'),numbercallfield onir.cron(removed),groups_idonres.users(useall_group_idsfor searching),usersfield onres.groups(useuser_ids),groups_idonir.ui.menu(usegroup_ids). -
Engine signature: Public methods are keyword-only after the leading positional
period/date_to. Always passcompany_id=...explicitly. -
fusion.reportlookup:_get_reportfalls back from per-company override to global (company_id=False) — order iscompany_id desc nulls last. -
Materialized view refresh:
fusion.account.balance.mvrebuilds via a dedicated autocommit cursor (REFRESH CONCURRENTLY can't run inside Odoo's regular transaction). Triggered by cron + on demand from the engine when data is older than the configured TTL. -
JSON
line_specs: Strings prefixedaccount:,prefix:,formula:orheader—line_resolver.pyresolves each spec to a row. Header rows have no compute payload and are silently skipped by downstream totals. -
Commentary cache: Keyed on
(report_id, company_id, period_from, period_to, comparison_mode)with a unique constraint. Re-runs use the cache unlessforce_refresh=True.
Test counts (Phase 2 ship)
- 130 logical tests, 0 failed, 0 errors
- Includes:
- 6 benchmarks (tagged
benchmark) - 1 LLM compat smoke (tagged
local_llm, skips when no LLM) - 5 OWL tours (tagged
tour, skips withoutwebsocket-client) - Property-based, integration, controller, materialized-view, coexistence, migration round-trip, PDF/XLSX export
- 6 benchmarks (tagged
Performance baseline
| Operation | Median | P95 | Budget |
|---|---|---|---|
engine.compute_pnl |
3ms | 8ms | <2000ms |
engine.compute_balance_sheet |
15ms | 20ms | <2000ms |
engine.compute_trial_balance |
3ms | 8ms | <1000ms |
engine.compute_gl |
25ms | 81ms | <3000ms |
engine.drill_down |
2ms | 10ms | <500ms |
controller.run (HTTP round-trip) |
9ms | 46ms | <2500ms |
All metrics within 1x of budget at Phase 2 ship. Numbers from
tests/test_performance_benchmarks.py against the dev VM
(westin-v19, ~1 fiscal year of data).
Known concerns / Phase 2.5 backlog
- Trial balance period-only sum doesn't auto-close to retained earnings
(drift visible in
test_trial_balance_total_near_zero, currently skipped) - Balance sheet
TOTAL LIABILITIES + EQUITYmath limited (no subtotal-of-subtotals expansion informula:specs) - GL
line_specsneedprefix:empty-string handling for "all accounts" semantics - Header rows (no compute payload) silently skipped by
line_resolver— fine for layout, but aheader_only=Trueflag would be clearer expenseprefix overlaps with subtypes (expense_direct_cost,expense_depreciation) — current line_specs need explicit ordering or a longer-prefix-wins rulewkhtmltopdfmay need configuration for PDF export on first installReportsAdapter.run_reportvsrun_fusion_reportnaming (legacy clash with Enterprise wrapper)- Tour tests skip when
websocket-clientis absent — install it in CI to exercise the OWL surface end-to-end