Accounting core¶
The accounting module is the heart of SAE Books. Every other module (bank feeds, GST/BAS, fixed assets, payroll) ultimately produces or consumes journals against the general ledger defined here.
Chart of accounts¶
A hierarchical list of accounts keyed by code (e.g. 1-1000,
1-1000-01). Eight account types (the AccountType enum):
ASSETLIABILITYEQUITYINCOMEOTHER_INCOMEEXPENSECOST_OF_SALESOTHER_EXPENSE
Each account carries a tenant_id, a company_id, an is_header flag
(non-posting header rows), an archived_at timestamp (soft-delete), an
optional parent_id (to form the tree), and a tax_code_default that
informs the default GST code when a line is posted against the account.
Bank-side accounts additionally carry ABA/Direct Entry fields (bsb,
bank_account_number, bank_account_title, apca_user_id,
bank_abbreviation) and an account_kind enum
(BANK_CHECKING, BANK_SAVINGS, CREDIT_CARD, BANK_LOAN, CASH,
OTHER) so credit cards, loans, and cash all appear in the bank-account
list and on the dashboard. Credit-limit accounts carry credit_limit
and credit_limit_kind (soft warns when exceeded; hard is
enforced). Trust bank accounts are marked via is_trust_account (NSW
Property Act commingling guard).
Codes are advisory — they help humans group accounts and drive default report ordering, but the system identifies accounts by UUID. Renaming or recoding an account never breaks history.
Journals & lines¶
The atomic unit of bookkeeping is a journal with two or more
journal lines. Each line is a debit or a credit against one
account, with an amount in the journal currency. The journal is
balanced (SUM(debits) = SUM(credits)) at post time — an unbalanced
journal is rejected.
invoice / bill / payment] --> Journal Journal --> Lines Lines --> Posting[(journal_lines)]
Posting writes the lines to the journal_lines table (with status
POSTED) and the parent JournalEntry is stamped posted_at. Reports
query journal_lines joined to journal_entries filtered by status; only
POSTED and REVERSED entries appear in financial reports (draft entries
are excluded).
Source documents and provenance¶
Every journal entry carries an origin (a JournalOrigin enum stored
as a plain string) that declares what created it, plus source_type and
source_id pointing back to the originating record. This provenance
keystone makes a hand-authored manual entry a visible exception
(MANUAL) rather than indistinguishable from an engine-posted one.
Declared origins (the full JournalOrigin enum): MANUAL, UNKNOWN,
INVOICE, CREDIT_NOTE, BILL, PAYMENT, EXPENSE, PAYRUN,
DEPRECIATION, FX_REVAL, DEFERRED_REVENUE, BANK_REC,
YEAR_END_CLOSE, TRUST_DISTRIBUTION, CASHBOOK_BACKFILL,
FIXED_ASSET, REVERSAL, INTERCOMPANY, TRANSFER,
SUPPLIER_CREDIT_NOTE, RECEIPT, RECLASSIFICATION,
BAD_DEBT_WRITEOFF, and BAD_DEBT_RECOVERY. UNKNOWN is the DB
column default for pre-provenance rows (no backfill); MANUAL is the
posting chokepoint default when no machine origin is declared.
Journal entries also support optional flagged_for_review and
review_note fields that let a reviewer mark a transaction for
follow-up without changing its posting state.
Reversal vs. hard-delete¶
- Reversal posts an opposite journal that nets to zero. The original journal stays in history, and both rows are visible on audit reports. This is the correct path for correcting posted activity in a period that has been reported on.
- Hard-delete removes the journal and its ledger rows. Admin-only, audited, used for bona-fide errors that should not appear in any report (test data accidentally posted to live, duplicate import runs, etc.). Hard-deleting a journal that crosses a closed period requires reopening that period first.
Periods¶
Period locking is implemented via the period_locks table (PeriodLock
model). A lock has a locked_through date; the engine rejects any
post dated on or before that date unless the lock is first removed.
The lock row itself carries locked_by (the actor's user ID) and an
optional reason field. Unlock is performed by deleting the PeriodLock
row; no separate audit-log entry is written for the lock or unlock — the
period_locks table is the record.
Currency¶
AUD is the default reporting currency. Foreign-currency transactions are
stored in their original currency and converted to AUD at the exchange
rate on the document date. Unrealised FX gains/losses on open AR/AP are
computed on revaluation runs (origin FX_REVAL) and posted as their
own journals — see saebooks.services.fx.reval.
Reporting¶
The standard reports under /api/v1/reports/:
| Report | Endpoint |
|---|---|
| Trial balance | GET /reports/trial_balance |
| Profit & Loss | GET /reports/profit_loss |
| Balance sheet | GET /reports/balance_sheet |
| Aged receivables | GET /reports/aged_receivables |
| Aged payables | GET /reports/aged_payables |
| Cash flow statement | GET /reports/cashflow |
| BAS summary | GET /reports/bas_summary |
| Depreciation schedule | GET /reports/depreciation_schedule |
| FX revaluation | GET /reports/fx_revaluation |
| Budget vs actual | GET /reports/budget_vs_actual |
| P&L by segment | GET /reports/pl_by_segment |
| Revenue by customer | GET /reports/revenue_by_customer |
| YTD turnover | GET /reports/ytd_turnover |
Note: endpoint names use underscores, not hyphens. Reports are computed on demand; there is no nightly aggregation step to break.