Skip to the lesson
CivOps AI Academy · M22Quotes, Orders and Invoicing: Order-to-Cash Up to the Ledger
0%

Chapter 1 · Sessions 1 to 3

From accepted quote to issued invoice

An invoice is a promise written down: what was delivered, to whom, for how much, due when. This chapter follows one from an accepted quote to an issued, numbered invoice that nobody can edit, and names the ten tables that hold it.

25 min10 bill_ tables6 invoice sourcesNever edited

By the end of this chapter you can

  • Say what the module does and what stays in the accounting system, tax engine and payment provider.
  • Name the six sources an invoice can come from and the billing terms an account carries.
  • Explain why a sent invoice is never edited and why numbering has no gaps.

What this module is for

Quotes, orders and invoicing turns accepted quotes and shipped or completed work into correct invoices: billing terms, tax by the rate in force, credit notes, payment application and statements. Every invoice is then posted once to the accounting system through a signed posting contract. The module stops there on purpose. The general ledger, payables and financial statements stay in the accounting system; tax calculation engines, subscription revenue schedules and card or bank processing stay with the products built for them.

Order to cash, up to the ledgerThe module listens for four events (quote accepted, sales order shipped, service work order completed, milestone accepted) and publishes four of its own (invoice issued, payment applied, invoice overdue, posting failed). LISTENS FORPUBLISHESQuote acceptedcrm.quote.acceptedSales order shippedinv.sales_order.shippedService work order completedfsm.service_work_order.completedMilestone acceptedprj.milestone.acceptedInvoice issuedbill.invoice.issuedPayment appliedbill.payment.appliedInvoice overduebill.invoice.overduePosting failedbill.posting.failedInvoicingbill_ tablesdraft, approve, issueapply paymentspost onceEverything left is owned by other modules; everything right is published for other modules.
Order to cash, up to the ledger. The module listens for four events (quote accepted, sales order shipped, service work order completed, milestone accepted) and publishes four of its own: invoice issued, payment applied, invoice overdue and posting failed.
  • In: billing accounts and terms, invoices from six sources, tax lines, credit notes, payment application, statements and reminders, and one idempotent posting per document.
  • Out: the general ledger, payables and financial statements; tax engines; revenue schedules; card and bank processing. These are integrated, not rebuilt.
  • Later tiers: remittance matching from bank statements and e-invoicing export are extra capabilities, not part of the first version.
  • Not built in the practice sessions: milestone and time-and-materials invoices, refunds, and the consumers for the shipped, completed and milestone-accepted events. There the billing clerk starts a draft from a shipped order or finished work by hand; only the quote-accepted consumer is built.

Billing accounts and terms

A billing account says who is billed and on what terms. It points to the customer as a party in the core tables, may link softly to the sales account, and carries a currency, payment terms, a tax-exempt flag with the certificate reference and its expiry date, an optional credit limit, and a status of active, on_hold or closed. The due date on an invoice is worked out from the terms when it is issued.

payment_termsPlain meaning
due_on_receiptPayable as soon as the invoice is received
net_15, net_30, net_45, net_60Payable that many days after the invoice date
eomPayable at the end of the month
depositAn amount is due up front, before the work is done

An exemption certificate has an expiry date for a reason: the day after it lapses, the account is taxable again unless a new certificate is on file. The invoice screen should warn before that day, not after.

Six sources, one invoice table

Six sources, one invoice tableSix boxes on the left (sales order, shipment, service work order, project milestone, time and materials, manual) each point to the single bill_invoice table. source_type says where the lines came fromsales_ordershipmentservice_work_orderproject_milestonetime_and_materialsmanual, approved over a limitbill_invoiceone header per invoicesource_type columnsoft link back to the source
One table, six sources. The source_type column records where an invoice came from, and a soft link (sales_order_id, service_work_order_id, project_id or quote_id) points back to it without a hard database dependency on another module.

A hard foreign key goes only to the shared core tables and to this module's own tables. Links to the sales order, work order, project and quote are soft: the id is stored, but the other module can change independently. A manual invoice has no source document, so it carries one control instead: above a set amount it needs a second person's approval, held in core_approval, and the invoice keeps the approval id.

Money, currency and numbers

All money is held as whole numbers of the currency's smallest unit (cents for the dollar), never as fractions. The currency code and its minor unit come from ISO 4217: most currencies have two decimal places, some have none and some have three, so the module stores the code beside every amount [4]. A tax rate is a number, but an amount of tax is always rounded once, by a rule written down, into whole cents.

