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>
88 lines
4.3 KiB
Python
88 lines
4.3 KiB
Python
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>"""
|