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.
| Shipped | v0.7.252 (post-sale prompt) → v0.7.259 (final polish) |
| Substrate | src/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:
| docLabel | Used for | Notes |
|---|---|---|
| Sale | Posted sale (transactions.transaction_type = 'sale') | Thermal-format default |
| Refund | Refund (refund) | Signs the money lines negative; header "Refund of TRX-YYYY-NNNNNN" |
| Estimate | Estimate (estimate) | Full-page Letter format, not thermal; terms + validity block; buildEstimateFromCart (v0.7.257) |
| Layaway / Deposit | Deposit (deposit_layaway / deposit_special_order) | Balance-owing line prominent |
| Invoice | Deposit pickup that rings the balance | Same 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.nameon 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 ontransaction_lines.serial_numberand reprints identically from every future re-render of the same doc; bothbuildSaleReceiptandbuildDetailReceiptfeed the samereceiptHtmlso 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_exemptinto 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
- Sales module — the transaction paths that fire the prompt
- Settings module — the letterhead + tax-number fields in
shop_config - Core keyboard layer — the Enter=Print / Esc=skip binding