Skip to content

Journals

A journal entry is a voucher: a header plus balanced lines. This page covers the manual entries staff write, how they are approved, and how a wrong one is corrected.

Rules: FIN-031 (no edits to posted entries — LAW), FIN-032 (maker-checker), ACC-030 (approval limits).


1. Lifecycle

stateDiagram-v2
  [*] --> pending: written on the website or the phone — always pending
  pending --> approved: approve_journal_entries (someone else)
  pending --> rejected: approve_journal_entries, reason required
  approved --> reversed: post_journal_reversal
  rejected --> [*]

There is no path from the browser or the phone to approved. Editing a row to set the status is refused: "Vouchers are approved through approve_journal_entries, not by editing the row (FIN-032)."

1a. Writing a voucher on the phone

Finance staff write a voucher from the app: More → Finance → New entry (also on Home). The phone calls one database function, create_manual_journal, which writes the header and the lines in one transaction (FIN-032).

The phone (create_manual_journal) The website (POST /finance/journals)
Permission finance.create finance.create
Status always pending, the caller as maker always pending
Types journal (JV), payment (PV), receipt (RV), contra (CT) — prefixes from fin_voucher_prefix one manual journal (JV); receipts, payments and contras have their own dialogs
Date not in the future, not in a locked or closed period not in a locked or closed period
Narration required optional
Lines 2 to 50; each an active account that is not a group; a debit or a credit; paise 2 or more; active, not a group
Balance exact, to the paisa within ₹0.01
Foreign currency refused — enter it on the website per-line currency and rate (FIN-034)
Sent twice the same voucher within ten minutes returns the first no guard
Audit finance.voucher.create, with the door finance.voucher.create

A payment must credit a cash or bank account (an account under Liquid Assets, or the cash or bank account in finance settings); a receipt must debit one; a contra touches only those. Every type is stored as manual_journal, so the manual-journal approval limit applies to all four (ACC-030).

The account picker searches the chart by code or name and offers only accounts that can take an entry. A party ledger is found by its party code (BP-0012, CU-000123, SU-0005), because its name carries it (PTY-004).

A bill or photo is attached after the voucher is saved: drive-upload (kind voucher_attachment, finance.edit), then a JournalAttachment row. Without finance.edit the voucher is saved without a bill.

On the phone a voucher also opens with its lines and files; a person who may decide it approves or rejects it there (approve_journal_entries), and a posted hand-made voucher can be reversed with a reason (post_journal_reversal, pending). Vouchers posted by a booking, a receipt or a supplier record are reversed on the website, which also updates that record.

Not built on the phone: foreign-currency lines, recurring templates, a party reference on a line other than the party's own ledger, bulk approval. The website's create route has not been moved onto create_manual_journal; it still writes with two inserts, kept pending by the guards.

2. Approving

approve_journal_entries(ids, decision, reason) — finance.journals.approve; more than one id also needs finance.journals.bulk_approve; rejecting needs finance.journals.reject or .approve, plus a reason.

Before a voucher is approved the database checks:

  • the approver did not create it (unless they hold finance.journals.approve_own, which only the super admin does);
  • the lines balance — at least two lines, positive total debit, debit and credit within ₹0.01;
  • the entry date is in an open accounting period;
  • for a voucher a person wrote — a journal, a contra, an on-account receipt, or a reversal on custom lines — the amount is within the approver's approval limit — manual_journal, ₹25,000 for a Finance Manager, above that finance.approvals.high_value;
  • a reversal is never approved before the voucher it reverses, and never by the person who made the voucher it reverses (ACC-020).

Rows that fail come back in skipped with a reason; the rest are approved. Originals are ordered ahead of their reversals on approve, and the reverse on reject.

When one voucher is approved or rejected on the website and the database skips it, the approver is told why (src/lib/finance/journalSkipMessage.ts; the phone uses the same words):

Skip reason What the approver sees
unbalanced "The voucher's debits and credits don't match — it can't be approved until it's corrected."
above_approval_limit The limit message with the amounts in rupees, e.g. "Manual journal / adjustment of ₹30,000 is above the ₹25,000 limit for you (Finance Manager): it needs CEO / GM"
original_not_approved The voucher reverses one that is not approved yet; approve the original first.
maker_checker_self_approval You cannot approve (or reject) your own journal entry; ask a different approver.
period_locked The voucher is dated in a locked accounting period.
not_found The voucher was not found.
refused The database's own message.
already_approved, already_rejected "This journal has already been processed by another user."

Only a voucher someone else already decided says "already processed".

Not approvals.approve

Journal approval used to be gated on the generic approvals.approve, which sales and operations managers hold. It is now finance.journals.approve, and ops and sales managers were revoked from it.

