ZuloOne
English
Sign in
Menu

📒 Obligations, payments and the general ledger (the process)

Updated

How the system recognises a debt, what settles it, and which of that reaches the general ledger. Metadata — 02-Accounting. Ledger posting — the process map (§16).

← Business process wiki

The key principle: a payment is a separate document, not a status change#

A debt is recognised by one document and settled by another. Treating "paid" as a subtype of the sales realization itself does not work on this platform.

A subtype transition removes the previous state's movements and posts the target chain. Moving a realization from Released to a paid state would drop the recognised revenue together with the receivable; asking a purchase order to "pay a bit more" would drop the receipt into stock. That is why every obligation register has its own settling document.

Hence the rule: every obligation register has its own settling document, which touches only this register and does not touch the document that created the debt.

Obligation Register Recognised by Settled by
To the supplier Payable PurchaseOrder → Received VendorPayment → Paid
Customer's obligation to us Receivable (customer and contract mandatory) SalesRealization → Issued (Released) CustomerPayment → Paid
To the employee PayrollLiability PayrollAccrual → Posted PayrollPayment → Paid
To the social insurance fund SocialInsurance SocialInsuranceAccrual → Posted SocialInsurancePayment → Paid

All four settling documents are built the same way: draft → Paid (locked subtype), lines with counterparty and amount, and a transaction script that posts the minus across the same analytics where the debt was recognised. The slice must match — otherwise the minus lands in another combination and the debt never closes. On Receivable the slice is customer and contract: a payment without the same SalesContract as the invoice does not net the debt. The contract credit limit and prepaid look at that slice; a limit held only on the customer still sums all of that customer's contracts.

Output VAT is part of the debt on the invoice itself (TaxRateApplied); a credit note removes only that tax part, and a debit note charges additional VAT to the same invoice. In both cases the invoice stays Issued.

How to read open debt#

A register by itself is movements. The layout on the totals tab leaves rows whose closing balance is not zero.

ReceivableByCustomerContract (Sales → Totals → Receivables, name in the list Receivable by customer and contract): groups are customer, then contract. Four amount columns: opening, incoming, outgoing, closing. An invoice 100 and tax 15 give incoming 115. A payment 40 gives outgoing 40 and closing 75 — the row remains. Another payment 75 brings closing to 0, and there is no row. Another contract of the same customer is a different row: a payment without the same contract does not close this slice.

PayableBySupplier (Purchasing → Totals → Payables, name Payable by supplier): one group, supplier. Incoming 100 + input tax 15 gives closing 115 until a payment closes it.

Column captions in the file are empty: the screen sets the kind name in the interface language. The layout does not show which payment closed which invoice: the register slices debt by customer and contract (or by supplier), not by document.

The debt by document lives in the companion registers. ReceivableByBasis (Sales → Registers → Receivable by basis, layout Receivable by customer, contract and basis) and PayableByBasis (Purchasing → Registers → Accounts payable by basis, layout Payable by supplier, contract and basis) add the basis: the order the debt hangs on, a realization or receipt without an order, or empty for an advance. Every document that moves Receivable or Payable moves its companion by the same amount, so the rows of the basis layout add up to the contract row. Payable by supplier and contract (PayableBySupplierContract) splits PayableBySupplier by contract. These layouts are also the sheets to compare with 1C after a migration — reconciling with 1C.

GlByLegalEntityAccount (Accounting → Totals → General ledger, name GL by legal entity and account) is the accounting debit / credit pair, not tax and not management accounting. Groups: legal entity, then account. Each side has four columns. Goods receipt 100 and recoverable tax 15: inventory — closing debit 100, VAT recoverable — closing debit 15, payables — closing credit 115. Payment 115 gives payables closing debit 115 next to credit 115: the row remains, this is turnover. An account whose debit and credit are both zero for the period is hidden. An empty column caption receives the resource name before the kind, otherwise two "opening" columns cannot be distinguished.

Input VAT on a receipt brings Payable up to gross; a purchase credit note (PurchaseCreditNote) removes only the tax part, and the order stays Received. A supplier return remains net and stock.

