Accounting
Sally's double-entry bookkeeping engine — chart of accounts, ledgers, voucher entry, bill and bank allocations, cost centres, document numbering, concurrency control, period locking, and soft-delete patterns.
Chart of Accounts
The chart of accounts is built on a hierarchical AccountGroup model. Each group has an optional parentId forming a tree structure.
Every account group belongs to one of four natures:
| Nature | Normal Balance | Examples |
|---|---|---|
| ASSETS | Debit | Cash-in-Hand, Bank Accounts, Sundry Debtors, Fixed Assets |
| LIABILITIES | Credit | Sundry Creditors, Loans, Capital Account |
| INCOME | Credit | Sales, Interest Received, Other Income |
| EXPENSES | Debit | Purchases, Rent, Salaries, Depreciation |
System groups (isSystem: true) are pre-seeded during company creation and cannot be deleted. Custom groups can be nested under any existing group.
Ledger Management
Ledgers are the fundamental accounting units. Each ledger belongs to an account group and inherits its nature.
Operations
- Create — name (unique per company), group selection, opening balance + type (debit/credit), optional GSTIN/PAN/address for party ledgers
- Edit — update name, group, opening balance. OCC version check prevents conflicting edits.
- Delete — soft delete only. Ledgers with transactions cannot be permanently removed. Moves to trash with restore option.
Opening Balance
Each ledger carries an openingBalance and openingBalanceType (DEBIT or CREDIT). This represents the starting position when the company is set up or migrated from another system.
Voucher Types
Sally supports eight voucher types organized into two categories:
Directly Entered via Voucher Form
| Type | Purpose |
|---|---|
| Payment | Money going out (cash/bank debit, party/expense credit) |
| Receipt | Money coming in (cash/bank credit, party/income debit) |
| Journal | Non-cash adjustments, provisions, closing entries |
| Contra | Fund transfers between cash and bank accounts |
Created via Invoicing
Sales, Purchase, Credit Note, and Debit Note vouchers are auto-generated when invoices are created. They flow through the Invoicing module rather than the voucher entry form.
Voucher Entry Form
The full voucher form (/vouchers/new) provides multi-line debit/credit entry:
- Multi-line entries — Add unlimited debit and credit rows. Each row has a ledger selector, amount, and optional narration.
- Ledger autocomplete — Searchable dropdown with group filtering. Shows ledger name + group for disambiguation.
- Balance check — Real-time validation ensures total debits equal total credits. The save button is disabled until balanced.
- Date picker — Defaults to today. Triggers period-lock validation on change.
- Narration — Free-text field for transaction description.
- Reference number — Optional external reference (cheque number, UTR, etc.)
Quick Voucher Entry
For the most common scenarios, Sally provides a simplified form that pre-fills the voucher structure:
- Quick Payment — Select who you're paying (party/expense ledger), which account to pay from (cash/bank), and the amount. Sally auto-creates the balanced debit/credit entries.
- Quick Receipt — Select who is paying you, which account receives the funds, and the amount.
Quick forms reduce a 4-field-per-row multi-line entry to a simple 3-field form, ideal for daily cash transactions.
Bill Allocations
Bill allocations track individual receivable/payable references for AR/AP ageing. Every transaction against a party ledger creates or settles a bill.
| Bill Type | Meaning |
|---|---|
| New Ref | Creates a new outstanding bill (e.g., a sales invoice creates a DR bill) |
| Against Ref | Settles an existing bill (e.g., receipt against a specific invoice) |
| Advance | Payment received/made before the bill is raised |
| On Account | General payment without linking to a specific bill |
Outstanding reports and ageing buckets (0–30, 31–60, 61–90, 90+ days) are computed from unadjusted bill records.
Bank Allocations
When a voucher entry involves a bank ledger, a bank allocation record captures instrument-level details:
- Transaction type — NEFT, RTGS, UPI, Cheque/DD, IMPS, or other
- Instrument number — cheque number, UTR, or UPI transaction ID
- Instrument date — date on the instrument (may differ from voucher date for post-dated cheques)
- Bank details — favouring name, bank party name, IFSC, account number
These allocations feed into bank reconciliation — matching imported statement lines against book entries by reference/amount.
Cost Centres
Cost centres provide an orthogonal categorization layer on top of the ledger structure. They are hierarchical (parent/child) and company-scoped.
- Hierarchy — Create nested cost centres (e.g., “North Region” → “Delhi Office” → “Marketing Dept”)
- Entry-level allocation — Each voucher entry can be split across multiple cost centres via
CostCentreAllocationrecords - Amount splitting — A single ₹10,000 expense entry can be allocated: ₹6,000 to “Project A” and ₹4,000 to “Project B”
Document Numbering
Every voucher and invoice gets a unique sequential number, aware of the financial year and configurable in format.
How It Works
- Resolve the FY string from the transaction date (e.g., April 2025 → “2025-26”)
- Atomic UPSERT + INCREMENT on the
DocumentCounterrow keyed by(companyId, kind, fy) - Format using the configured template
Format Template
Default: {prefix}/{fy}/{seq}
Example output: SAL/2025-26/0042
{prefix}— configurable per counter kind (e.g., SAL, PUR, JRN){fy}— financial year string (long “2025-26” or short “25-26”){seq}— zero-padded sequence number
Atomic Counter
The counter uses database-level atomic increment to prevent race conditions. Two simultaneous saves will always get different numbers. Burned numbers (from failed transactions) are acceptable by design.
Optimistic Concurrency Control
Every mutable entity carries a version integer field that increments on each update.
- GET responses include the current version in the response body and
ETagheader - PATCH requests must echo the version via
If-Matchheader or body field - If another user updated the entity between your GET and PATCH, the versions won't match
- Returns 409 Conflict with details: expected vs. current version, entity type, entity ID
This prevents the “last write wins” problem — if two accountants edit the same voucher simultaneously, the second one gets a conflict and must refresh before retrying.
Period Lock Enforcement
Financial periods can be locked to prevent modifications to past data. The assertPeriodOpen(companyId, date) function is called by every mutation endpoint.
- If the transaction date falls within a LOCKED or CLOSED period, the mutation is blocked with a 403 error
- The UI performs a pre-check (
getBlockingPeriod) to warn users before they submit - Only period status OPEN allows new transactions
Soft Delete Pattern
Accounting data is never permanently deleted through normal operations. Instead, entities are “soft deleted”:
deletedAt— timestamp when the item was trashed (null = active)deletedById— user who performed the deletion- All queries include
deletedAt: nullfilter by default - Trash — Settings → Trash shows all soft-deleted items with restore option
- Permanent delete — available from trash after retention period (default 90 days)
This pattern applies to: Ledgers, Vouchers, Invoices, Stock Items, Stock Groups, Stock Journals, Godowns, Units, Batches, and more.