Skip to main content

Receivables and revenue

Receivables mirror payables, one control account to the other side. Every rule that holds for AP holds here: the entry, the sub-ledger obligation and the tax register line are one fact, written in one transaction.

An AR invoice

Revenue is credited at the net, output VAT is a separate credit, and the AR control account is debited at the total. Written alongside it: the AR obligation in obligations (kind: "invoice", party_kind: "customer", keyed by client_code and the invoice’s own number), and - when VAT was charged - the direction: "output" register line in tax_entries against GL 2100. The obligation’s amount_base is read back off the posted AR leg, not computed from a rate the caller may not have supplied. That is what lets close_checks.ar_control_tie compare base against base.
Output VAT is the twin of input VAT, and the same pair of Close checksums reads it back: the register against the GL balance, and each line’s tax_amount == ROUND(base_amount × rate). The output-VAT per-line checksum is one GreatBook added that the source workbook never had.

A receipt clears it

The receipt records a settlement row on its rail plus allocation rows saying which invoice it paid and how much of it. Nothing about the invoice is edited: its due is derived as total - allocated - advance_applied, adjusted by any credit or debit notes against it. Which GL account a rail lands on is a property of the rail, and each rail reconciles against its own external source rather than against one global cash figure. The nine rails and their accounts are on Settlement, rails and the AP/AR spine.

Customer advances and deposits

The non-cash application is a real journal entry with no rail movement behind it. The Close list carries Σ|advance pool| as a must-be-zero checksum precisely to catch an advance that was received and never applied.
A deposit held is supposed to be a non-zero liability, so it is reported as an explainable balance rather than a break. Reporting a legitimate balance as a break trains people to ignore breaks, which is why the Close list has two kinds of row.

Adjustment notes change the derived due, never the invoice

The original invoice is never edited. A note references it, posts its own entry, and moves the derived outstanding. due = total - allocated - advance_applied + Σ(debit notes) - Σ(credit notes). Two checks guard a note against what the original actually carried: its quantity gap may not exceed the original’s quantity, and its VAT rate must match the original’s. Both are described in full on Adjustment notes, with the other two shapes on Adjustments, FX and consolidation.

Factoring: the distinction is derecognition

Selling a receivable and borrowing against one look identical in the bank account and are opposite in the ledger. Getting this backwards either hides a liability or reports revenue-backed cash twice. The Close list polices it as its own family, tying to the AR sub-ledger.

Intercompany invoices mirror

The two are expected to eliminate on consolidation. Current consolidation reads keep each side as independent evidence, so a missing or mismatched half stays visible rather than being treated as a proved pair.

Accrued and deferred revenue

Both are moved by the Accountant’s adjusting entries at close, through the human-approved posting gate like every other entry - never by a direct write.

Where it is enforced

The test that would fail if it broke

backend/tests/test_settlement.py and backend/tests/test_close_checks.py drive the AR spine and its tie-outs against a real migrated Postgres. backend/tests/test_loans.py covers the recourse versus non-recourse split, including that a non-recourse advance takes the invoice’s derived due to zero.
The document path posts creates_ap only. An AR invoice reaches the ledger through create_invoice, not through the Bookkeeper’s document lane - so a sales invoice uploaded as a document is classified, filed and reviewable, but its posting rule is not yet written. That is stated the same way everywhere in these docs: what is not built is named, not implied.