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/asOfareYYYY-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.