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.

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 refTRX-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)
  • 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