Skip to main content

Receipt engine

Before v0.7.252 the Till printed a receipt and the detail panel printed a slightly different receipt and the estimate printed a receipt on estimate paper. Three renderers, three sets of near-parallel HTML, three chances for the shop's GST # to say one thing on the sale slip and another on the estimate. v0.7.252–.259 collapsed them into one code path: one receiptHtml() renderer, one openReceiptPrompt() post-transaction prompt, one letterhead read from shop_config.

Shippedv0.7.252 (post-sale prompt) → v0.7.259 (final polish)
Substratesrc/modules/sales/receipt.ts (or equivalent module-local, since receipts are Sales-shaped) + shop_config letterhead fields

The post-sale prompt​

openReceiptPrompt(txn) fires after every sale / refund / deposit / estimate. Three buttons: Print, Email-soon, No receipt. Enter = Print; Esc = skip (via the Core keyboard layer); localStorage carries an always-auto-print opt-in for an operator who's decided they want the receipt every time without the extra click.

Reprint offers the same delivery choices as the original sale (ISSUE-007, v0.6.995). When an operator triggers a reprint from a historic transaction, the prompt shows the same Print / Email-soon / No receipt buttons that fired the first time — no shortcut, no "assume print" — so a customer coming back for a re-email gets a real re-email option, not a paper receipt they didn't ask for.

On-order lines read the committed queue on first-print AND reprint (ISSUE-001, v0.6.987). The "on order — arriving soon" section on the receipt reads from the committed order queue at render time, so what the customer sees on the first printed receipt matches what they see on any later reprint of the same transaction. Before v0.6.987 the first print could show a projected list that later diverged from what actually got ordered.

Deposits get the same prompt with a balance-owing line the receipt renders (v0.7.296) — "Total $2,200 · $500 paid · balance owing $1,700" — and the header line reflects the deposit type (Layaway / Special order).

Gift-card issuance renders the issued card number in the receipt header (v0.7.326, audit Tier-1 #8) so the operator hands the customer a slip with the card number on it, not a slip that just says "gift card".

One receipt, five docLabels​

The docLabel parameter switches the receipt's headline for the transaction type:

docLabelUsed forNotes
SalePosted sale (transactions.transaction_type = 'sale')Thermal-format default
RefundRefund (refund)Signs the money lines negative; header "Refund of TRX-YYYY-NNNNNN"
EstimateEstimate (estimate)Full-page Letter format, not thermal; terms + validity block; buildEstimateFromCart (v0.7.257)
Layaway / DepositDeposit (deposit_layaway / deposit_special_order)Balance-owing line prominent
InvoiceDeposit pickup that rings the balanceSame layout as Sale

Every one is one function call on the same renderer with a different docLabel and the transaction snapshot. Changing the letterhead once changes it on every doc.

Industry-standard retail anatomy​

v0.7.253 pulled the receipt onto the retail-industry format:

  • Letterhead block — shop name, address, phone, GST # + PST # (from shop_config)
  • Transaction ref — TRX-YYYY-NNNNNN + occurred-at date/time
  • "Served by" — the cashier's initials (v0.7.255, from __actor.name on the transaction)
  • Line rows — SKU + qty @ unit-price + line total; humanized product name (no all-caps, no underscore-in-name). Serialized items (bikes, e-bike batteries + motors) render a bold S/N: <serial> row under the product name, on the thermal till/sale receipt and every reprint (v0.7.392) as well as the full-page Estimate (shipped earlier). See Sales — Serial-number capture. The serial is stored on transaction_lines.serial_number and reprints identically from every future re-render of the same doc; both buildSaleReceipt and buildDetailReceipt feed the same receiptHtml so first-print and reprint stay consistent.
  • Subtotal / discount / GST / PST / Total — right-aligned column; tax name humanized (v0.7.259 stripped the underscore from bike_pst_exempt into readable prose)
  • Tendered / Change — under Total for cash sales; brand + last-4 for card (v0.7.292 / .293 via payLabel())
  • Footer text — "Thank you" line + shop-configurable footer

Estimates print as a Letter quote​

v0.7.257 broke the estimate off the thermal-slip layout — an estimate goes to the customer as a full-page Letter quote with:

  • Full letterhead + shop registration numbers up top
  • Customer name + attention block
  • The estimated lines with per-line unit price + total
  • Terms + validity line ("Valid 30 days from the issue date …" — configurable)
  • Signature block

Print-on-estimate-create prompt lands the same way as a sale (v0.7.258) — Print / Email-soon / No receipt.

The unified detail-panel Print​

v0.7.256 unified the Till detail-panel Print button onto the same receiptHtml. A staffer reprinting a historic transaction from the ledger gets the exact same paper the original customer got; a staffer printing a refund gets the refund-flavoured version of the same receipt; a staffer printing a special-order pickup gets the invoice-flavoured version. One renderer, no drift.

What NOT to do​

  • Don't build a second receipt renderer for a special case. A new transaction type gets a new docLabel, not a new file.
  • Don't hard-code the letterhead in the renderer. Every field comes from shop_config — the fork template ships blank and the shop fills it in on Settings.
  • Don't auto-print without the opt-in. Auto-print is a per-operator decision; a busy Till is a bad place to guess at operator intent.
  • Don't stack a second "Print" prompt on top of the auto-print. Once the prompt's fired and the operator's dismissed it, the moment is over.

See also​