The transition from the card is made by a command: Pay vendor, Receive payment, Pay payroll, Pay social insurance — see commands and subtypes.

Overpayment is not blocked the same way everywhere#

  • PayrollLiability — allowNegativeBalance: false: the engine rejects overpayment; paying more than was accrued is impossible in principle.
  • Payable and SocialInsurance — a negative balance is allowed deliberately: advances to a supplier and top-ups after a revised calculation to the fund are both legal. Only document sanity is checked: non-empty lines, positive amount.

The cost of a disposal: one rule, in one place#

Cost is written off not by a document but by the fact that stock balance decreased. The CostingIssue driver hangs on the Stock register, sees the document's full set of postings, collapses them by item and writes off only the net negative.

Consequences an analyst should know:

  • A transfer between cells does not touch cost — two legs on one item, net zero. The goods did not leave; they moved.
  • What to value with is a setting, not code. FIFO or weighted average is chosen in CostingSettings. On two lots at different prices the difference is immediate: two lots 10×7 and 10×9, write off 15 → FIFO gives 115, average gives 120.
  • Production output is valued at the actual component write-off, not by recalculation: whatever the engine took off the lots is what the product costs. Stock value is neither created nor destroyed by output — it is carried across.
  • The benchmark is a price type, not a valuation method. The standard cost is a dated price under CostingSettings.StandardPriceType; FIFO/AVG still compute actual. Sales margin is IGrossMarginService in Sales: revenue minus the InventoryValue write-off on the same document. Breakage without revenue is not part of this number. Details — costing.

Where the cost of a free receipt comes from#

A lot is created by a receipt that fixed a price: receiving a purchase order and production output. A surplus on an adjustment and an upward recount on a stock count have no price by definition — the goods were not bought, they were found. Such a receipt is valued at the item's current average: the found units cost the same as the ones already there, and the average does not move.

An item that was never bought is received as a lot with zero cost — an honest zero, not a loss: the system has no fact about its price.

What reaches the general ledger#

GL posting is an additional contour on top of registers. An unconfigured account profile means no leg, and the document posts as usual.

The legs belong to the document. Each document writes its own GL movements — there is no separate journal entry per sale or payment. Cancel the document (move it back to Draft, void it) and its legs leave the book together with its stock and settlement movements. Example: receive 10 × 7, issue a realization for 4 × 20 — revenue 80 and cost of sales 28 are in the book; return the invoice to Draft — both are gone.

The same holds for the tax calculation an invoice, a receipt or a credit/debit note spawns: cancelling the document returns its calculation to draft (tax leaves the tax ledger, the return and the book), and posting it again re-determines the same calculation with the current base. If the tax period is already filed, the cancellation is refused — correct it with a credit or debit note. Details: Tax · TaxCalculation.

Most legs are part of the document's posting itself. Cost legs — cost of sales, write-off, surplus, work in progress, absorbed labour — are written right after posting, because their amount is what the costing engine took off the lots during it; they are still the document's own movements and leave with it. The full map: Who writes the general ledger.

The amount of a sales posting is the same PricingService.LineAmount used by receivables, revenue, VAT and points — including the invoice discount. A ledger that computed qty × price without the discount drifted from registers silently.

Event Debit Credit
Receiving an order Inventory Payables
Paying the supplier Payables Cash
Realization Issued (Released) Receivables Revenue
Output VAT Receivables VAT payable
Input VAT VAT recoverable Payables
Customer payment Cash Receivables
Customer payment, contract in a foreign currency Cash at the payment-date rate Receivables at carrying value; the gap to exchange gain (Cr) or loss (Dr)
Receivable revaluation at period end Receivables (gain) or exchange loss Exchange gain or receivables (loss)
Sale (cost) Cost of sales Inventory
Write-off, breakage, issue from store Inventory write-off Inventory
Payroll accrual Payroll expense Liability to employees
Withholding a contribution Liability to employees Liability to the fund
Employer contribution Social insurance expense Liability to the fund
Paying payroll Liability to employees Cash
Paying the fund Liability to the fund Cash
Adjustment or stock-count surplus Inventory Surplus income
Paying tax to the budget VAT payable Cash
Customer return Revenue Receivables
Return to the supplier Payables Inventory
Production order Released Work in progress Inventory
Production order Finished (absorbed labour) Inventory Labour absorbed

