Journal Vouchers & General Ledger
v0.1.0Double-entry journal drafting, zero-sum minor unit validation, Maker-Checker segregation, GL postings, and immutable Storno reversals.
Journal Vouchers & General Ledger
The General Ledger represents the financial truth of an organization. In Obelawium Accounting, all ledger mutations take the form of balanced Journal Vouchers managed through ium()->accounting()->journalEntries() and reported through ium()->accounting()->ledger().
1. The Voucher Lifecycle
A journal voucher advances through an explicit state machine:
stateDiagram-v2
[*] --> DRAFT: createDraft()
DRAFT --> SUBMITTED: submit(actorId)
SUBMITTED --> APPROVED: approve(approverId)
SUBMITTED --> REJECTED: reject(approverId, reason)
REJECTED --> DRAFT: revise()
APPROVED --> POSTED: post(actorId)
POSTED --> REVERSED: reverse(actorId, reason)
REVERSED --> [*]
2. Drafting a Balanced Journal Voucher
Vouchers can be defined using minor units (integers) or major units (decimals) via CreateJournalEntryDto.
Using Decimal Major Units (fromMajor)
use Obelaw\Ium\Accounting\Data\CreateJournalEntryDto;
use Obelaw\Ium\Accounting\Enums\JournalEntryType;
$dto = CreateJournalEntryDto::fromMajor([
'entry_date' => '2026-03-31',
'entry_type' => JournalEntryType::Standard,
'currency_code' => 'USD',
'memo' => 'Office Equipment Purchase via Bank Wire',
'lines' => [
[
'account_code' => '1510', // Office Equipment (Leaf Asset)
'side' => 'debit',
'amount' => 4250.00,
'cost_center_code' => 'CC-OPS',
'memo' => 'MacBook Pro workstations',
],
[
'account_code' => '1111', // Bank Operating (Leaf Asset)
'side' => 'credit',
'amount' => 4250.00,
'memo' => 'Wire Transfer #9021',
],
],
]);
// Persists the voucher with status = DRAFT
$voucher = ium()->accounting()->journalEntries()->createDraft($dto);
The Zero-Sum Check
When saving or submitting, the system asserts:
$$\sum \text{Debits (minor)} = \sum \text{Credits (minor)}$$
If debits and credits do not match to the exact integer minor unit, the method halts immediately with an UnbalancedJournalEntryException.
3. Maker-Checker Segregation of Duties
To protect organizations from unilateral or fraudulent ledger disbursements, ium-accounting features built-in Maker-Checker governance:
// Step 1: Preparer / Accountant submits the draft
ium()->accounting()->journalEntries()->submit($voucher->id, $preparerId);
// Step 2: Financial Controller / Auditor reviews and approves
// If $controllerId === $voucher->creator_id, throws MakerCheckerViolationException!
ium()->accounting()->journalEntries()->approve($voucher->id, $controllerId);
// Step 3: Posting to the General Ledger
$postedVoucher = ium()->accounting()->journalEntries()->post($voucher->id, $controllerId);
Strict Governance Setting: Set
config('ium.accounting.maker_checker.strict')tofalseonly in local development or single-user environments.
4. General Ledger Postings & Immutability
When a voucher is posted:
JournalEntrystatus transitions permanently toPOSTED.- Individual
GeneralLedgerPostingrows are written for each line item. - Amounts are converted into the functional base currency: $$amount_functional = \text{round}(amount \times exchange_rate)$$
- Eloquent model hooks lock the voucher. Any subsequent in-place SQL
UPDATEorDELETEthrows anImmutableJournalEntryException.
5. Correcting Errors: The Storno Reversal Pattern
GAAP and IFRS standards forbid deleting or modifying posted financial records. Corrections must always generate an equal-and-opposite Storno Reversal:
$reversalVoucher = ium()->accounting()->journalEntries()->reverse(
entryId: $postedVoucher->id,
actorId: $controllerId,
reason: 'Incorrect asset account selected on original purchase wire'
);
What Happens During a Storno Reversal?
- The original entry status becomes
REVERSED. - A new journal entry is created with status
POSTEDandentry_type = JournalEntryType::Reversal. - The new entry contains identical accounts and amounts, but with inverted sides: original debits become credits, and original credits become debits.
- The new entry links back via
reversed_entry_id = $original->id. - The forensic audit log records both actors, timestamps, and justification.
6. Real-Time Financial Statements
Query the General Ledger via ium()->accounting()->ledger():
Trial Balance
$tb = ium()->accounting()->ledger()->generateTrialBalance($periodId = null);
// Verification check
if ($tb['is_balanced'] === true) {
echo "Trial balance verified: Debits = {$tb['total_debit_major']}, Credits = {$tb['total_credit_major']}\n";
}
Balance Sheet
$balanceSheet = ium()->accounting()->ledger()->generateBalanceSheet($asOfDate = '2026-03-31');
dump($balanceSheet['assets']);
dump($balanceSheet['liabilities']);
dump($balanceSheet['equity']);
dump($balanceSheet['discrepancy']); // Guaranteed 0
Income Statement (Profit & Loss)
$pnl = ium()->accounting()->ledger()->generateIncomeStatement($periodId);
echo "Revenues: {$pnl['total_revenue_major']}\n";
echo "Expenses: {$pnl['total_expense_major']}\n";
echo "Net Income: {$pnl['net_income_major']}\n";