Financial Reports
Business view
Once transactions are posted, the module produces the standard set of accounting reports, all computed live from the posted ledger (not cached snapshots that can drift):
- Trial Balance — every account's net balance on its natural side, for a period. If debits don't equal credits company-wide, something's wrong with the posting engine — this is the fastest way to spot it.
- Financial Statements — Income Statement, Balance Sheet, Cash Flow Statement, and Statement of Changes in Equity, in one place with a tab switcher.
- Responsibility Center P&L — revenue and expense broken down by responsibility center (department/location/branch/program), with anything not tagged to a center rolled into an explicit "Unassigned" line so the totals always tie back to the overall income statement.
- Receivables & Payables — who owes the clinic money and who the clinic owes, with aging buckets (30/60/90/120 days by default) split separately for patients, insurers, and suppliers.
- Statement of Account — a single patient, insurer, or supplier's full transaction history with a clinic — the equivalent of a bank statement, for that one party.
Reports can be exported to Excel/CSV from the web app for most report types — but not all of them, and not from the mobile app at all (see the gap noted below).
Technical view
Trial Balance
trialBalance query (packages/server/src/resolvers/queries/ledgerQueries.js:551-589) — one row per postable account with a non-zero net balance, status: POSTED lines only. Screen: mobile trialBalance/TrialBalanceScreen.js, web TrialBalance/TrialBalance.js; date range + branch filter, client-side balanced/out-of-balance check (Math.abs(totalDebit - totalCredit) < 0.01). Permission: VIEW_REPORTS or VIEW_GL.
Financial Statements
Mobile: financialStatements/FinancialStatementsScreen.js; web: FinancialStatements/FinancialStatements.js. Four tabs, all fully implemented (none are stubs):
- Income Statement — driven by the
PlClassification/PlGroupenums onAccount(Setup & Foundations); a nested waterfall: Revenue → groups → Total Revenue → Cost of Revenue → Gross Profit → Operating Expenses → EBITDA → Depreciation → EBIT → Interest → Other Non-Operating → Profit Before Tax → Taxes → Net Profit. - Balance Sheet — collapsible account-hierarchy tree per section (Assets/Liabilities/Equity), server-computed balanced check (
Total Assets == Total Liabilities + Total Equity,packages/server/src/accounting/reports/statements.js:422). - Cash Flow — Operating/Investing/Financing sections plus net change and opening/closing cash, server-computed reconciliation check (
reconciles: netChange.equals(actualChange),statements.js:515). - Equity — opening equity, contributions, net income, movements, closing equity.
Branch filtering is narrower than it looks: on web, only the Balance Sheet tab has a branch filter (FinancialStatements.js — no branch code anywhere in the Income Statement tab); the mobile screen filters both Income Statement and Balance Sheet. Cash Flow and Equity are company-wide on both platforms by design, since cash and equity aren't meaningfully attributable to one branch in this model.
Responsibility Center P&L
Mobile: responsibilityCenterPnl/ResponsibilityCenterPnlScreen.js; web: ResponsibilityCenterPnl/ResponsibilityCenterPnl.js (renamed from "Cost Center P&L" — the rename is complete everywhere, no leftover "CostCenter" strings anywhere in the stack). Both call the responsibilityCenterPnl query → report function in packages/server/src/accounting/reports/responsibilityCenter.js:14-83: revenue is credit-normal (credit − debit), expense is debit-normal (debit − credit), contra-revenue reduces revenue debit-normally; any line without a responsibilityCenterId rolls into an explicit "Unassigned" bucket "so the totals always reconcile to the overall income statement" (the code's own comment). Zero-activity centers are dropped from the output. Permission: VIEW_REPORTS.
Receivables & Statement of Account
The two platforms split AR/AP differently. Mobile receivables/ReceivablesScreen.js has six separate tabs: AR Balances, AP Balances, AR Aging, AP Aging, Accruals, Accounts. Web Receivables/Receivables.js has four tabs — Balances, Aging, Accruals, Accounts — with AR and AP rendered as two side-by-side tables within the Balances tab and within the Aging tab, rather than as separate tabs. Both are backed by the same six queries (arBalances, apBalances, arAging, apAging, payablesAccruals, receivablesPayables, in packages/server/src/resolvers/queries/subledgerQueries.js:159-205). Aging buckets are a client-supplied query argument (buckets: [Int!]), defaulting to a hardcoded [30, 60, 90, 120] (accounting/reports/aging.js:4) when the client omits it — the identically-named AccountingSettings.agingBuckets schema column is never actually read by any resolver, despite existing in the database. Tapping a balance row deep-links into Statement of Account with the entity pre-filled. Statement of Account (mobile: statementOfAccount/StatementOfAccountScreen.js; web: StatementOfAccount/StatementOfAccount.js) supports Patient/Insurer/Supplier/Lab entity types with a date range (or "All dates" — dates are optional and the statement runs unbounded if omitted). Permission: VIEW_REPORTS for the Receivables tabs; VIEW_REPORTS or VIEW_GL for Statement of Account.
Export
exportAccountingReport mutation (packages/server/src/resolvers/mutations/actions/accounting/exportAccountingReport.js, switch statements at lines 44-280 and 297-346) supports 16 report keys: income-statement, balance-sheet, cash-flow, equity, vat-return, gl-tree (General Ledger), account-ledger, payables-accruals, deferredRevenue, pendingInventory, responsibility-center-pnl (renamed from cost-center-pnl), service-line-pnl, provider-pnl, chart-pnl, payer-pnl, doctor-commission. Anything else throws Unsupported report. Export access itself needs VIEW_REPORTS or DO_ALL, except for gl-tree/account-ledger/doctor-commission, which are exportable by anyone who can reach the screen.
Two real gaps to know about before you promise a client "export everything":
- No export case exists for Trial Balance, Statement of Account, or Journal Entries — even a hand-crafted request for these report types fails server-side, not just missing a button.
- Export is a web-only feature. Grepping the mobile app for the export mutation returns nothing — there is no export capability anywhere in the mobile accounting module, for any report. If a user needs an Excel export, they need the web app.