Invoice numbers are gapless within a series. A draft carries a temporary number so it can be saved, and the real number is drawn from the number sequence only at the moment of issue, inside the same database transaction that issues the invoice. If that transaction fails, no number is used up. Skipped numbers are the first thing an auditor looks for.

An issued invoice is never edited

Invoice statusDraft, approved, issued, partially paid and paid in a row, a dashed arrow from draft straight to issued below the approval limit, and void and disputed branching from issued. draftapprovedissuedpartially_paidpaidbelow the limit: no second approvalvoiddisputedA sent invoice is never edited: a mistake is credited and, if needed, reissued.void and disputed keep the record; nothing is ever deleted.
Invoice status. A draft is approved when a second person must see it, then issued; below the limit it can go straight to issued. After issue the status moves only with payment, dispute or void, and never by changing the figures.

The reason is plain: the customer, the accounting system and the tax authority may all hold a copy of what was sent. Records of this kind are kept for a period set by the rules that apply to the business, and guidance such as the IRS's record-keeping publication describes what to keep [2]. Keep the original exactly as issued.

The ten tables

The ten bill_ tablesTen tables: billing account, invoice, invoice line, tax rate, payment, payment application, credit note, posting, dunning step and dunning event, with arrows for the main links and a note that they link to the core_ spine tables. bill_billing_accountwho is billed, on what termsbill_invoiceheader, never editedbill_invoice_lineline with its taxbill_tax_ratedated rate tablebill_paymentmoney receivedbill_payment_applicationpart paid to an invoicebill_credit_notethe only correctionbill_postingone document, posted oncebill_dunning_stepthe reminder schedulebill_dunning_eventa reminder sent or skippedLinks to the spine, the core_ tables:core_party, core_item, core_uom,core_reason_code, core_approvalEvery table also carries id, tenant_id, created and updated stamps, row_version, archived_at and ext.
The module's ten tables. Every table also carries the standard columns (id, tenant_id, created and updated stamps, row_version, archived_at and ext). Amounts are whole cents and every table is filtered by tenant.

Every column of the ten tables

Copy these tables into your own docs/bill-tables.md; your agent builds the migration from them. Types: uuid, text, boolean, date, integer and timestamptz (a date and time with its time zone) are plain; money is a bigint holding whole cents; quantity is numeric(18,4); a rate is numeric(9,6). Required means not null. An arrow means a hard foreign key (a column whose value must match the id of a row in that table); a soft link is a plain uuid with a comment and no constraint, because the module that owns the row may not be installed. Every table also has the standard columns: id, tenant_id, created_at, created_by, updated_at, updated_by, row_version, archived_at and ext.

bill_billing_account

ColumnTypeRequiredAllowed values or note
party_iduuid → core_partyyes
crm_account_iduuid, soft link to crm_accountnoScopes what a salesperson may see
currencytextyesA three-letter ISO 4217 code
payment_termstextyesdue_on_receipt, net_15, net_30, net_45, net_60, eom, deposit
tax_exemptbooleanyes
exemption_certificate_reftextnoThe certificate reference
exemption_expires_ondatenoThe day the certificate lapses
credit_limitbigint (cents)no
statustextyesactive, on_hold, closed
ext: old_customer_codetext inside extnoThe old tool's customer code, set by the import; unique per tenant when present

bill_invoice

ColumnTypeRequiredAllowed values or note
numbertextyesUnique per tenant; DRAFT- and eight characters of the id while a draft
billing_account_iduuid → bill_billing_accountyes
statustextyesdraft, approved, issued, partially_paid, paid, void, disputed
issue_datedatenoSet at issue
due_datedatenoWorked out from the account's terms at issue
currencytextyesA three-letter ISO 4217 code
subtotalbigint (cents)yes
tax_totalbigint (cents)yes
totalbigint (cents)yesEquals subtotal plus tax_total
balance_duebigint (cents)yesNever negative and never above total; 0 when paid or void
source_typetextyessales_order, shipment, service_work_order, project_milestone, time_and_materials, manual
sales_order_iduuid, soft link to inv_sales_orderno
service_work_order_iduuid, soft link to fsm_service_work_orderno
project_iduuid, soft link to prj_projectno
quote_iduuid, soft link to crm_quoteno
approval_iduuid → core_approvalnoSet when a second person must approve
issued_hashtextnoSHA-256 of the issued content