3. Reversing

post_journal_reversal is the only way to reverse. The old POST /finance/journals/:id/reverse behaviour — a free-standing inverse voucher — is gone. What it guarantees now:

  • Every reversal names the voucher it reverses. reversalOfEntryId is mandatory; a reversal without one is refused.
  • A reversal follows its original. Reversing a pending voucher produces a pending reversal, which cannot be approved before the original. Rejecting a voucher rejects its pending reversals.
  • Reversals can never exceed the voucher. Checked under an advisory lock, so partial reversals are allowed and repeated cancellation reversals cannot stack up past the original amount.
  • A reversal of a reversal is refused, and a rejected voucher cannot be reversed.
  • It posts approved only for a caller who may approve and did not make the original. Otherwise it posts pending and waits for someone else.

Corrections after the reversal are new entries — for supplier transactions, correct_supplier_transaction (finance.supplier_transactions.correct, reason required, audited).

4. Opening balances are vouchers

set_account_opening_balance(accountId, amount, reason, asOf, note) posts an OB- voucher pending, so a second person approves it, with the equity side on account code 3000. Restating a balance reverses the previous voucher first (or rejects it, if it was still pending). Account.openingBalance is a cache, not the source — it cannot be typed in from the browser, and finance_opening_balance_drift() reports accounts where the cache and the voucher disagree (AUD-010).

This also fixed a double count: reports used to add Account.openingBalance on top of the opening-balance voucher.

5. System vouchers

Most vouchers are not written by hand. They are posted by the function that performs the business event, in the same transaction, so there is no operation without its entry:

Event Posted by
A verified receipt verify_payment
An approved refund approve_refund
A B2B cancellation decision post_decision_journal — only the person who just decided it, once per reference
An airline cancellation filing file_airline_cancellation

post_decision_journal refuses any reference type outside the three B2B cancellation kinds: "Voucher type X cannot be posted without approval; post it pending instead."

5a. The register

Finance → Journal lists every voucher one server page at a time, newest first (GET /finance/journals?paged=1), with a search over the voucher number, memo and source, a status filter (waiting for approval, approved, rejected) and a Voucher date range on entryDate — Indian calendar days, both ends included. The paging bar under the register gives rows per page (25, 50 or 100), "1–25 of N vouchers" and first, previous, next and last page. It used to show the newest 500 vouchers with nothing to say older ones existed (PLT-050, UX-030).

5b. The Transactions feed

Finance → Transactions is one list of every receipt, invoice, ledger entry and journal voucher, built and paged by the server (GET /finance/transactions, finance_transactions_page). A receipt opens its voucher (the payment's newest payment journal); a ledger entry opens its journalEntryId; a voucher is listed on its own only when neither a receipt nor a ledger entry already shows it, so nothing is counted twice. The search (reference, booking number, memo, a voucher's line narrations, receipt number, method, source), type, status, a Date range (Indian days, both ends included) and the order (date, amount, type, status) run in SQL. The paging bar under the feed gives rows per page (25, 50 or 100), "1–25 of N transactions" and first, previous, next and last page.

It used to merge the registers in the browser from the newest 500 vouchers, so an older voucher was in no feed. A status filter now keeps only rows with that status — a ledger entry has none, so it is not listed under Pending; it used to pass every status filter (PLT-050, UX-030).

6. Routes

Route Method Permission
/finance/journals GET finance.view
/finance/journals POST finance.create — always lands pending
create_manual_journal (RPC, the phone) — finance.create — always lands pending
/finance/journals/pending GET finance.view
/finance/journals/:id GET finance.view
/finance/journals/:id/approve, /reject POST finance.journals.approve / .reject
/finance/journals/approve-bulk POST finance.journals.bulk_approve
/finance/journals/:id/reverse POST finance.journals.reverse or finance.create

The journal list (unfiltered), the approval queue and one voucher are each one request to the database (finance_screen, finance_journal_screen), which checks finance.view itself (PRF-010). A filtered list (?status=, ?accountId=, …) reads as before.

7. Where to look

Concern Path
Guards, reversal rules, decision journals supabase/migrations/20260919100000_finance_journal_controls.sql
Opening balances supabase/migrations/20260919100600_finance_opening_balances_reports.sql
Approval limits supabase/migrations/20260919100700_finance_approval_limits.sql
Supplier corrections supabase/migrations/20260919100400_finance_supplier_tds_controls.sql
A voucher from the phone supabase/migrations/20261001160000_a_journal_from_any_door.sql
Tests supabase/tests/finance_controls.sql, supabase/tests/a_journal_from_any_door.sql