Skip to content

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

  1. Go to Finance → Journal tab.
  2. Find the wrong voucher — newest first, search by voucher number if you have it.
  3. Click the Reverse button on the right-hand side of the row.
  4. Confirm the dialog. The reversal posts immediately with a REV-nnnn voucher number.
  5. 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 (see src/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 JournalEntry with isReversal = true, reversalOfEntryId pointing at the original, and every line's debit/credit swapped.
  • Writes an audit log row finance.voucher.reverse against 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 of xxxxxx" 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

  1. Ask the approver to open Finance → Approvals tab.
  2. They find the pending row, click View to confirm the contents are wrong.
  3. They click Reject. The row flips to rejected and leaves the approvals queue.
  4. 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 skippedIds with reason maker_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:

Period "FY26 Q1 — Apr-Jun" is closed (entry date 2026-04-05)

Why this happens

Two defences protect closed periods:

  1. The API handler (assertAccountingPeriodAllowsPosting() in src/lib/api.ts) rejects writes with a 400 error.
  2. A DB trigger (journal_entry_period_locksupabase/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 closednobody. 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

  1. Finance → Settings tab.
  2. Scroll to Accounting Periods.
  3. Find the period row. Status chip should read Locked.
  4. Click the Reopen button (shows a LockOpen icon).
  5. A confirmation dialog appears. Read it. Click Continue.
  6. The period status flips back to Open.
  7. Go back and post your correction.
  8. 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:

  1. Post the correction dated in today's open period.
  2. 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).
  3. 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.