bill_invoice_line

ColumnTypeRequiredAllowed values or note
invoice_iduuid → bill_invoiceyes
line_nointegeryes
item_iduuid → core_itemno
descriptiontextyes
quantitynumeric(18,4)yes
uom_iduuid → core_uomnoThe unit of measure
unit_pricebigint (cents)yes
discountbigint (cents)no
line_totalbigint (cents)yes
tax_codetextnoEXEMPT on an exempt line
tax_amountbigint (cents)yes

bill_tax_rate

ColumnTypeRequiredAllowed values or note
jurisdictiontextyesCountry and region, such as ZZ-T1
tax_codetextyes
ratenumeric(9,6)yesA fraction: 0.07 means 7%
valid_fromdateyes
valid_todatenoEmpty on the current row; two rows for one jurisdiction and tax_code may not overlap

bill_credit_note

ColumnTypeRequiredAllowed values or note
numbertextyesUnique per tenant
invoice_iduuid → bill_invoiceyes
reason_code_iduuid → core_reason_codeyes
amountbigint (cents)yesExcludes tax
tax_amountbigint (cents)yesThe credit is amount plus tax_amount
statustextyesdraft, approved, issued, applied
reissued_invoice_iduuid → bill_invoicenoThe corrected invoice, once reissued

bill_payment

ColumnTypeRequiredAllowed values or note
billing_account_iduuid → bill_billing_accountyes
received_ondateyes
amountbigint (cents)yes
currencytextyesMust match the account's currency
methodtextyescard, ach, wire, check, cash, other
referencetextnoA cheque number or similar; never a card or bank account number
provider_reftextnoThe payment provider's own reference
unapplied_amountbigint (cents)yesBetween 0 and amount

bill_payment_application

ColumnTypeRequiredAllowed values or note
payment_iduuid → bill_paymentyes
invoice_iduuid → bill_invoiceyes
amountbigint (cents)yesA mistake is reversed by a new row with a negative amount, never by an update
applied_ondateyes

bill_dunning_step

ColumnTypeRequiredAllowed values or note
days_overdueintegeryesUnique per tenant
channeltextyesemail, letter, call_task
template_keytextyes
stop_on_disputebooleanyes

bill_dunning_event

ColumnTypeRequiredAllowed values or note
invoice_iduuid → bill_invoiceyesOne event per invoice and step
dunning_step_iduuid → bill_dunning_stepyes
attimestamptzyes
outcometextyessent, skipped_dispute, skipped_paid, failed

bill_posting

ColumnTypeRequiredAllowed values or note
document_typetextyesinvoice, credit_note, payment
document_iduuidyesUnique per tenant together with document_type; a plain uuid, as the document may be in any of three tables
idempotency_keytextyesUnique per tenant
target_systemtextyesquickbooks, xero, netsuite, sage_intacct, other
statustextyesqueued, posted, failed, reversed
external_reftextnoThe accounting system's reference
attemptsintegeryesStarts at 0; one is added every time the service sends
last_errortextno
posted_attimestamptzno

Outside this module, the invoice also follows the shape of an electronic invoice: the EN 16931 semantic model defines what an invoice must be able to say, and Peppol BIS Billing 3.0 applies it to a network [1][3]. The tables are built so an export can be produced later, without a second data model.

Exercise · Sort your last twenty invoices30 minutes

You need: Your invoicing tool (or spreadsheet), a private folder outside the repository, and a copy of the table above

Customer names and amounts are private business data. Work in your own tool, keep any export in a private folder and never paste it into a chat or a prompt.

Outcome: A one-page inventory of twenty invoices by source, terms, edits and gaps. It tells you which parts of the module you will use most, and where today's process breaks the never-edit rule.

Knowledge check

A customer reports a wrong quantity on an invoice that was issued last week. What does the module do?

Knowledge check

Why does a draft invoice carry a temporary number instead of a real one?

References

  1. European Commission: EN 16931, the European standard on electronic invoicing. https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Obtaining+a+copy+of+the+European+standard+on+eInvoicing
  2. IRS Publication 583: Starting a Business and Keeping Records. https://www.irs.gov/publications/p583
  3. Peppol BIS Billing 3.0 (OpenPeppol). https://docs.peppol.eu/poacc/billing/3.0/
  4. ISO 4217 currency codes. https://www.iso.org/iso-4217-currency-codes.html

