Skip to content

Finance reports

The catalogue, and the two things that used to make reports wrong.


1. Two corrections that apply to every report

Reports are dated by entryDate, in IST. Every aggregation uses the same expression — COALESCE(JournalEntry."entryDate", createdAt at Asia/Kolkata) — and counts only approved lines. Before Wave 1B some reports dated by createdAt, so a back-dated entry landed in the wrong month and a month-end figure changed depending on when somebody keyed it in.

A report can no longer be cut short. PostgREST returns at most 1,000 rows (max_rows in supabase/config.toml) and says nothing when it truncates. The account statement, account ledger, group ledger, GST summary, AP aging, the ledgerwise settlement report and the bank auto-match candidates are now aggregated in SQL and returned as a single JSON value, which cannot be truncated. Reads that still page do so through fetchAllPages, which throws rather than returning a partial answer:

This read passed 200,000 rows without reaching the end. Refusing to return a partial result — narrow the date range or filters and try again.

The same rule covers exports — see Lists, paging and search and PLT-050.

Two more fixes worth knowing: opening balances are counted once, from their voucher and never from Account.openingBalance on top of it; and the GST summary includes reversals, so GST on a cancellation comes off the output tax instead of being billed twice.

2. The SQL report functions

Each checks the permission of the report it serves, so a report cannot be read by calling the function directly.

Function Serves Permission
finance_account_ledger(accountIds, from, to, includeOpening) account statement, account ledger, group ledger finance.view
finance_account_totals(from, to, groupType) ledgerwise settlement finance.view
finance_gst_summary(from, to) GST summary finance.reports.gst_summary.view
finance_aging_report(asOf) aging summary cards finance.reports.aging.view
finance_pl_summary(from, to) P&L summary finance.reports.profit_loss.view
finance_trial_balance(from, to) trial balance finance.view
finance_day_book(date), finance_day_book_page(...) day book finance.view
finance_group_pnl(groupIds) group P&L finance.reports.group_profit_loss.view
finance_opening_balance_drift() opening-balance drift check (AUD-010) finance.view or admin.audit.view

Two exceptions to the entryDate rule

finance_aging_report dates off the invoice (dueDate / issuedAt) and does not read journal lines at all — an invoice is overdue relative to its own due date, not to a posting date. AR and AP aging detail are bucketed in TypeScript on top of the invoice and supplier rows, and use slightly different buckets from the summary card (current / 1-30 / 31-60 / 61-90 / 90+ versus 0-30 / 31-60 / 61-90 / 90+).

3. Catalogue

All under Finance → Reports. A user with finance.view reaches the tab; each report appears only if the user also holds its permission.

Report Endpoint View permission
Day Book /finance/day-book?date= finance.reports.day_book.view
Trial Balance /finance/trial-balance?asOf= finance.reports.trial_balance.view
Balance Sheet /finance/balance-sheet?asOf= finance.reports.balance_sheet.view
Profit & Loss /finance/profit-loss?from=&to= finance.reports.profit_loss.view
Cash Flow /finance/cash-flow?from=&to= finance.reports.profit_loss.view
Group P&L /finance/group-profitability finance.reports.group_profit_loss.view
Account statement /finance/account-statement finance.view
AR aging /finance/aging finance.reports.aging.view
AP aging /finance/aging-payables finance.reports.aging.view
Aging summary /finance/aging-report?asOf= finance.reports.aging.view
Receivables & Payables /finance/receivables-payables finance.reports.receivables_payables.view
GST Summary /finance/gst-summary?from=&to= finance.reports.gst_summary.view
GSTR-1 / GSTR-3B /finance/gstr-1, /finance/gstr-3b finance.reports.gst_summary.view
Settlements by booking /finance/settlement-report finance.reports.settlements.view
Settlements by ledger /finance/settlement-ledgerwise finance.reports.settlements.view
Pending settlements /finance/pending-settlements finance.reports.settlements.view
Cancellations /finance/cancellation-report?from=&to= finance.reports.settlements.view
Supplier transactions /finance/supplier-transactions finance.view
Stock status inventory-side finance.reports.stock_status.view
Group status inventory-side finance.reports.group_status.view

Export is a separate permission per report (….export). The multi-sheet workbook export goes through src/lib/exportFinanceWorkbook.ts, which walks every page and throws rather than producing a short file.

Permission checks that were missing

Aging, receivables and payables, pending settlements, /finance/ledger, the customer ledger, the P&L summary and inventory P&L were readable by anyone who could reach the page. Each now checks its own permission, in the database as well as the route.

4. Group P&L

Revenue comes from finance_group_pnl — group invoices net of credit notes, active passengers only. A cancelled passenger stops counting as revenue, and the group invoice stops billing them (PAX-032).

Two gaps, both recorded in the rules rather than hidden:

  • A foreign-currency contract with no exchange rate is refused, not counted at 1.0 (FIN-034).
  • There is no per-seat selling price on a passenger's flight row, so the airline gross margin of AIR §25 cannot be attributed per passenger. block_pnl() returns what it can and names what it cannot.

5. Conventions

  • from / to / asOf are YYYY-MM-DD, interpreted in IST.
  • Amounts are INR. The original currency and rate are on each journal line and show in the voucher drill-down.
  • Only approved vouchers are counted. Pending and rejected entries are excluded so an in-flight posting is never double counted.
  • Every number in a report should be traceable to the vouchers behind it (INT-004).

6. How fast they load

The trial balance, aging and balance sheet on the Reports tab come in one request; profit & loss, cash flow and the day book are one request each, the database checking finance.view itself; receivables & payables is added up in the database and sent as one answer instead of every booking and supplier transaction (PRF-010). The partner bookings still also count under the customer they were booked for, as before. The account statement, group profitability and GST summary are unchanged (three to five requests).

7. Where to look

Concern Path
Report functions supabase/migrations/20260919100600_finance_opening_balances_reports.sql
Trial balance, day book, group P&L supabase/migrations/20260918110000_money_integrity.sql
Handlers src/lib/api.ts — handleFinance*
Paging and export ceilings src/lib/paging.ts, src/lib/exportAll.ts
Tests supabase/tests/finance_controls.sql, supabase/tests/finance_paging.sql

Settlements by booking — what the figures are

Billed is what the books bill: price + GST once finance has approved the booking voucher (fin_booking_billed(); 0 while it is pending or rejected) − the credit for approved passenger cancellations. Collected is the booking's paid amount: verified receipts net of refunds, with its share of a receipt split over several bookings. Outstanding is billed − collected, never below 0. A booking whose voucher is still pending therefore bills 0 and reads as settled here, even if the customer has paid something on account; its row says awaitingApproval. The headline totals (finance_settlement_summary) and the rows use the same rule — the rows read the voucher state through the computed field booking_awaiting_approval — and the report refuses to answer if the two disagree. Until 02/10/2026 the rows billed price + GST whatever the voucher state, so they did not add up to the headline while a voucher waited. Until 30/09/2026 a rejected payment counted as collected and billed left out GST and cancellation credits (FIN-033, CXL-002). The group screens show the same paid and balance.