Files
gsinghpal 9ebf89bde2 changes
2026-05-16 13:18:52 -04:00

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_periodsPeriod 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 headerline_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