Chapter 2 · Sessions 4 and 5

Tax, corrections and cash

Three things go wrong after an invoice is drafted: the tax is the wrong rate for the date, the invoice itself is wrong, or the money arrives in a shape nobody expected. Each has one rule here, and the rule never involves editing history.

25 minDated tax ratesCredit notes onlyPayments split across invoices

By the end of this chapter you can

  • Choose the tax rate for an invoice from a dated rate table and keep it fixed afterwards.
  • Correct an invoice with a credit note and a reissue, and apply a payment to one or many invoices.
  • Describe a reminder schedule that stops when the invoice is paid or disputed.

Tax lines by ship-to and date

Each invoice line carries a tax code and a tax amount. The rate comes from one of two places: an integrated tax engine (best when the rules are complex, because the engine owns them) or, for simple cases, a dated rate table in bill_tax_rate. Either way, the rate depends on where the goods or service go, the ship-to, and on the date the rules say applies. Which date counts is a tax-rules question for the business's accountant; the module stores one date on the invoice and uses it for every line.

bill_tax_rate columnMeaning
jurisdictionThe place the rate applies to
tax_codeThe category the line carries, such as a standard or reduced rate
rateThe rate as a number
valid_from, valid_toThe first and last day the row applies; valid_to is empty on the current row

The lookup is a date test: the row where valid_from is on or before the date and valid_to is empty or on or after it. A rate change is a new row with a new valid_from, never an edit to the old one, in the same way a price book change is a new version.

Tax follows the dateA year drawn to scale with two dated rate rows: 6.00 percent until 30 June and 6.50 percent from 1 July. An invoice issued on 15 March uses 6.00 and one issued on 10 August uses 6.50. Example rates in bill_tax_rate (not a real jurisdiction)6.00% · valid_from 2026-01-016.50% · valid_from 2026-07-01JanFebMarAprMayJunJulAugSepOctNovDecissued 15 Maruses 6.00%issued 10 Auguses 6.50%The March invoice keeps 6.00% for ever. Looking up today's rate for it would rewrite history.
Tax follows the date. Two example rows drawn to scale over a year. An invoice issued on 15 March keeps the March rate for ever; the rate change on 1 July applies only to invoices dated on or after it.

A worked example in whole cents

A two-line invoice: 4 units at 2,500 cents and one service at 12,500 cents, so a subtotal of 22,500. At the example rate of 6.00% the lines carry tax of 600 and 750, a tax_total of 1,350 and a total of 23,850. At 6.50% the same lines carry 650 and 813 (812.5 rounded half up), a tax_total of 1,463 and a total of 23,963. Write the rounding rule down (per line or on the total, and which way a half cent goes), apply it everywhere, and test it, because it will be questioned the first time two invoices differ by one cent.

The rates in this chapter (6.00% and 6.50%) are examples for reading the figures. The practice seed in the sessions uses other made-up rates (5% and 7%), so its hand-counted totals differ from these; neither is a real jurisdiction's rate.

An exempt customer is handled by the account, not by the line: when tax_exempt is set and the certificate has not expired, lines carry zero tax and the certificate reference, so the audit trail shows why.

Credit notes: the only correction

A credit note credits an invoice in full or in part, with a reason code from the shared reason-code table, an amount and a tax amount held separately, and a status of draft, approved, issued or applied. A reissued invoice is linked back by reissued_invoice_id. Credit notes use the same gapless numbering as invoices, in their own series, and the same approval rule above a limit.

A correction is a credit noteThree boxes in a row: the issued invoice, a credit note against it with a reason code, and a reissued corrected invoice linked back by reissued_invoice_id. Issued invoiceINV-000123 · total 23,850status issued, hash storedCredit noteCN-000001 · full creditamount 22,500 + tax 1,350Reissued invoiceINV-000124 · correctedreissued_invoice_id links itNothing is overwritten: three documents, three numbers, one reason code on the credit note.A partial credit follows the same path and credits only the wrong part, tax included.
Invoice, credit note, reissue. Three documents each keep their own number. The credit note's reason code feeds the invoice accuracy KPI in chapter 3.

Electronic invoice formats treat a credit note as a document type of its own, with the same structure as an invoice and a reference to the invoice it corrects. UBL 2.1, the OASIS standard also published as ISO/IEC 19845, defines both an Invoice and a CreditNote [1]. Modelling it this way now means the tables are built so an export can be produced later, with no workaround.

