Debit notes (AR and AP) — design
Status: approved in chat 2026-10-05, spec awaiting review. Not on docs/ROADMAP.md yet; add
an item when the plan is written.
Decision being reversed
Section titled “Decision being reversed”The AR design (2026-09-27-accounts-receivable-design.md, scope note and the “No debit notes
in v1” line) and the MyInvois design (2026-09-27-myinvois-design.md §57, “AR and AP have
none”) both say an extra charge is a new invoice. That stands as the workaround, not the
rule: Malaysian ERPs (AutoCount, SQL Account) offer AR and AP debit notes, and customers
migrating from them expect the document. This spec adds them. Amend both designs to point here.
Meaning
Section titled “Meaning”customer_debit_note(AR): raises what a customer owes on a previously issued invoice (undercharge, freight, interest).supplier_debit_note(AP): the supplier bills us more. It raises what we owe and behaves like a purchase invoice. Buyer-issued returns and claims stay withsupplier_credit_note; this spec does not duplicate them.
Approach
Section titled “Approach”New kind values in the existing sales_invoice and purchase_invoice tables, exactly as
credit notes are. A table per note type was rejected: it duplicates numbering, posting,
allocation, aging and PDF code. The sign of a line still comes from kind; lines stay positive.
Behaviour
Section titled “Behaviour”- Posts like an invoice, never mirrored. AR: Dr receivable gross, Cr income net, Cr tax.
AP: Dr expense/asset/GRNI, Cr payable.
accounting/still reacts to the semantic tag. - Workflow: the invoice graph shape (preparer issues directly), registered as its own kind
and added to
workflow.AccrualOnlyKinds. - Optional
original_invoice_id, and on the AR side an optionalreason(purchase invoices have no reason column; the AP debit note carries none). When an original is given it must be an issued, uncancelled invoice of the same customer/supplier. No amount cap, no self-allocation, and the currency and rate are the document’s own, not inherited. - Payment terms and due date as an invoice (a credit note is due immediately).
- A valid target for receipts, payments, credit-note offsets and payment batches. Never a source of an allocation.
- Void refuses while allocations exist, as for invoices.
- Reference prefixes
DN(AR) andSDN(AP). There is no opening debit note: an old system’s open debit note is migrated as an opening invoice (same sign and ledger effect); settled ones go to the archive under their own kind. - No new permission keys:
receivables.invoice.*andpayables.invoice.*cover it.
Database (one migration, next free number)
Section titled “Database (one migration, next free number)”- Extend
sales_invoice_kind_known,purchase_invoice_kind_known. - Original-link CHECKs:
sales_invoice_credit_note_shape(as relaxed by00640) andpurchase_invoice_original_only_credit_notemust allow an optional original on a debit note. ar_allocation_target_is_invoiceand its AP twin must accept the debit note as target.tax_return_line_source_kind_knowngains both kinds.einvoice_submission_type_knownandeinvoice_submission_type_matches_sourcegain03,13.archived_document_kind_knownand trade-link pairs only if archive/trade lines need them (confirm in the plan; default: archive yes, trade link no).- The freeze triggers read semantics, not kinds, so they need no change. Every table keeps
org_id,FORCE ROW LEVEL SECURITY,org_isolation; no new table is added.
AR (internal/platform/receivables): constants and SourceKinds; graph registration and
step.go v2 kinds; Rules() entry plus label; resolveKind; checkIssuable (no cap);
due-date logic in issue(); allocation target and receipt target checks; aging.go — the
applied join and the sign CASE, which today treats every non-sales_invoice as a
credit; delivery match (a debit note is not linked in v1, refused by name).
AP (internal/platform/payables): constants; documentGraph(KindDebitNote, …); Rules();
prefix and CreateInvoice guard; due-date logic; allocation target; checkPayment;
aging.go join; matchedKind (not matched in v1); invoiceShares decision; assets and
withholding-register lookups checked, default is exclude.
Cross-cutting: taxreturn/compute.go must not net a debit note as a credit even when
original_invoice_id is set (the loader at ~L290-330 assumes any non-invoice row with an
original is a credit note); payment-basis loader decision; imports/documents.go and
archive.go; printing title keys (en, ms); webhooks pass-through; fixtures and
cmd/worker/wiring_test.go.
MyInvois
Section titled “MyInvois”AR debit note → type 03 with BillingReference to the original when present. AP self-billed
debit note → type 13. Register the sales and purchase guards for the new kinds (an
unregistered kind never queues an e-Invoice), extend the codec check and UBL builder, and the
void-exit and publish loops in setting.go. Unverified against LHDN’s sandbox: record it
on the roadmap as an open item beside the pre-mandate credit-note one, owned by the adviser.
Extend the enums in api/openapi.yaml: SalesDocumentKind, ApAgingRow.kind,
ArAgingRow.kind, PurchaseInvoiceInput.kind, PurchaseInvoice.kind,
EInvoiceSubmission.source_kind and .document_type, TaxReturnLine.source_kind,
TradeChainInvoice.kind, ArchivedDocument.kind, and the three inline kind query
parameters. No new endpoints. Then make generate; regenerate api/public-openapi.yaml
(adding enum values is not breaking for the public baseline).
Third tab on both Invoices pages (routes.tsx search enum, InvoicesPage.tsx); dialogs follow
the invoice branch with an optional original picker and reason; drawers; debit notes included in
useOpenInvoices / receipt and payment pickers; en/ms/zh strings (receivables, payables, trade,
imports, printing, einvoice) with the i18n parity test; page tests; e2e seed.
- Posting: debit note posts unmirrored in AR and AP.
- Aging: a debit note ages positive (fails today without the
CASEfix — watch it fail). - Allocation: receipt/payment/credit-note offset against a debit note; a debit note is refused as a source; void refused while allocated.
- SST return: debit note with an original is not netted as a credit.
- DB drift: new CHECKs accept the kinds; allocation trigger accepts the target.
- E-invoice: type
03/13built and the guard queues it. - End to end: debit note issued → receipt → aging zero (
web/e2e). - Presence-before-absence: each refusal test first shows the accepting case.
Out of scope
Section titled “Out of scope”Refund notes (04/14), customer refunds, delivery/GRN matching for debit notes, and a
buyer-issued AP debit note.
Open items
Section titled “Open items”- LHDN sandbox validation of types
03and13(adviser).