RECORDGOD/app/receipts.py
type-two bf08cd9b35 feat(pos): receipt logo + Monster Robot template (store name/address/footer)
receipts.py renders store_logo (img) above the title; store_logo added to sales
settings (get/save) + a Logo URL field in POS Settings. Logo hosted at
site/receipt-logo.webp (Monster Robot, from WowPlatter receipt_templates). Print
window now waits for window.onload so the logo prints. Deploy via /opt/recordgod/deploy.sh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:33:08 +10:00

88 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import html as _html
import json
def _e(s):
return _html.escape(str(s if s is not None else ""))
def render_receipt_html(sale, items, customer, st):
"""One self-contained receipt (inline CSS) used by both the print window and the email body."""
cur = st.get("currency_symbol", "$")
def m(v):
try:
return f"{cur}{float(v or 0):.2f}"
except (TypeError, ValueError):
return f"{cur}0.00"
store = st.get("store_name") or "RecordGod"
addr = st.get("store_address") or ""
footer = st.get("receipt_footer") or "thanks for digging"
logo = st.get("store_logo") or ""
logo_html = (f'<div class="ct" style="margin-bottom:4px"><img src="{_e(logo)}" alt="" '
f'style="max-height:72px;max-width:200px;object-fit:contain"></div>') if logo else ""
date = str(sale.get("sale_date") or "")[:16].replace("T", " ")
who = (customer or {}).get("name") or "Guest"
rows = ""
for i in items:
disc = float(i.get("discount") or 0)
dtag = f' <span style="color:#c2185b">{m(disc)}</span>' if disc > 0 else ""
rows += (f'<tr><td>{i.get("qty",1)}× {_e(i.get("item_name") or i.get("sku"))}{dtag}</td>'
f'<td style="text-align:right">{m(i.get("line_total"))}</td></tr>')
sub = sale.get("subtotal")
disc_total = float(sale.get("discount_amount") or 0)
tax = float(sale.get("tax_amount") or 0)
incl = (st.get("tax_type") or "exclusive") == "inclusive"
trade = float(sale.get("trade_in_credit") or 0)
tot = float(sale.get("total") or 0)
summ = f'<tr><td>Subtotal</td><td style="text-align:right">{m(sub)}</td></tr>'
if disc_total > 0:
summ += f'<tr><td>Discount</td><td style="text-align:right">{m(disc_total)}</td></tr>'
if tax > 0:
summ += f'<tr><td>GST{" incl" if incl else ""} ({st.get("tax_rate","0")}%)</td><td style="text-align:right">{m(tax)}</td></tr>'
if trade > 0:
summ += f'<tr><td>Trade-in credit</td><td style="text-align:right">{m(trade)}</td></tr>'
# split-payment lines, else the single method + (cash) tender/change
pay = ""
method = sale.get("payment_method") or ""
splits = sale.get("split_payments")
if isinstance(splits, str):
try:
splits = json.loads(splits)
except ValueError:
splits = None
if splits and isinstance(splits, list):
for p in splits:
if p.get("method") and p.get("amount"):
pay += f'<tr><td>{_e(p["method"].title())}</td><td style="text-align:right">{m(p["amount"])}</td></tr>'
else:
pay += f'<tr><td>{_e(method.title() or "Paid")}</td><td style="text-align:right">{m(sale.get("amount_paid", tot))}</td></tr>'
tendered = sale.get("amount_tendered")
if method == "cash" and tendered:
pay += f'<tr><td>Tendered</td><td style="text-align:right">{m(tendered)}</td></tr>'
pay += f'<tr><td>Change</td><td style="text-align:right">{m(float(tendered) - tot)}</td></tr>'
bal = round(tot - float(sale.get("amount_paid") or 0), 2)
bal_line = (f'<tr><td style="color:#c2185b">Balance due</td>'
f'<td style="text-align:right;color:#c2185b">{m(bal)}</td></tr>') if bal > 0.005 else ""
notes = f'<div class="ct" style="margin-top:6px">{_e(sale.get("notes"))}</div>' if sale.get("notes") else ""
return f"""<div class="receipt-container" style="font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;line-height:1.6;color:#111;max-width:300px;margin:0 auto;padding:6px">
<style>.receipt-container td{{padding:1px 0}} .receipt-container table{{width:100%;border-collapse:collapse}} .receipt-container .hr{{border-top:1px dashed #999;margin:7px 0}} .receipt-container .ct{{text-align:center}} @media print{{.receipt-actions{{display:none}}}}</style>
{logo_html}<div class="ct" style="font-weight:700;font-size:15px">{_e(store)}</div>
{f'<div class="ct" style="color:#555">{_e(addr)}</div>' if addr else ''}
<div class="ct" style="color:#555">{_e(sale.get('sale_number',''))} · {_e(date)} · {_e(who)}</div>
<div class="hr"></div>
<table>{rows}</table>
<div class="hr"></div>
<table>{summ}<tr style="font-weight:700"><td>Total</td><td style="text-align:right">{m(tot)}</td></tr>{pay}{bal_line}</table>
{notes}
<div class="hr"></div>
<div class="ct" style="color:#555">{_e(footer)}</div>
</div>"""