Modules / Invoicing
Invoicing & collections
Send invoices for what you sold, record what customers pay, and know who owes you money and for how long — without re-typing anything the sales side already captured.
People ask for it as: “send invoices”, “who owes us money”, “chase late payments”, “stop re-typing quotes into the accounts”, “statements for customers”
Who uses it
| Role | Does | Sees |
|---|---|---|
| Whoever sends the invoices (office, bookkeeper) | drafts invoices, issues them, sends them, records receipts | every invoice and payment |
| Whoever chases money | works the overdue list, records promises and disputes, allocates receipts | every open invoice and its history |
| Owner / finance manager | approves credit notes and write-offs, sets terms, numbering and the account map | everything |
| Sales (read-only) | checks whether a customer is overdue before quoting them again | invoice status and balance for a customer; never lines or margin |
| The system | derives settlement and aging, sends reminders, raises recurring invoices, posts to the ledger | everything, through its own service account |
What it has to do
Reference user stories. Each has a test written into it — the given / when / then — which becomes an automated check once it is built. Expect to keep some, change some and drop some.
Things people do
AR-01As the person who invoices, I want an invoice created from an accepted quote in one step, so that nothing is re-typed and the customer is billed what they agreed.
Given an accepted quote with lines, a currency and the customer's tax treatment · when I raise the invoice · then a draft exists with the quote's lines, keyed invoice_id = "quote:<quote_id>", tax recomputed at this invoice's tax point; raising it again returns the same invoice rather than a second one
Whoever sends the invoices (office, bookkeeper) · screen: Invoice · reference: not built
AR-02As the biller, I want to raise an invoice by hand for something that never had a quote, so that ad-hoc work can still be billed.
Given a customer, and a line with a description, quantity, price and tax code · when I save it · then line totals, the per-rate tax summary and the total are computed by the system, never typed, and the invoice is a draft
Whoever sends the invoices (office, bookkeeper) · screen: Invoice · reference: not built
AR-03As the biller, I want issuing an invoice to fix its number, its dates and its contents, so that what the customer received cannot silently change.
Given a draft invoice with at least one line and a customer with an address and terms · when I issue it · then it is stamped with a unique number from the series, an issue date, a tax point and a due date from the terms; the contents are frozen and every later edit is refused with "credit it instead"
Whoever sends the invoices (office, bookkeeper) · screen: Invoice · reference: not built
Usually different per customer: the number format, whether it restarts each year, and whether drafts are used at all
AR-04As the biller, I want to correct an issued invoice with a credit note rather than an edit, so that the audit trail and the tax records survive.
Given an issued invoice, whether paid or not · when I credit it in full or in part · then the credit note has its own number, lines and per-rate tax, is allocated against the invoice, reduces its balance, and the original stays readable
Whoever sends the invoices (office, bookkeeper) · screen: Credit note · reference: not built
AR-05As the person recording money, I want one receipt to settle several invoices, so that a customer paying three bills with one transfer is handled properly.
Given a receipt of 10,000 and three open invoices · when I allocate it across them · then each invoice's balance falls by its allocation, the unallocated remainder stays on the customer as credit, and allocating more than the receipt or more than an invoice's balance is refused
Whoever chases money · screen: Payments · reference: not built
AR-06As the person chasing money, I want one list of what is overdue and by how long, so that I know who to call this morning.
Given invoices past their due date · when I open the overdue list · then I see them in the configured aging buckets, aged from the basis the business chose, with customer, amount outstanding, age and last contact
Whoever chases money · screen: Collections · reference: not built
AR-07As the collector, I want to record what a customer promised or disputed, so that the next person picking it up is not starting again.
Given a customer with several overdue invoices · when I log one promise to pay on a date, covering all of them · then it is recorded once against the customer, shows on each invoice, and suppresses reminders until that date; a dispute suppresses them until it is resolved
Whoever chases money · screen: Collections · reference: not built
AR-08As the biller, I want to send the customer a statement of everything outstanding, so that they can reconcile their side.
Given a customer with open invoices, credit notes and unallocated receipts · when I produce an open-item statement as at a date · then it lists every unsettled document with a closing balance equal to their sum, and records that it was sent
Whoever sends the invoices (office, bookkeeper) · screen: Customer · reference: not built
AR-09As the owner, I want to write off a remainder that will never be paid, so that it stops appearing on the chase list and the books show the loss.
Given an invoice with a balance below the write-off threshold, or any balance with my approval · when I write it off with a reason · then the write-off is allocated against the invoice like a payment, the balance clears, and the loss is posted to the ledger as bad debt
Owner / finance manager · screen: Invoice · reference: not built
AR-12As a sales rep, I want to see whether a customer is overdue before I quote them again, so that I am not discounting for someone who has not paid.
Given a customer with an invoice 40 days past due · when I open them in the sales system · then I see the overdue amount, the oldest age, and whether they are on credit hold — read-only, through invoicing's own route
Sales (read-only) · screen: Customer · reference: not built
AR-13As the biller, I want to send the invoice to the customer and know it went, so that "did you get it?" has an answer.
Given an issued invoice · when I send it · then the document is produced, the send is recorded with the date and address used, and re-sending records a second send rather than a second invoice
Whoever sends the invoices (office, bookkeeper) · screen: Invoice · reference: not built
Usually different per customer: the delivery route (attached to an email, a portal, a national e-invoicing network) and the document format
AR-14As the biller, I want to cancel an invoice that should never have existed, so that the numbering stays explainable.
Given an issued invoice with no allocations · when I void it, where the jurisdiction allows voiding at all · then it is marked void with a reason, keeps its number, posts a reversing entry, and drops out of every balance; where voiding is not allowed, a full credit note is the only path and the app says so
Whoever sends the invoices (office, bookkeeper) · screen: Invoice · reference: not built
AR-15As the biller, I want to invoice a deposit before the work, so that "50% up front" is a real invoice rather than a note.
Given an agreement to bill half in advance · when I raise a deposit invoice and later the final one · then the deposit is a normal invoice with its own tax treatment, and the final invoice shows the deposit already allocated so only the balance is due
Whoever sends the invoices (office, bookkeeper) · screen: Invoice · reference: not built
Usually different per customer: whether deposits are billed at all, and whether tax falls at the deposit or at delivery
AR-16As the collector, I want a customer put on credit hold when they are far enough past due, so that we stop shipping to someone who is not paying.
Given a customer over their credit limit or past the hold threshold · when sales tries to quote or accept for them · then the sales side sees the hold and the reason; releasing the hold is an approver's act and is recorded
Whoever chases money · screen: Customer · reference: not built
Usually different per customer: whether credit limits exist at all — many small businesses want the warning without the block
AR-17As the biller, I want to see the list of invoices with their state and what is outstanding, so that I can find one without searching for it.
Given invoices in every state · when I open the list · then I can filter by state, customer, currency and date, and each row shows the number, the customer, the total and what is still outstanding
Whoever sends the invoices (office, bookkeeper) · screen: Invoices · reference: not built
Things that happen on their own
AR-10on read (derived)The system works out what is still outstanding and how old it is whenever anyone looks, so that "overdue" is never stale and no job can be missed.
Given an invoice of 1,000 with a 400 receipt allocated and a 100 credit note · when anyone opens it or the overdue list · then it shows 500 outstanding, part paid, in the bucket its age falls in — all computed, with no stored paid flag and no nightly job
· reference: not built
AR-11nightly, for customers on a reminder scheduleThe system sends a payment reminder at the intervals the business set, so that chasing does not depend on someone remembering.
Given an invoice 7 days overdue on a 7/14/30-day schedule · when the nightly run completes · then exactly one reminder for that step exists — keyed (invoice, kind, step), so a re-run sends nothing further — and a promise or dispute suppresses it
· reference: not built
AR-18on the day each recurring agreement falls dueThe system raises the invoices that repeat — retainers, subscriptions, service contracts — so that nobody bills them by hand every month.
Given a monthly retainer due on the 1st · when the run completes on the 1st · then one draft invoice exists for that period, keyed (agreement, period), and a second run creates nothing
· reference: not built
Usually different per customer: whether recurring billing exists at all, and whether the drafts are issued automatically or reviewed first
Things that cross into other modules
AR-20when a quote is accepted in salesSales asks invoicing to raise a draft invoice for the accepted quote, so that nothing is re-keyed.
Given an accepted quote · when sales calls invoicing's route · then a draft invoice exists against the same business partner with the quote's LINES (tax recomputed here, not copied), keyed invoice_id = "quote:<quote_id>"; calling twice returns the same invoice
· owned by ar · reference: not built
AR-21when an invoice is issued, voided, a receipt allocated, a credit note approved, or a write-off approvedInvoicing posts the accounting entries to the ledger, so that the books agree with the invoices without a monthly re-keying exercise.
Given an issued invoice of 1,000 plus 50 tax, with the account map configured · when it posts · then a balanced journal (receivable 1,050 / revenue 1,000 / tax 50) is posted through the ledger's own route with journal_id derived from the document id, so re-posting changes nothing; an open AR subledger item is created for the invoice and cleared when it settles; posting into a closed period is refused and the refusal names the period
· owned by gl · reference: not built
AR-22when a customer is invoiced for the first timeThe customer comes from the shared record rather than being typed again.
Given business-partner is installed · when an invoice is raised for a company that already exists in sales · then it uses the same partner id, and no second customer row is created
· owned by business-partner · reference: not built
How things move
invoice
lifecycle (stored, a person's decision): draft → issued → void | cancelled
- draft → issued by biller · when at least one line; a customer with address, terms and tax treatment · number, issue date, tax point and due date stamped; contents frozen (AR-03); posts to the ledger (AR-21)
- draft → cancelled by biller · a draft that was never issued leaves no number behind
- issued → void by approver · when no allocations, and the jurisdiction permits voiding · needs reason · reversing journal; drops out of every balance (AR-14)
credit_note
draft → approved → allocated → void
- draft → approved by approver · when lines and tax present; where it names an invoice, the credit does not exceed that invoice's total · posts to the ledger
- approved → allocated by biller or collector · allocated against one or more invoices, or left on the customer as credit
dunning
none → reminded → promised → disputed → resolved → escalated
- none / reminded → promised by collector · needs promise_date · suppresses reminders until that date (AR-07)
- none / reminded / promised → disputed by collector · needs reason · suppresses reminders
- disputed → resolved by collector · needs outcome · reminders resume, or a credit note settles it
- reminded / promised / resolved → escalated by collector · credit hold considered (AR-16)
What it keeps
| Thing | Meaning | Identified by | Shared? |
|---|---|---|---|
| invoice | a demand for payment, addressed to a customer | invoice_id — immutable for life, derived where it has a source ("quote:<quote_id>", "rec:<agreement>:<period>") and a uuid otherwise. The invoice NUMBER is an attribute stamped at issue, never the key | module-owned |
| invoice line | one billed item | invoice_idline_no | module-owned |
| invoice tax | the tax summary the document must print — one row per rate on the invoice | invoice_idtax_code | module-owned |
| payment | money received from a customer | payment_id — derived from the bank reference where there is one ("bank:<statement>:<line>"), otherwise from the import batch and row so a re-import is still idempotent | module-owned |
| credit note | a tax document reducing what a customer owes | credit_id (immutable); credit_no stamped at approval | module-owned |
| writeoff | a balance the business accepts it will not collect | writeoff_id | module-owned |
| allocation | what settled what — the single mechanism behind every reduction in a balance | source_kindsource_idinvoice_idseq | module-owned |
| dunning event | a reminder sent, a promise made, a dispute raised or resolved | bp_idkindstepref_id | module-owned |
| send | a record that the document went out | invoice_idsent_at | module-owned |
| statement | an open-item statement as at a date, as it was sent | bp_idas_at | module-owned |
| recurring agreement (optional) | something billed on a repeating schedule | agreement_id | module-owned |
| number series | the counter behind invoice and credit-note numbers — STATE, not a setting | series_id (e.g. "INV-2026") | module-owned (state) |
| terms | when payment is due | terms_id | module-owned (configuration) |
| settings | how this business invoices — everything an office manager may change | key | module-owned (configuration) |
| account map | which ledger account each posting hits | purposetax_code | module-owned (configuration); the accounts themselves belong to the ledger |
| tax code | a rate and its treatment (standard, zero, exempt, reverse charge) | tax_code | SHARED — this module owns it until a ledger exists, then the ledger does |
Invoicing is where a duplicated customer list finally costs money: the statement will not match the sales view and nobody can say which is right. Install business-partner first.
What runs on its own
| Story | When | What |
|---|---|---|
| AR-11 | nightly | reminders at each customer's schedule |
| AR-18 | daily | raise the invoices that repeat |
Questions that shape your version
Read these out and let the customer correct one — people rarely recall a policy on demand and can always react to one. Standard is a named authority you can look up and hold us to. Commonly seen is exactly that: arrangements we have met, with no survey behind them. A customer doing none of them is not doing it wrong.
- Do you invoice from an accepted quote, or is billing separate from sales?
- What should an invoice number look like, and does it restart each year?
Standard EU VAT Directive 2006/112/EC art. 226 — an invoice must carry a sequential number, from one or more series, that uniquely identifies it. Most VAT regimes outside the EU require the same shape.
- most vat regimes
- one unbroken sequence per company, no gaps — a deleted invoice is voided, never removed
- uk eu
- a prefix plus a running number, restarting each financial year
- groups
- a prefix per trading entity so two companies never share a number
- When is payment due — 30 days, end of month, half up front?
Standard EU Directive 2011/7/EU on late payment: 30 days unless expressly agreed, 60 maximum between businesses. UK: Late Payment of Commercial Debts (Interest) Act 1998 gives a statutory right to interest.
- b2b services
- net 30 from the invoice date
- construction
- net 30 to 60, with retention held until practical completion
- saas
- paid in advance, monthly or annually
- retail consumer
- paid at the point of sale; an invoice is the exception
- Do customers pay several invoices with one transfer?
- Does anything repeat every month?
- Who may approve a credit note or write off a balance?
- Do you want reminders sent automatically, or do you prefer to chase by hand?
- saas
- automatic at 7, 14 and 30 days overdue, then the service pauses
- professional services
- nothing automatic — a partner calls before anything is sent
- smaller firms
- a statement once a month rather than per-invoice chasing
- Do you keep your books here, or in Xero/QuickBooks?
- under 20 staff
- Xero or QuickBooks, with an accountant who expects to keep using it
- larger or multi entity
- books here, because consolidation across entities is the reason they moved
- Do you invoice in more than one currency?
- Do customers ever deduct tax at source, or take a discount for paying early?
What you can change yourself
These are settings, not code: someone in your team changes them on a screen, without a developer and without a release.
- number format and whether it restarts each year, aging buckets and whether age runs from the due date or the invoice date, the reminder schedule, the write-off threshold, credit-limit behaviour (warn or block), statement wording, and the company details on the document
- payment terms — 30 days, end of month following, 50% up front
- tax codes, rates and treatment — until the ledger owns them
- which ledger account receivables, revenue, each tax code, bad debt and FX differences post to
Where the platform limits what we can promise
Gap-free sequential invoice numbers are a legal requirement in many places, and DataK3 has no sequences. Use a high-water-mark row plus MAX(invoice_no), with the invoice row's own primary key as the uniqueness guard and a retry on conflict; raise the mark in its own committed transaction, and do NOT use SELECT … FOR UPDATE (it produced sustained write conflicts on a dev bucket). An open fault currently lets concurrent inserts of the same key all acknowledge with one row surviving, so issue through a single writer, re-read the number after issuing, and tell the customer what the guarantee actually is today.
ALTER TABLE … ADD COLUMN unsettles reads on that table for minutes. migrate the test copy first, and change the live one at a quiet hour
No read-your-writes inside an open transaction, and rows committed seconds earlier can be briefly invisible. allocate, commit, then re-derive the balance with SUM() in a second transaction; re-read before acting on an empty result
DataK3 rejects CREATE VIEW. the sales-facing balance is a route, not a view
For builders
- Module spec — this page, for agents
- The lifecycle and the design template
- ar-core — invoices, tax per rate, receipts and allocation, credit notes, write-offs, collections and statements
Known gaps in the reference
- ar-core covers AR-01 to AR-09, AR-12 to AR-14, AR-17 and AR-21; recurring invoices (AR-18), scheduled reminders (AR-11), deposits (AR-15) and credit holds (AR-16) are not built
- AR-21 posts an issued invoice, a receipt and a write-off; a VOID does not yet post its reversing journal, so a voided invoice leaves its original entry in the books until someone reverses it in the ledger
- the ledger posting credits tax per rate when the per-rate rows read back, and as one line against the default tax account when they do not — a rate breakdown in the books is best-effort, the total never is
- GET /api/invoices has no pagination and no source filter: it materialises every invoice in the bucket and derives settlement per row, so it stops answering inside a client timeout at a few hundred invoices
- the column named bp_id is really a bp_KEY: invoicing accepts whatever a customer calls their customers (ACME-01), while the party master derives a BIGINT bp_id from that key. Both meanings share one column, told apart only by whether they parse as a number, so the cross-module join needs a CAST. Retyping it would break standalone invoicing; the four-release fix is in design/ar-bp-id-change-request.md
- withholding tax (the customer legally pays less than the invoice) is not modelled: without it the balance never clears
- early-settlement discounts are not modelled — in most jurisdictions they also require a credit note for the tax
- interest and late fees are not modelled
- no document format for national e-invoicing (Peppol, ZUGFeRD, ZATCA, FatturaPA) — ask the jurisdiction before promising one
- no bank feed: statement import is a file the biller uploads, and idempotence depends on the bank supplying a stable reference
- invoice numbering is only as strong as the platform fault above allows