Accounting — Process flows
Architecture & lifecycle auto-generated from code (dispatch, CHECK status enums, SET status writes). Sequences & notes are authored.
Architecture
%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
flowchart LR
C["Client"] --> A["Pages Fn accounting<br/>bolt_session auth · company_id scope"]
A --> D["dispatch<br/>void · reopen"]
D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
K --> DB[("D1 tables")]
D --> DB
D --> EV["events<br/>append audit trail"]
EV --> DB
DB -.-> T["accounting_payment<br/>accounting_open_item_settlement<br/>accounting_payment_allocation<br/>accounting_payment_void<br/>accounting_wht_certificate<br/>budget<br/>posting_lock<br/>accounting_fx_reval_run"]
Lifecycle
Kernel document lifecycle (erp_document.docstatus)
Every money/stock-moving document in this module posts through the shared kernel, walking a fixed docstatus:
%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
stateDiagram-v2
[*] --> Draft: createDocument (docstatus 0)
Draft --> Submitted: submitDocument (docstatus 1, journal posted)
Submitted --> Cancelled: cancelDocument (docstatus 2, reversed)
Submitted --> [*]
Cancelled --> [*]
accounting_period.status
States: open (initial) · soft-closed · locked
| status | set by (grepped SET status='…') |
|---|---|
locked |
closePeriod |
open |
reopenPeriod |
accounting_fx_reval_run.status
States: queued (initial) · running · posted · failed · cancelled
accounting_tax_return_export.status
States: preview (initial) · finalized · filed · amended
Key flow: submitPayment (receipt / payment)
One end-to-end call. Note that the GL journal posts through the kernel before the subledger batch; on any batch abort where no payment row committed, the orphaned journal is reversed via cancelDocumentIdempotent.
%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","actorBkg":"#2d2d30","actorBorder":"#569cd6","actorTextColor":"#d4d4d4","signalColor":"#9cdcfe","signalTextColor":"#d4d4d4","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa","labelBoxBkgColor":"#3a3d41","labelBoxBorderColor":"#dcdcaa"},"sequence":{"useMaxWidth":false}}}%%
sequenceDiagram
participant C as Client
participant F as accounting Fn
participant L as PostingLock
participant K as Kernel GL
participant D as Accounting subledger
participant W as WHT
C->>F: POST PAYMENT or RECEIPT with idempotency_key
F->>F: require idempotency_key and functional currency
F->>L: assertPostingOpen posting_date
L-->>F: open or 409 locked
F->>F: aggregate allocations per open item
F->>D: openItemSnapshot check aggregate le open amount
F->>F: build balanced dr cr lines assertBalanced
F->>K: createDocument then submitDocument
K->>K: post erp_journal_entry idempotent on key
K-->>F: submitted doc id and journal entry id
F->>F: validate wht_code read-only (no number drawn)
F->>D: pre-check payment by kernel_document_id
alt already posted replay
D-->>F: prior payment
F->>W: ensureWhtCertificate reuse existing
F-->>C: ok payment idempotent true
else fresh post
F->>D: batch insert payment + allocations + open_item_settlement
alt batch abort
opt concurrent winner already committed
F->>W: ensureWhtCertificate reuse existing
F-->>C: ok payment idempotent true (no journal reversal)
end
F->>K: cancelDocumentIdempotent reverse orphan journal
alt settlement CHECK abort
F-->>C: 409 concurrently settled retry
else other abort cause
F-->>C: rethrow original error
end
else committed
F->>W: ensureWhtCertificate draw gap-free cert_no
F-->>C: ok payment and wht_certificate
end
end
Notes
- Purpose. Kernel-based double-entry accounting for a multi-tenant Thai ERP: journal vouchers, AR/AP settlement (payment/receipt), period close & posting locks, budgeting, WHT certificates, FX unrealized reval, bank statement import + reconciliation, recurring journals, and PP30/PND tax reports. Auth requires capability
finance,accounting, ormanage; every query iscompany_id-scoped. - Kernel-first, subledger-second, atomic. Handlers build balanced GL lines (
assertBalanced), post them viakernel.createDocument+submitDocument(which own theerp_journal_entryand idempotency dedup), then land their own rows in oneenv.DB.batch. If that batch aborts and no payment row committed, the already-posted journal is reversed withcancelDocumentIdempotentso the GL is never left with a journal and no subledger row (round-7 LL-2). Note:submitPaymentemits no domain event on success —emitEventfires only from period close, FX reval, and bank match/unmatch. - Idempotency is mandatory for money moves.
submitPaymenthard-rejects a missingidempotency_key(400) before any DB work — the key is what makessubmitDocumentdedup a network-retry instead of double-posting payment + journal + settlement. FX reval and recurring journals derive deterministic keys (fx-reval:period:runNo,recurring:template_id:run_date) so a crashed run replays the same journal rather than posting a second one. - Append-only ledgers, mutable reversal side-tables.
erp_accounting_payment,payment_allocation,wht_certificate,vat_return_pp30, and the FX reval rows haveBEFORE UPDATE/DELETEtriggers thatRAISE(ABORT). A void therefore never mutates the payment; it inserts into the mutableerp_accounting_payment_voidand re-openserp_accounting_open_item_settlement. The WHT certificate row and its statutory number are always retained (Thai law) even when voided. - Concurrency guards. Settlement over-allocation is serialized by a CHECK'd
open_item_settlementledger (loser's whole batch aborts and its journal is reversed → 409 retry). Single bank-line match is serialized by a mutablereconciliation_activePK anchor (append-only match table stays audit-only);unmatchdeletes the anchor and flips the line back tounreconciledbut does not reverse GL. - Gotchas seen in code.
withTransactionis a dead no-op (guardsenv.db.execbut the binding isenv.DB) — atomicity comes fromenv.DB.batch, not that wrapper. Multi-currency settlement is intentionally refused: a payment currency must equal the functional currency, otherwise it would book foreign numerals as THB. Period close is rejected (409, recorded as aperiod_close_runstaterejected) if any posting-relevant draft exists; reopen expires the posting lock rather than deleting it, because the append-onlyperiod_close_runholds a hard FK toerp_posting_lock.