# 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` — `Period` dataclass + comparison-period math - `account_hierarchy` — chart-of-accounts tree walk - `totaling` — debit/credit/balance roll-ups - `currency_conversion` — multi-currency conversion via `res.currency.rate` - `line_resolver` — JSON `line_specs` → rendered rows - `drill_down_resolver` — line → underlying journal items - `anomaly_detection` — variance vs prior period (z-score + abs/pct gates) - `commentary_generator` — LLM narrative with templated fallback - `commentary_prompt` — provider-agnostic system + user prompt Persisted models in `models/`: - `fusion.report` — definition with JSON `line_specs` - `fusion.report.commentary` — LLM-output cache (one per period+mode) - `fusion.report.anomaly` — flagged variances - `fusion.account.balance.mv` — pre-aggregated materialized view - `fusion.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 overrides - `services/reports_service.js` — central reactive state + RPC wrappers - `views/report_viewer/` — top-level OWL view + view-registry adapter - `components/report_table/` — generic financial-table renderer - `components/drill_down_dialog/` — modal for journal-item listing - `components/period_filter/` — date-range + comparison picker - `components/ai_commentary_panel/` — LLM commentary surface - `components/anomaly_strip/` — variance summary banner - `tours/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` (use `models.Constraint`), `@api.depends('id')` (raises `NotImplementedError`), `@route(type='json')` (use `type='jsonrpc'`), `numbercall` field on `ir.cron` (removed), `groups_id` on `res.users` (use `all_group_ids` for searching), `users` field on `res.groups` (use `user_ids`), `groups_id` on `ir.ui.menu` (use `group_ids`). - **Engine signature:** Public methods are keyword-only after the leading positional `period` / `date_to`. Always pass `company_id=...` explicitly. - **`fusion.report` lookup:** `_get_report` falls back from per-company override to global (`company_id=False`) — order is `company_id desc nulls last`. - **Materialized view refresh:** `fusion.account.balance.mv` rebuilds 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 prefixed `account:`, `prefix:`, `formula:` or `header` — `line_resolver.py` resolves 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 unless `force_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 without `websocket-client`) - Property-based, integration, controller, materialized-view, coexistence, migration round-trip, PDF/XLSX export ## 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 + EQUITY` math limited (no subtotal-of-subtotals expansion in `formula:` specs) - GL `line_specs` need `prefix:` empty-string handling for "all accounts" semantics - Header rows (no compute payload) silently skipped by `line_resolver` — fine for layout, but a `header_only=True` flag would be clearer - `expense` prefix overlaps with subtypes (`expense_direct_cost`, `expense_depreciation`) — current line_specs need explicit ordering or a longer-prefix-wins rule - `wkhtmltopdf` may need configuration for PDF export on first install - `ReportsAdapter.run_report` vs `run_fusion_report` naming (legacy clash with Enterprise wrapper) - Tour tests skip when `websocket-client` is absent — install it in CI to exercise the OWL surface end-to-end