Reversing and correcting
You posted something. You realise there's a mistake. This runbook walks you through the four most common situations and tells you the exact tool to use — reversal, reject-and-recreate, edit, or reopen-the-period.
Get this wrong and you either double-count the mistake or blow a hole in the audit trail. Get it right and the original posting stays on the record with a clean reversing entry next to it.
The decision tree
flowchart TD
Start[I posted something wrong] --> Q1{Is it still pending<br/>approval?}
Q1 -->|Yes| Reject[Ask the approver to <b>Reject</b><br/>then repost a fresh voucher]
Q1 -->|No, already approved| Q2{Is the period<br/>still open?}
Q2 -->|No, period locked/closed| Reopen[Ask GM / Finance Manager<br/>to <b>Reopen</b> the period first]
Q2 -->|Yes| Q3{Wrong amount only<br/>OR wrong accounts?}
Q3 -->|Wrong amount| Reverse1[Use <b>Reverse</b>,<br/>then post fresh with correct amount]
Q3 -->|Wrong accounts| Reverse2[Use <b>Reverse</b>,<br/>then post fresh with correct accounts]
Q3 -->|Tiny typo in memo only| Memo[Live with it — memo is not<br/>editable post-approval.<br/>Document in the reversal memo if you must.]
Reopen --> Q3
Reverse1 --> Verify
Reverse2 --> Verify
Reject --> Verify
Verify[Verify: Trial Balance rebalances,<br/>audit log carries both entries]
Scenario 1 — Wrong amount on a posted journal
You already got the voucher approved. The amount on it is wrong.
What to do — reversal
Reversal creates a contra-entry that cancels the original by flipping
every debit with its credit. The original voucher stays on the books
(audit trail preserved); the reversal sits next to it with
isReversal = true.
Steps
- Go to Finance → Journal tab.
- Find the wrong voucher — newest first, search by voucher number if you have it.
- Click the Reverse button on the right-hand side of the row.
- Confirm the dialog. The reversal posts immediately with a
REV-nnnnvoucher number. - Post a fresh voucher with the correct amount. Follow the appropriate entry in Recording transactions.
What the system does
- Calls
POST /finance/journals/:id/reverse(seesrc/lib/api.ts:21878). - Pre-checks that the original is
status='approved'(pending entries cannot be reversed — see warning below). - Pre-checks there is no existing reversal for this entry (you can't reverse something twice).
- Creates a new
JournalEntrywithisReversal = true,reversalOfEntryIdpointing at the original, and every line's debit/credit swapped. - Writes an audit log row
finance.voucher.reverseagainst the original entry id with the reversal voucher number attached.
Permissions you need
finance.journals.reverse(held by ACCOUNTANT, FINANCE_MANAGER, and super-admins).- The system also requires: the person reversing must not be the original creator. This is the maker-checker rule.
Maker-checker rule for reversals
You cannot reverse your own journal
If you created the entry, you cannot reverse it — the system will
throw Maker-checker: you cannot reverse a journal entry you
created. Ask a different approver. Hand the job to a colleague.
Only super-admins (CEO / GM / IT_ADMIN) hold
finance.journals.reverse_own and can override this.
When NOT to use reversal
Period is locked or closed
If the target period is locked (amber) or closed (red), the
reversal will be rejected by both the API and the DB trigger
(enforce_accounting_period_lock — see
supabase/migrations/20260418150000_journal_period_lock_trigger.sql).
Your options:
- Date the reversal in today's (open) period — acceptable for correcting errors you catch after period-close.
- Ask GM / Finance Manager to reopen the period — audit-log disclosable; use only when the mistake materially misstates prior-period balances.
Already-reconciled bank entries
If the original journal was already matched to a bank statement line, reversing it leaves the bank statement line orphaned. Un-match the bank line first (Finance → Reconcile → find the line → Unmatch), then reverse the journal. Re-match after you post the corrected voucher.
Verify
- [ ] Journal tab now shows two rows: the original (voucher no.
xxxxxx) and the reversal (REV-yyyyyy) with a "Reversal ofxxxxxx" caption. - [ ] Trial Balance: balances are back to where they were before the wrong voucher.
- [ ] After your fresh, correct voucher: Trial Balance now shows the correct amounts.
- [ ] Audit log carries three rows: create (original), reverse, create
(fresh). Admin → Audit Log, filter by
journal_entry.
Scenario 2 — Wrong account
Example: You posted a supplier bill to 5101 Hotel Purchases but it
should have been 5102 Flight Purchases.
What to do — same as Scenario 1 (reverse + re-post)
Reversal is still the right tool. Don't try to "edit" the voucher — no UI allows editing a posted voucher's account codes, and for good reason.
Decision — reversal vs delete-and-re-enter
In this codebase you cannot delete an approved journal. The only ways out are reversal or, if the entry is still pending, rejection.
If the entry is pending: skip to Scenario 3.
If the entry is approved: follow Scenario 1 exactly, then post a fresh voucher with the correct account codes.
Scenario 3 — Journal stuck in pending state
An auto-posted journal (e.g. from a booking, a GST split, a commission calculation) or a manual journal you created is pending and nobody has approved it yet.
Your options
| Who you are | What you can do |
|---|---|
| Original creator | Nothing — you cannot approve or reject your own. Ask a colleague. |
Different user with approvals.approve |
Open Finance → Approvals tab, find the row, click Approve or Reject. |
| Super-admin (CEO / GM / IT_ADMIN) | Same, and can additionally override the maker-checker self-approval guard. |
To unstick by rejecting + recreating
- Ask the approver to open Finance → Approvals tab.
- They find the pending row, click View to confirm the contents are wrong.
- They click Reject. The row flips to
rejectedand leaves the approvals queue. - You (or anyone with
finance.create) go to Finance → Vouchers (or Journal tab → New Journal Entry) and post a fresh, correct voucher.
To unstick by approving
Only if the entry is actually correct and someone just forgot to approve. Approver opens Finance → Approvals → Approve.
Bulk approvals
If there are many stuck journals that are all correct (e.g. 30 pending
booking revenue entries for yesterday's sales), the Approve All
button at the top of the Journal Approvals panel in the Approvals tab
runs POST /finance/journals/approve-bulk. Notes:
- Any journal you created is silently skipped (maker-checker); the
result card shows
skippedIdswith reasonmaker_checker_self_approval. - Any journal that was already approved / rejected by someone else in
the interim is skipped with reason
concurrent_transition— no error.
Bulk approval is irreversible-ish
Approving a batch posts them all at once. If one of them is wrong, you can still reverse it individually, but you cannot "unapprove" by retrospectively flipping back to pending. Review the batch with the View dialog before you click Approve All.
Scenario 4 — Journal rejected by period close
You tried to post a voucher dated inside a closed or locked period. The API rejected it with a message like:
Why this happens
Two defences protect closed periods:
- The API handler (
assertAccountingPeriodAllowsPosting()insrc/lib/api.ts) rejects writes with a 400 error. - A DB trigger (
journal_entry_period_lock—supabase/migrations/20260418150000_journal_period_lock_trigger.sql) catches any writer that bypasses the API (scripts, direct DB access, other Edge Functions).
Together these guarantee no row can land inside a locked or closed period via any path.
Procedure to reopen a period
Reopening is an audit-log event
Reopening a period is visible to auditors. Every reopen-close pair
is tracked by the lockedAt, lockedBy, closedAt, closedBy
columns on the AccountingPeriod row (plus the audit log). Do not
reopen a period for a trivial correction — post the fix in the
current open period and leave a note. Reopen only when the
prior-period balance is materially wrong.
Who can reopen:
- Accounting period is locked → anyone with
finance.edit(ACCOUNTANT and above) can unlock. - Accounting period is closed → nobody. Closed is permanent.
The API returns
Closed periods cannot be reopened. You would need to have the sysadmin restore from backup, or work around by posting the correction in the current period.
Steps — unlocking a locked period
- Finance → Settings tab.
- Scroll to Accounting Periods.
- Find the period row. Status chip should read
Locked. - Click the Reopen button (shows a
LockOpenicon). - A confirmation dialog appears. Read it. Click Continue.
- The period status flips back to
Open. - Go back and post your correction.
- When you're done, close the period again following the month-end close workflow.
What if you really need to undo a closed period?
You can't, by design. The work-around:
- Post the correction dated in today's open period.
- Add a memo that explains it's a correction for the closed period,
e.g.
Correction: Hotel Al Haram invoice INV-2326 miscoded to 5101 — moved to 5102. Original posted 2026-04-05 in FY26 Q1 (closed). - The audit trail now tells the story. Historical Trial Balance for the closed period remains wrong (by a small amount, in opposite accounts that net to zero); today's books are correct.
Lock, don't close, until the audit is signed off
Standard practice here: lock a month at close, but leave it not-closed until quarterly review is signed off by the CEO. Locking blocks new postings but stays reversible. Closing is one-way.
Appendix — the state machine
stateDiagram-v2
[*] --> pending: Composer posts voucher
pending --> approved: Finance → Approvals → Approve
pending --> rejected: Finance → Approvals → Reject
approved --> reversed: Finance → Journal → Reverse
pending --> [*]: Never affects balances
rejected --> [*]: Terminal — create a fresh voucher
reversed --> [*]: Reversal voucher sits alongside original
- pending entries never hit balances. Safe to reject.
- approved entries are on the books. Must be reversed, not edited.
- rejected is terminal — you cannot re-approve a rejected entry; create a new one.
- reversed means the original stays, a reversal sits beside it, and the net effect on balances is zero. This is the only way to undo a posted entry.
See also ../journals.md for the reference-level
details.