Integrating General Ledger with Business Documents

The single most valuable property of an ERP system is this: every business document — a sales delivery, a purchase receipt, a payment — lands in the general ledger without anyone re-keying it. When that pipeline works, the books are always current and month-end closing stops being a firefight. This article describes the document-to-voucher design we use in our web ERP projects.

The core idea: business documents know nothing about accounting

Keep the accounting logic out of business modules. A sales delivery module should emit facts — "100 units of item A left warehouse W to customer C at price P" — and a dedicated posting engine decides what that means for the ledger. This separation, natural in a modular monolith, means accounting rules can evolve (new tax rates, new account mappings) without touching the operational code.

The posting pipeline

  1. Business event — a document is confirmed (posted) in its own module. Confirmation is a state transition, not a save: a confirmed document is frozen.
  2. Posting request — the module publishes a posting request containing the document type, key, amounts and dimensions (org, department, project, currency).
  3. Template resolution — the posting engine resolves a posting template for the document type. A template maps document fields to debit/credit account rules: "delivery of goods → Dr. COGS, Cr. Inventory; receivable → Dr. AR, Cr. Revenue".
  4. Voucher generation — the engine generates a balanced voucher (debits equal credits, enforced as a hard invariant) in the accounting currency, attaching the source document reference to every line.
  5. Ledger write — the voucher posts to the GL in the same transaction as the document confirmation whenever the database allows, or via an outbox pattern when modules are decoupled.

Posting templates as configuration

Never hard-code account numbers. A posting template is a small rule table, usually maintained by the implementation consultant: for each document type and business scenario (warehouse transfer, consignment, return), it declares which accounts are debited and credited, and how amounts are derived from document fields. Templates are versioned and effective-dated, so a tax-rate change starting next quarter is a new template version, not a code deploy.

Keep templates simple — a list of lines with account, direction, amount expression and dimension mapping. The moment templates grow loops and conditionals, consultants cannot maintain them and you have built a second programming language badly.

Idempotency: posting must be safe to repeat

Retries are guaranteed in any web system. The posting engine must be idempotent: posting the same document twice produces one voucher, not two. Two mechanisms make this practical:

  • Natural key on the voucher — a unique constraint on (source_document_type, source_document_id) makes duplicate insertion fail loudly.
  • Outbox pattern — the posting request is written to an outbox table in the same transaction as the document confirmation, then relayed asynchronously. If the relay crashes, it re-reads the outbox; the unique key deduplicates.

Reversals follow the same rule: cancelling a document generates a reversing voucher, never an in-place edit. The ledger is append-only; corrections are new entries.

Period control and closing

Two guard rails keep the ledger sane:

  • Period status — each accounting period is open, soft-closed (no user entries, engine entries allowed) or closed (nothing). The posting engine checks period status before writing.
  • Subledger reconciliation — AR/AP subledger balances and inventory valuation are reconciled against GL control accounts continuously, and any drift is flagged on a dashboard rather than discovered at audit time.

What the user sees

From the user's perspective the experience is simple: confirm a document, then click "view voucher" to see the generated entries, or "posting log" when something went wrong. Never hide failures — a document that is confirmed but failed to post is the worst possible state, so the UI surfaces it immediately and blocks period closing until resolved. For how this plays out in the frontend, see Enterprise Web UI Patterns; the production side of the same pipeline feeds material costing, described in Implementing BOM Explosion and MRP.

Conclusion

Treat the general ledger as a consumer of business events, generate vouchers from declarative templates, make posting idempotent and append-only, and close periods with reconciliation dashboards instead of spreadsheets. Do this and finance stops being the bottleneck of the ERP — it becomes the part that is always right.