Payments and their application

A payment records money received: the account, the date, the amount and currency (in the currency's minor unit [2]), the method (card, ach, wire, check, cash or other), the reference the customer gave, and the payment provider's reference. The module never stores a card number or bank account number; the payment provider holds them. A payment is then applied to invoices in bill_payment_application, one row for each invoice it pays.

Applying one paymentA bar to scale for a payment of 30,000 cents, split into 23,850 applied to one invoice, 4,000 applied to a second and 2,150 left unapplied. One payment received: 30,000 cents (bill_payment.amount)to INV-000123 · 23,850INV-0001254,000unapplied2,150bill_payment_application rows: 23,850 + 4,000 = 27,850bill_payment.unapplied_amount: 30,000 - 27,850 = 2,150, held as unapplied cashBalance due: INV-000123 23,850 to 0, INV-000125 4,000 to 0. Always: amount = applied + unapplied.
One payment, three outcomes. To scale: most of the payment settles one invoice, some settles a second, and the rest stays unapplied until someone decides what to do with it.
  • Partial payment: less than the invoice total. The status becomes partially_paid and balance_due falls by the amount applied.
  • Payment across invoices: one payment applied to several invoices, one application row each.
  • Overpayment: the excess stays in unapplied_amount as unapplied cash. It can be applied to a later invoice, or handed back by the payment provider outside this module (refunds are not built here); the payment itself is never edited to hide it.
  • The invariant: a payment's amount always equals its applications plus its unapplied_amount. A test can check this for every row.

Matching a bank statement line to an open invoice is a later capability. Bank statement and payment notification formats are defined by ISO 20022 (camt.053 for statements, camt.054 for notifications) [3]; the reference and provider_ref columns hold what a later remittance-matching step can use.

Statements and reminders

A statement lists a customer's open invoices, credits and payments at a date. Reminders are a schedule, not a habit: each bill_dunning_step says how many days overdue, which channel (email, letter or call_task), which template, and whether to stop on dispute. Each reminder that fires, or is skipped, is recorded in bill_dunning_event with an outcome of sent, skipped_dispute, skipped_paid or failed. A paid invoice simply leaves the ladder, so nothing more is recorded for it; skipped_paid is recorded only when a reminder was queued before the payment arrived and is then not sent.

The reminder ladderDays overdue from 0 to 60 drawn to scale with reminders at day 3, 14, 30 and 45. When the invoice stays unpaid all four are sent; when it is disputed on day 10 the later ones are skipped_dispute; when it is paid on day 20 the invoice leaves the ladder and nothing more is recorded (skipped_paid is recorded only for a reminder queued before the payment arrived). Example schedule: bill_dunning_step rows you chooseday 0day 10day 20day 30day 40day 50day 60Stays unpaidsent · emailsent · emailsent · call tasksent · letterDisputed on day 10sent · emailskipped_disputeskipped_disputeskipped_disputePaid on day 20sent · emailsent · emailnone: paidnone: paid
The same four reminders, three histories. Days overdue are drawn to scale. The days and channels are an example; you choose your own and they cost nothing to change. Sending email or letters may cost money through your provider, so choose the channel knowingly.
Exercise · Price one invoice by its date30 minutes

You need: One real issued invoice from your tool, a calculator, and your accountant's written statement of the tax rate and the date that applies

Use a single invoice with at least two lines. Do not guess a tax rate: take it from your accountant, your tax authority's page or your tax engine. Keep the invoice in a private folder.

Outcome: A short table showing one invoice repriced in whole cents from a dated rate row, with the rounding rule, any difference from the issued invoice explained, and the rule for the next rate change.

Knowledge check

A tax rate changes on 1 July. An invoice issued in March is shown in a report in August. Which rate does its tax use?

Knowledge check

A payment of 30,000 cents is applied 23,850 to one invoice and 4,000 to another. What is the payment's unapplied_amount?

References

  1. OASIS: Universal Business Language (UBL) Version 2.1. https://docs.oasis-open.org/ubl/UBL-2.1.html
  2. ISO 4217 currency codes. https://www.iso.org/iso-4217-currency-codes.html
  3. ISO 20022: financial messaging standard. https://www.iso20022.org/

Chapter 3 · Sessions 4 to 6

Posting once, and proving it

The invoice is only correct if the accounting system agrees with it, once. This chapter covers the posting contract, who may do what, how the module is measured, and how you move from the old tool without losing a cent.

25 min1 key per document5 roles0 cents difference

By the end of this chapter you can

  • Explain how an idempotency key stops a retry from posting twice.
  • Name the roles and screens that touch money and the rule each follows.
  • Reconcile the platform to the accounting system at cut-over and prove the module with its four KPIs.

The posting contract

Every invoice, credit note and payment is posted once to the accounting system. The contract is simple: the module writes a bill_posting row for the document with a unique idempotency key, a posting service sends the document and the key, and the accounting system answers with an acknowledgement and its own reference. The module does not keep a ledger; it keeps proof that each document reached the ledger exactly once.

The contract is signed: the adapter signs each request with a secret it shares only with the accounting side, kept in the host's environment settings, so the receiver can check who sent it and that nothing changed on the way. The database allows one posting row per document, whatever its key, so an opening posting loaded at cut-over is found and reused when that document is later paid or applied.

The posting contractSix steps: a document is issued, a bill_posting row is queued with an idempotency key, the posting service sends it, the accounting system makes one ledger entry, the acknowledgement returns an external reference, and the status becomes posted. Below, the statuses: queued goes to posted when acknowledged, or to failed after the last attempt, from which a retry returns it to queued; posted can later go to reversed. Document issuedinvoice, credit note or paymentPosting row queuedbill_posting + idempotency_keyPosting service sendsthe document and its keyAccounting systemone ledger entry per keyAcknowledgementexternal_ref comes backStatus postedattempts and posted_at keptbill_posting.status as the answer arrivesqueuedrow made before sendingpostedledger entry confirmedreversedundone by a reversalfailedafter the last attemptacknowledgedreversalretryNo acknowledgement: attempts + 1 and last_error kept, then a retry with backoff.After the last attempt: status failed and bill.posting.failed published.Every retry sends the same idempotency key.
The posting contract. The row is created before anything is sent, so a crash cannot lose a document, and its status is queued until the answer arrives, then posted, or failed after the last attempt; a posted document can later be reversed.
bill_posting columnWhy it is there
document_type, document_id (unique together)Which invoice, credit note or payment this is; one row per document, whatever its key
idempotency_key (unique)The same on every attempt; the database refuses a second row with the same key
target_systemquickbooks, xero, netsuite, sage_intacct or other
statusqueued, posted, failed or reversed (undone by a reversing entry, never by deleting)
external_ref, posted_atThe accounting system's reference and when it confirmed
attempts, last_errorHow many tries, and the latest reason a try failed

An idempotent retry

A network can fail after the accounting system has done the work but before the reply arrives. The sender cannot tell this from a failure before the work, so it must be safe to send again. An operation is idempotent when doing it twice has the same effect as doing it once. Payment and billing APIs achieve this by asking the caller to attach a key, and by returning the earlier result when the same key comes back. Build the key from the document, such as its type and id, so every attempt produces the same one.

A retry does not post twiceA sequence: the posting service sends a document with key K, the accounting system creates ledger entry E1, the reply is lost, the service retries with the same key K, and the accounting system returns E1 without creating a second entry. Posting serviceAccounting systemattempt 1 · key Kcreates E1Ledger1 entry (E1)reply lost: timeout, attempt recordedattempt 2 · the same key Kkey K already posted: returns E1Two attempts, one entry. A new key on each retry would have made two.
Two attempts, one entry. The reply to attempt 1 is lost. Attempt 2 carries the same key K, so the accounting system returns the entry it already made.

Where the accounting system does not honour keys itself, the adapter does the same work: it checks its own bill_posting row and looks the document up in the accounting system by the key, stored in the document's reference field, before it creates anything.

A failed posting is shown to a person with its last_error, can be retried from the postings screen, and publishes bill.posting.failed so another module or an alert can react. A retry never edits the document; it only repeats the posting.

Who may do what

RoleDoesNever
Billing clerkDrafts, issues and corrects invoices; records and applies payments; chases late payersApproves a document; updates a posting row
SalesReads the invoices and balances of their own customersSees another salesperson's customers
Invoice approverApproves invoices and credit notes that need a second personApproves one they created; creates invoices
Posting serviceSends each document to the accounting system and records the resultIs used by a person
Cut-over importerLoads the old tool's open items once, then is switched offUpdates anything; stays on after cut-over

Nobody deletes anywhere. Two rules are enforced twice, by a restrictive rule in the database and by a test: the approver cannot approve what they created, and only the posting service updates a posting. The screens follow the roles: invoice, payment and setup screens for the billing clerk (setup is where accounts, dated tax rates and reminder steps are kept), an approvals queue for the approver, and a board for open balance, ageing and the four KPIs.

Measuring the module

KPIDefinitionWhat a bad number says
DSODays sales outstanding: receivables ÷ credit sales × daysCustomers pay late, or invoices go out late
Invoice accuracyInvoices issued without a later credit note ÷ invoices issuedErrors reach customers
Time to invoiceMedian hours from shipment or completion to issueBilling waits on paper or people
Posting successDocuments posted first time ÷ documents issuedThe integration is fragile

Take each number once from the old tool before you build, and again from the new one after. For example, with receivables of 150,000 and credit sales of 450,000 over a 90-day period, DSO is 150,000 ÷ 450,000 × 90 = 30 days. The figures are illustrative; use your own.

Cut-over without losing a cent

Migration is a small, careful import, not a history transfer. Export the open invoices, customers and unapplied payments from the old tool; load each opening balance as one posting, in the same way as any other document, with a key that starts opening: so it can never clash with a live one; and issue new invoices from the cut-over date while the old system keeps its history. The importer role is created in Session 3, used only for the rehearsal and the cut-over run, and removed afterwards.

Reconciliation at cut-overTwo boxes, the platform's open balance per account and the accounting system's receivables, both point to a third box: the difference must be zero cents before the cut-over continues. Platform open balancesum of balance_due per accountat the cut-over momentAccounting receivablessame accounts, same momentfrom the accounting systemDifference: 0 centscontinue only at zeroAny other number is stopped and explained account by account, never rounded away.
Reconcile before you continue. The platform's open balance per account must equal the accounting system's receivables at the same moment. The only acceptable difference is zero cents.

Two further capabilities are built on the same tables. E-invoicing export produces an EN 16931 or UBL 2.1 document for customers or mandates that require it [2]. Remittance matching proposes which open invoices a bank statement line pays. Neither changes the rules above.

Records are kept as long as the rules that apply to the business require; the IRS's record-keeping guide lists what to retain [1]. Because issued invoices and postings are never deleted, retention is a matter of archiving, not of rebuilding.

Exercise · Break the retry on purpose30 minutes

You need: Your rehearsal database (never the live one), your AI coding agent, and the journal file or test double that stands in for the accounting system

Do this in the rehearsal environment you built in the sessions. The aim is to see the guarantee hold, then to see what it looks like when it does not.

Outcome: Three observed results: one ledger entry after a retry with the same key, two entries with a time-based key, and refusals for the invoice update and the duplicate key. Together they show the guarantees hold and what each one prevents.

Knowledge check

A posting times out after the accounting system recorded the entry. What makes the retry safe?

Knowledge check

At cut-over the platform's open balance and the accounting system's receivables differ. What do you do?

References

  1. IRS Publication 583: Starting a Business and Keeping Records. https://www.irs.gov/publications/p583
  2. European Commission: EN 16931, the European standard on electronic invoicing. https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Obtaining+a+copy+of+the+European+standard+on+eInvoicing

Chapter 4 · 12 questions · 80% passes

Final assessment

Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.

15 min12 questions≈ 15 minutesRetake allowed

Choose one answer for each question, then submit. You will see the right answer and why for every question.

1. What is the only way to correct an issued invoice?
2. A posting times out and the service retries. What stops the accounting system recording it twice?
3. A tax rate changes on 1 July. Which rate does an invoice issued on 15 March use when it is reopened in August?
4. A payment of 30,000 cents is applied 23,850 to one invoice and 4,000 to another. What is its unapplied_amount?
5. Which of these stays outside the module and is integrated instead?
6. Why does a draft invoice carry a temporary number?
7. Where are card and bank account details held?
8. A reminder falls due on an invoice marked disputed, and the step has stop_on_dispute true. What is recorded in bill_dunning_event?
9. Which KPI is invoices issued without a later credit note divided by invoices issued?
10. At cut-over the platform's open balance differs from the accounting system's receivables. What is the correct action?
11. Which standard defines the semantic model of an electronic invoice?
12. Why is money held as whole numbers of cents?