Skip to content

Journal Entry Guide

Journal Entry β†’ Bookkeeper: Full Walkthrough & Demo Guide

Section titled β€œJournal Entry β†’ Bookkeeper: Full Walkthrough & Demo Guide”

What Actually Happens When You Click β€œSubmit”

Section titled β€œWhat Actually Happens When You Click β€œSubmit””

Here is the precise chain of events, from browser click to committed GL entry:

[Browser]
└─ POST /api/v1/workflow/JournalEntry/{uuid}/transition { action: "Submit" }
β”‚
[workflow_router.py]
└─ Loads JournalEntry from DB via GenericRepository
└─ Sets doc.docstatus = DocStatus.SUBMITTED
└─ Calls repo.save(session, doc)
β”‚
[GenericRepository.save()]
└─ Detects docstatus changed β†’ SUBMITTED
└─ Instantiates JournalEntryController(doc)
└─ Calls controller.on_submit()
β”‚
[JournalEntryController.on_submit()]
β”œβ”€ Validates: total_debit > 0, β‰₯ 2 legs
β”œβ”€ Validates: posting date is within an open AccountingPeriod
β”œβ”€ Builds GL payload (legs, company, narration, date)
β”œβ”€ Creates OutboxEntry ← written to "outboxentry" table
β”œβ”€ Sets doc.status = "Pending GL" ← visible in UI
└─ Updates BudgetUtilization for each debit line
β”‚
[get_session() auto-commits the transaction]
└─ journalentry row: docstatus="1", status="Pending GL"
└─ outboxentry row: operation="post_journal_entry", status="Pending"
β”‚
[relay_outbox worker β€” runs separately]
└─ Scans outboxentry table for status="Pending"
└─ POSTs to Book-Keeper: /journal-entries/compound
└─ Book-Keeper returns: { transfer_id: "compound_entry_xxxx", status: "posted" }
└─ Updates OutboxEntry.status = "Sent"
└─ Updates JournalEntry.gl_batch_id = "compound_entry_xxxx"

After clicking Submit, the action handler receives:

{
"success": true,
"old_state": "Draft",
"new_state": "Submitted",
"action": "Submit",
"message": "Transitioned from Draft to Submitted"
}

The form should reload and status should now show β€œPending GL”.


Connect to the finance database and verify:

Terminal window
# Connect to postgres
psql postgresql://postgres:secret@localhost:5432/postgres
-- Confirm docstatus and status using the internal name identifier (e.g. JOUR-XXXXXXXX)
SELECT name, entry_number, status, docstatus, gl_batch_id
FROM journalentry
WHERE name = 'JOUR-53CBEC53';
-- Expected:
-- status = "Pending GL"
-- docstatus = "1" (SUBMITTED)
-- gl_batch_id = NULL (not yet relayed)

-- See the pending outbox entry (reference_id holds the entry_number)
SELECT id, operation, status, reference_id, error, retry_count
FROM outboxentry
WHERE reference_doctype = 'JournalEntry'
ORDER BY created_at DESC
LIMIT 5;
-- Expected:
-- operation = "post_journal_entry"
-- status = "Pending"
-- reference_id = "0017" (or your equivalent entry_number)

The relay worker is the bridge to Book-Keeper. In development, it automatically runs alongside your server if you start it with the worker flag:

Terminal window
uv run m dev --env-file monolith.env --enable-worker

If the worker is running, it will automatically pick up the pending outbox entry within a few seconds and log the success to the console.


After relay, check that gl_batch_id is now populated and outbox status is β€œSent”:

-- Outbox should now be Sent
SELECT operation, status, error
FROM outboxentry
WHERE reference_id = '0017'; -- using the entry_number
-- Expected: status = "Sent"
-- JournalEntry should now have gl_batch_id
SELECT name, entry_number, status, gl_batch_id
FROM journalentry
WHERE name = 'JOUR-53CBEC53';
-- Expected:
-- status = "Submitted" (updated by relay after GL confirmation)
-- gl_batch_id = "compound_entry_xxxxxxxx"

Step 6 β€” Query Book-Keeper directly (Event Sourcing)

Section titled β€œStep 6 β€” Query Book-Keeper directly (Event Sourcing)”

Because Book-Keeper uses an Event Sourced Architecture, there are no static journal_entries or gl_entries tables. Instead, all changes are written as immutable events!

Verify the event in the Book-Keeper database:

Terminal window
psql postgresql://postgres:secret@localhost:5432/book_keeper
-- The gl_batch_id corresponds directly to the stream_id in the events table
SELECT tenant_id, stream_id, event_type, data, timestamp
FROM events
WHERE stream_id = 'compound_entry_c29b04e5'; -- use your actual gl_batch_id

What you will see in the data JSON blob: The JSON data payload will contain the exact debits, credits, and the Book-Keeper internal transfer_id (e.g. 26) which maps to the internal journal_transactions table.


Finance Module (Business-M) Book-Keeper Service
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Journal Entry (Draft) β”‚ β”‚ β”‚
β”‚ ↓ Submit β”‚ β”‚ Events (Ledger) β”‚
β”‚ Journal Entry β”‚ relay β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ (Pending GL) + Outbox──┼──worker──▢│ β”‚ stream_id β”‚ β”‚
β”‚ ↓ β”‚ β”‚ β”‚ compound_xxβ”‚ β”‚
β”‚ Journal Entry │◀─batch_id─│ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ (Submitted, gl_batch) β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
PostgreSQL PostgreSQL
(postgres DB) (book_keeper DB)

Key points:

  • Journal entries are immutable after submission (docstatus=1 prevents edits)
  • The Outbox pattern ensures GL posting is reliable even if Book-Keeper is temporarily offline
  • If relay fails, entries retry automatically (up to MAX_RETRIES) before going Dead
  • Every transition is auditable β€” creation, submission, GL batch ID all stored
  • Budget utilization is tracked in real-time against budget allocations per cost center

docstatus status What it means
0 Draft Being drafted, editable
1 Pending GL Submitted, waiting for GL relay
1 Submitted Relay complete, GL batch confirmed
2 Cancelled Reversed in both Finance and Book-Keeper