2024-08-05 05:10:47 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# Print a fake receipt
|
|
|
|
|
2024-08-24 21:25:43 +00:00
|
|
|
import re
|
|
|
|
import textwrap
|
2024-08-05 05:10:47 +00:00
|
|
|
from datetime import datetime
|
2024-08-24 21:25:43 +00:00
|
|
|
|
2024-08-05 05:10:47 +00:00
|
|
|
from escpos import printer
|
|
|
|
|
|
|
|
items = [
|
|
|
|
{"sku": 666, "desc": "Dragon Tears", "price": 5.6},
|
|
|
|
{"sku": 661, "desc": "Lizard Tongue", "price": 7.77},
|
|
|
|
{"sku": 911, "desc": "Bat Wing", "price": 13.7},
|
|
|
|
{"sku": 42, "desc": "Towel", "price": 19.99},
|
|
|
|
]
|
|
|
|
tax_percent = 0.05
|
|
|
|
header = {"sku": "SKU", "desc": "Product Description", "price": "Price"}
|
|
|
|
header_format = "{sku:<6} {desc:<33} {price:<6}"
|
|
|
|
item_format = "{sku:>06} {desc:<33} {price:>6.2f}"
|
|
|
|
subtotals_format = "{dummy:6} {desc:>33} {price:>6.2f}"
|
|
|
|
|
|
|
|
# As good as time as any
|
|
|
|
timestamp = datetime(2012, 12, 21, 11, 59, 59)
|
|
|
|
served_by = "Imruryg the Brave"
|
|
|
|
|
|
|
|
address = """57 Dandelion Tower Drive
|
|
|
|
Glimmerhollow, TQ, 981-PPU
|
|
|
|
Phone: +1-403-555-2106"""
|
|
|
|
|
|
|
|
disclaimer = """For entertainment purposes only.
|
|
|
|
Do not use for summoning demons and/or conjuration of spirits.
|
|
|
|
Magick should only be perfomed by trained professionals.
|
|
|
|
All sales are final. No refunds or exchanges for enchanted items.
|
|
|
|
Hocus Pocus will not be responsible for any damages, injuries, or
|
|
|
|
losses that occur while using or misusing these items.
|
|
|
|
Always check local bylaws and regulations before invoking any spells.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Font "b" on my TM-P80-clone printer can squeeze 64 characters per line
|
|
|
|
disclaimer_width = 64
|
|
|
|
|
|
|
|
recipt_barcode = "1234567890"
|
|
|
|
|
|
|
|
|
|
|
|
# Justify-text on left AND right sides by padding spaces,
|
|
|
|
# code by: Georgina Skibinski https://stackoverflow.com/a/66087666
|
|
|
|
def justify(txt: str, width: int) -> str:
|
|
|
|
prev_txt = txt
|
|
|
|
while (l := width - len(txt)) > 0:
|
|
|
|
txt = re.sub(r"(\s+)", r"\1 ", txt, count=l)
|
|
|
|
if txt == prev_txt:
|
|
|
|
break
|
|
|
|
return txt.rjust(width)
|
|
|
|
|
|
|
|
|
|
|
|
p = printer.Usb(0x04B8, 0x0E20, profile="TM-P80")
|
|
|
|
|
|
|
|
# Store Logo at the Top
|
|
|
|
p.set_with_default()
|
|
|
|
p.image("hocus-pocus.gif", center=True)
|
|
|
|
|
|
|
|
# Print Address, centered
|
|
|
|
p.ln(1)
|
|
|
|
p.set_with_default(align="center")
|
|
|
|
for l in address.split("\n"):
|
|
|
|
p.textln(l)
|
|
|
|
p.ln(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Print date and time
|
|
|
|
p.set_with_default(align="left")
|
|
|
|
p.textln(timestamp.strftime("%A, %B %d, %Y %I:%M%P"))
|
|
|
|
|
|
|
|
# Print cashier's name
|
|
|
|
p.set_with_default(align="right")
|
|
|
|
p.textln("Served by: " + served_by)
|
|
|
|
|
|
|
|
# Add some empty space before itemized list
|
|
|
|
p.ln(2)
|
|
|
|
|
|
|
|
|
|
|
|
## Add a bit of line spacing for itemized list for easier reading
|
|
|
|
p.set_with_default()
|
|
|
|
p.line_spacing(80, 180)
|
|
|
|
|
|
|
|
## Itemized list header (bold with underline)
|
|
|
|
p.set_with_default(bold=True, underline=True)
|
|
|
|
p.textln(header_format.format(**header))
|
|
|
|
|
|
|
|
## Itemized List
|
|
|
|
p.set_with_default()
|
|
|
|
for idx, item in enumerate(items):
|
|
|
|
txt = item_format.format(**item)
|
|
|
|
if idx == len(items) - 1:
|
|
|
|
# If this is the last item, add underline
|
|
|
|
# to visually "close" the list.
|
|
|
|
p.set_with_default(underline=True)
|
|
|
|
p.textln(txt)
|
|
|
|
|
|
|
|
p.set_with_default()
|
|
|
|
|
|
|
|
## Subtotal
|
|
|
|
subtotal = sum([x["price"] for x in items])
|
|
|
|
p.textln(subtotals_format.format(dummy="", desc="subtotal", price=subtotal))
|
|
|
|
|
|
|
|
## Tax
|
|
|
|
tax_amount = subtotal * tax_percent
|
|
|
|
tax_desc = "Guild Tax (%d%%)" % (int(tax_percent * 100.0))
|
|
|
|
p.textln(subtotals_format.format(dummy="", desc=tax_desc, price=tax_amount))
|
|
|
|
|
|
|
|
## Total
|
|
|
|
## NOTE: because we use double-sized font, alignment won't match
|
|
|
|
## the previous lines. Instead, with trim leading whitespace,
|
|
|
|
## and use the printer's built-in right-alignment feature.
|
|
|
|
p.set_with_default(align="right", custom_size=True, width=2, height=2)
|
|
|
|
total_amount = subtotal + tax_amount
|
|
|
|
total_desc = "Total"
|
|
|
|
total_text_line = subtotals_format.format(dummy="", desc=total_desc, price=total_amount)
|
|
|
|
total_text_line = total_text_line.strip()
|
|
|
|
p.textln(total_text_line)
|
|
|
|
|
|
|
|
# Add some empty space before disclaimer
|
|
|
|
p.ln(4)
|
|
|
|
|
|
|
|
# preprocess disclaimer text:
|
|
|
|
# In python it's easy to use multilined strings,
|
|
|
|
# but to print the disclaimer we want to merge lines
|
|
|
|
# and condense whitespaces.
|
|
|
|
txt = disclaimer.replace("\n", " ")
|
|
|
|
txt = re.sub(" +", " ", txt)
|
|
|
|
# textwrap.wrap() ensures words are not broken (unlike "escpos.block_text()").
|
|
|
|
txt = textwrap.wrap(txt, width=disclaimer_width)
|
|
|
|
# Justify each line
|
|
|
|
txt = [justify(x, disclaimer_width) for x in txt]
|
|
|
|
p.set_with_default(font="b")
|
|
|
|
for l in txt:
|
|
|
|
p.textln(l)
|
|
|
|
|
|
|
|
# A creature for good luck
|
|
|
|
p.set_with_default()
|
|
|
|
p.ln(2)
|
|
|
|
p.image("creature5.gif", center=True)
|
|
|
|
|
|
|
|
p.cut(mode="PART", feed=True)
|