Accounts are set as a reference into the chart of accounts in accounting settings (ArAccount, RevenueAccount, …). String *AccountCode fields remain the posting key and fallback: on saving the profile, the code is stamped from the chosen account. An unconfigured leg — empty reference and empty code — means no posting.

A contract in a foreign currency#

The customer owes in the contract's currency; the books, tax and revenue are in the legal entity's own. Each document stamps the rate on its date, Receivable carries the debt in both currencies (Amount and AmountFunc), a payment realises the difference against the carrying value of the part it settles, and a ReceivableRevaluation books the unrealised difference at period end. A contract in the entity's own currency posts exactly as before. Rules, settings and a worked example: exchange differences.

Payment documents have neither a store nor a division, and a counterparty has no link to a legal entity — there is nothing to derive the posting addressee from. So VendorPayment and CustomerPayment carry legal entity as an optional header field: leave it empty and the payment settles the register as before, just with no posting.

Why VAT debits receivables and credits payables#

A sales realization posts revenue and receivables at the amount excluding tax. The full sales posting with VAT is Dr receivables (with tax) / Cr revenue (without) / Cr VAT. The tax posting adds the two missing legs in the ledger and adds the tax to the Receivable register too, so a gross payment closes both the ledger and the register. The mirror is Payable on input tax.

Purchasing is built as the mirror: the order posts inventory and payables excluding tax, plus PurchaseOrder.NonRecoverableVat — the non-recoverable share of input VAT, stamped in OnBeforePost. The recoverable share is Dr VAT recoverable / Cr payables for RecoverableAmount. Payables in the Payable register are brought up to the full tax amount (TaxAmount): the supplier is owed gross. TaxCode.NonRecoverablePct 0 (default) means all tax is recoverable, stamp 0, behaviour as before. The calculation line and TaxLedger store RecoverableAmount; the return sums INPUT.

The Tax ledger by code layout (Tax → Totals → Tax ledger) reads the same ledger: code, direction, legal entity. Base 1000 at 15% gives amount 150. Non-recoverable 40% leaves recoverable 90. A row is hidden when closing base, amount and recoverable are all zero.

Two mechanisms compute tax — the universal rules engine (TaxCalculation → TaxLedger) and the Saudi country contour (VatPayable). Only the first is posted to the ledger: the country contour is reporting-only on top of the universal one, and posting both would double the liability.

They have different purposes, but they must use the same rate. The transition into Issued (Released) fixes TaxRateApplied on the document — the rate picked by the tax contour on the invoice date — and the country posting reads it from there. There is no stamp and no accrual on Reserved…Shipped: a reservation creates neither TaxCalculation nor VatPayable. The rate has one source.

A posting is dated by the document date#

The date a posting carries into the ledger comes from the source document, not from the day Post was pressed. This is not cosmetic: the date picks the accounting period (FiscalPeriod), and that period is written into the posting. Registers move by the document date — so an August invoice posted in September with "today" posting would put the subledger in August and the ledger in September, and the period reconciliation would be out by the total of every late document. A closed August could no longer be corrected.

The shared posting service has the date parameter mandatory, without a default: a missing date must not silently become today. Covered by HR/Tests/PostingDateGLTest.cs — two non-overlapping periods, a past-dated document, checks of both the movement date and the selected period.

Three ledgers on one posting#

The GL register carries four extra resources: ManagementDebit/Credit and TaxDebit/Credit. A leg says which ledgers its amount fills (on a manual journal — JournalEntry.Circuits). Empty, and trade postings, mean FIN,MGT: financial (Debit/Credit) and management agree, and tax does not reach management. VAT legs and tax payment carry FIN,TAX: management stays net, and tax sees the liability and settlement.

Why a write-off has its own account rather than cost of sales#

Cost of sales is the cost of what was sold, and gross margin is calculated from it. Breakage, shortage and issues are not sales: putting them in the same account would distort margin by the loss amount. So a disposal outside a sale gets a separate inventory write-off account.

A surplus is income, not a reversal of a write-off. The loss account and COGS stay clean: finding something does not erase breaking something. The leg is Dr inventory / Cr InventorySurplusAccountCode. A zero lot — an item that was never bought — has no amount and therefore no posting.

A stock count posts the delta#

StockCount computes "counted − system" when the draft is saved and puts the number into QtyDelta. Posting runs StockCountTx; the CostingIssue driver sees the minus and writes lots off, and a surplus is still caught by ISurplusCostingService. Direct PostMovementAsync from OnBeforePost cannot stay: the driver does not see those movements, so stock fell while value and ledger stood still.

Paying tax is a separate document#

TaxPayment settles the "VAT payable" account in the ledger (Dr VAT / Cr cash) and does not touch TaxLedger: the accrual is already there from calculation. Sending to the tax authority is a mock (ITaxAuthoritySubmitService → MOCK-OK:… / MOCK-REJECT:…). Every attempt writes a TaxSubmission journal row. Filing the return (TaxReturn → Filed) calls the same channel and is not rolled back when the mock refuses — the outcome is read in the journal, not in the subtype. Live HTTP is a country package on top of the same service (CoC), not a journal replacement. Printing: return TaxReturnZr (period, codes, header amounts — not sales pricing) and payment TaxPaymentZr (legal entity, code, line amount — like VendorPaymentZr).

Amounts are taken as facts, not recomputed#

Cost posting to GL reads ItemCostFifo movements stamped with this document, not a recalculation from lines. Otherwise the valuation method would have to be repeated in two places, and stock accounting would diverge from the ledger exactly on the day the setting was changed.

A production order reads the same negative ItemCostFifo: Released moves the amount to the work-in-progress account. Moving to Finished takes the Released leg back with the rest of the order's movements; materials out and product in are neutral on the one inventory account, so Finished books only absorbed labour (Dr inventory / Cr labour absorbed).

A reconciliation act reads the basis registers#

ReconciliationAct shows one counterparty's debt for a period: opening balance, a debit or credit line per document, closing balance. It reads ReceivableByBasis for the customer and PayableByBasis for the supplier — there the counterparty is a dimension, so one counterparty is read, not the whole register. Its sign is the counterparty's: positive — it owes us. A company that both buys and sells fills Customer and Supplier in one act, and the payable is subtracted: owes 1,000 + shipped 500 − delivered to us 300 = 1,200. The act posts nothing; Agree only freezes the figures that Fill read.

What's not there yet#

  • There is no drill-down from a general-ledger total row to every source document. The delivered layout groups by legal entity and account. You open the source document or a manual journal separately.
  • Closed month and core boundary are two independent doors. IFiscalPeriodService.ClosedReasonAsync / platform IFiscalCalendar refuse posting and unposting on every path (DocumentPostingService) when the date is covered by a FiscalPeriod whose status is not Open. A ledger leg into a closed period is then simply not built. With no calendar row for the date, operations are not blocked. Overlaps, and closing over the head of an earlier open period, are still cut by FiscalPeriodEventHandler. The core posting boundary (IAccountingPeriodService.ClosedPeriod) is another door: one date per tenant, Administrator → Posting boundary. It is not derived from FiscalPeriod status — one date cannot express "February is closed, January is open", and the right to move the boundary is separate from the right to edit the dictionary.
  • FiscalPeriod.Status is a free string, not an enumeration. The set is closed (Open / closed), and its place is in metadata. Code compares case-insensitively and treats everything that is not Open as CLOSED: a typo must forbid posting, not permit it.
  • Payment is applied to a basis, not matched to an invoice line. The basis registers keep the open debt per order (or invoice without an order), and a payment line names its basis. There is no record of which payment line closed which part of which invoice beyond that, and a payment without a basis stays an advance on the contract.
  • Document status does not reflect payment. The Paid subtype on a sales realization is unreachable, and moving into it is blocked: it would remove receivables without payment, burn loyalty points, zero accrued VAT, and the document would freeze (Paid is read-only). Payment status is the Receivable register, not the subtype.

Same topic for other readers: User · Developer

Where next#

Was this page helpful?