Waiter PWA fixes, and extra feautures. Also added Emergency Mode, search etc
This commit is contained in:
@@ -54,13 +54,32 @@ _DIVIDER_CHARS = {
|
||||
"empty": "",
|
||||
}
|
||||
|
||||
_PRINT_FONT_DEFAULTS = {
|
||||
"print.font_item_name": "16:0",
|
||||
"print.font_options": "0:0",
|
||||
"print.font_table": "16:0",
|
||||
"print.font_order_number": "48:1",
|
||||
"print.font_header": "48:1",
|
||||
_PRINT_SETTING_KEYS = [
|
||||
"print.ticket_mode",
|
||||
"print.divider_style",
|
||||
"print.font_order_number",
|
||||
"print.font_meta",
|
||||
"print.font_item_name",
|
||||
"print.font_quick",
|
||||
"print.font_pref",
|
||||
"print.font_extra",
|
||||
"print.font_ingredient",
|
||||
"print.font_item_note",
|
||||
"print.font_order_note",
|
||||
]
|
||||
|
||||
_PRINT_SETTING_DEFAULTS = {
|
||||
"print.ticket_mode": "detailed",
|
||||
"print.divider_style": "dash",
|
||||
"print.font_order_number": "48:1:0",
|
||||
"print.font_meta": "0:0:0",
|
||||
"print.font_item_name": "16:1:0",
|
||||
"print.font_quick": "0:0:0",
|
||||
"print.font_pref": "0:0:0",
|
||||
"print.font_extra": "0:0:0",
|
||||
"print.font_ingredient": "0:0:0",
|
||||
"print.font_item_note": "0:0:0",
|
||||
"print.font_order_note": "0:1:0",
|
||||
}
|
||||
|
||||
# SIZE byte values (ESC ! base, no bold bit):
|
||||
@@ -68,27 +87,28 @@ _PRINT_FONT_DEFAULTS = {
|
||||
# 16 = double-height (bit4)
|
||||
# 32 = double-width (bit5)
|
||||
# 48 = double-height + double-width (bits 4+5)
|
||||
# Bold is applied separately via ESC E.
|
||||
# Bold applied via ESC E, caps applied in software before encoding.
|
||||
|
||||
def _decode_font(value: str) -> tuple[int, bool]:
|
||||
"""Parse 'SIZE:BOLD' string → (esc_bang_byte, bold_flag)."""
|
||||
def _decode_font(value: str) -> tuple[int, bool, bool]:
|
||||
"""Parse 'SIZE:BOLD:CAPS' string → (esc_bang_byte, bold_flag, caps_flag)."""
|
||||
try:
|
||||
parts = str(value).split(":")
|
||||
size = int(parts[0])
|
||||
bold = len(parts) > 1 and parts[1] == "1"
|
||||
return size, bold
|
||||
caps = len(parts) > 2 and parts[2] == "1"
|
||||
return size, bold, caps
|
||||
except (ValueError, AttributeError):
|
||||
return 0, False
|
||||
return 0, False, False
|
||||
|
||||
|
||||
def _load_print_fonts(db: Session) -> dict:
|
||||
def _load_print_settings(db: Session) -> dict:
|
||||
rows = db.query(PosSettings).filter(
|
||||
PosSettings.key.in_(_PRINT_FONT_DEFAULTS.keys())
|
||||
PosSettings.key.in_(_PRINT_SETTING_KEYS)
|
||||
).all()
|
||||
fonts = dict(_PRINT_FONT_DEFAULTS)
|
||||
settings = dict(_PRINT_SETTING_DEFAULTS)
|
||||
for row in rows:
|
||||
fonts[row.key] = row.value
|
||||
return fonts
|
||||
settings[row.key] = row.value
|
||||
return settings
|
||||
|
||||
|
||||
def _divider(p: Network, style: str = "dash"):
|
||||
@@ -100,14 +120,42 @@ def _divider(p: Network, style: str = "dash"):
|
||||
p._raw(b'\n')
|
||||
|
||||
|
||||
def _item_line(name: str, qty: int) -> str:
|
||||
"""Build a dot-leader line: 'Club Sandwich . . . . 1' at 48 chars."""
|
||||
qty_str = str(qty)
|
||||
gap = LINE_WIDTH - len(name) - len(qty_str)
|
||||
if gap < 3:
|
||||
return f"{name} {qty_str}"
|
||||
dots = (". " * ((gap // 2) + 1))[:gap]
|
||||
return f"{name}{dots}{qty_str}"
|
||||
def _item_line(name: str, qty: int, line_width: int = LINE_WIDTH) -> str:
|
||||
"""Build a dot-leader line ending with 'xN'.
|
||||
line_width must reflect the effective width at the chosen font size
|
||||
(double-width fonts halve the available char count to 24)."""
|
||||
suffix = f"x{qty}"
|
||||
available = line_width - len(name) - len(suffix)
|
||||
if available < 2:
|
||||
# Name alone is too long — put qty on same line with a single space
|
||||
return f"{name} {suffix}"
|
||||
dots = (". " * ((available // 2) + 1))[:available]
|
||||
return f"{name}{dots}{suffix}"
|
||||
|
||||
|
||||
def _apply_font(p: Network, size: int, bold: bool):
|
||||
p._raw(bytes([0x1b, 0x21, size]))
|
||||
p._raw(b'\x1b\x45\x01' if bold else b'\x1b\x45\x00')
|
||||
|
||||
|
||||
def _reset_font(p: Network):
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
|
||||
|
||||
def _print_line(p: Network, text: str, size: int, bold: bool, caps: bool,
|
||||
align: bytes = b'\x1b\x61\x00'):
|
||||
"""Apply font, optionally capitalize, print text + newline, reset font."""
|
||||
p._raw(align)
|
||||
_apply_font(p, size, bold)
|
||||
out = text.upper() if caps else text
|
||||
_raw_text(p, out + "\n")
|
||||
_reset_font(p)
|
||||
|
||||
|
||||
def _greek_date(dt: datetime.datetime) -> str:
|
||||
"""Return date/time string in Greek format: HH:MM DD-MM-YYYY"""
|
||||
return dt.strftime("%H:%M %d-%m-%Y")
|
||||
|
||||
|
||||
def check_printer(ip: str, port: int) -> bool:
|
||||
@@ -152,88 +200,368 @@ def send_test_print(ip: str, port: int, name: str) -> Tuple[bool, str]:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def send_test_order_print(ip: str, port: int, db: Session) -> Tuple[bool, str]:
|
||||
"""Print a fake order using the current font/layout settings — for settings preview."""
|
||||
if _is_spoof_mode(db):
|
||||
logger.info("Spoof printing ON — dropping test order print")
|
||||
return True, ""
|
||||
|
||||
# ── Fake data structures (no DB writes) ──────────────────────────────────
|
||||
class _Table:
|
||||
label = "O2"
|
||||
number = 2
|
||||
|
||||
class _User:
|
||||
nickname = "bonamin"
|
||||
username = "bonamin"
|
||||
|
||||
class _Order:
|
||||
id = 99
|
||||
table = _Table()
|
||||
opener = _User()
|
||||
table_id = 2
|
||||
opened_by = 1
|
||||
notes = "Χωρις καψαλισμα παρακαλω"
|
||||
|
||||
class _Item:
|
||||
def __init__(self, product_id, quantity, selected_options, removed_ingredients, notes):
|
||||
self.product_id = product_id
|
||||
self.quantity = quantity
|
||||
self.selected_options = selected_options
|
||||
self.removed_ingredients = removed_ingredients
|
||||
self.notes = notes
|
||||
|
||||
import json as _json
|
||||
|
||||
items = [
|
||||
# Item 1: Freddo Espresso — quick options + preference + note
|
||||
_Item(
|
||||
product_id=1001,
|
||||
quantity=2,
|
||||
selected_options=_json.dumps([
|
||||
{"name": "Διπλος", "price_delta": 0.5, "type": "quick"},
|
||||
{"name": "Εξτρα ζαχαρη", "price_delta": 0.0, "type": "quick"},
|
||||
{"name": "Παγωμενος", "price_delta": 0.0, "type": "quick"},
|
||||
{"name": "Γαλα", "price_delta": 0.0, "type": "pref"},
|
||||
{"name": "Βρωμης", "price_delta": 0.3, "type": "pref_sub"},
|
||||
]),
|
||||
removed_ingredients=None,
|
||||
notes="Πολυ κρυο παρακαλω",
|
||||
),
|
||||
# Item 2: Club Sandwich — extra with sub + removed ingredients
|
||||
_Item(
|
||||
product_id=1002,
|
||||
quantity=1,
|
||||
selected_options=_json.dumps([
|
||||
{"name": "Extra Bacon", "price_delta": 1.5, "type": "extra"},
|
||||
{"name": "Τραγανο", "price_delta": 0.0, "type": "extra_sub"},
|
||||
{"name": "Extra Bacon", "price_delta": 1.5, "type": "extra"},
|
||||
{"name": "Τραγανο", "price_delta": 0.0, "type": "extra_sub"},
|
||||
{"name": "Ψωμι", "price_delta": 0.0, "type": "pref"},
|
||||
{"name": "Σικαλεως", "price_delta": 0.0, "type": "pref_sub"},
|
||||
]),
|
||||
removed_ingredients=_json.dumps(["Ντοματα", "Μουσταρδα"]),
|
||||
notes=None,
|
||||
),
|
||||
# Item 3: Margherita — quick + extra + removed
|
||||
_Item(
|
||||
product_id=1003,
|
||||
quantity=3,
|
||||
selected_options=_json.dumps([
|
||||
{"name": "Well Done", "price_delta": 0.0, "type": "quick"},
|
||||
{"name": "Extra Τυρι", "price_delta": 1.0, "type": "extra"},
|
||||
{"name": "Extra Τυρι", "price_delta": 1.0, "type": "extra"},
|
||||
{"name": "Extra Τυρι", "price_delta": 1.0, "type": "extra"},
|
||||
]),
|
||||
removed_ingredients=_json.dumps(["Ελιες", "Κρεμμυδι"]),
|
||||
notes=None,
|
||||
),
|
||||
]
|
||||
|
||||
# Patch product lookup so _print_kitchen_ticket gets real names
|
||||
_FAKE_NAMES = {1001: "Freddo Espresso", 1002: "Club Sandwich", 1003: "Margherita Pizza"}
|
||||
|
||||
# Monkey-patch db.query for Product only inside this call
|
||||
_orig_query = db.query
|
||||
|
||||
class _FakeQuery:
|
||||
def __init__(self, model):
|
||||
self._model = model
|
||||
self._filter_id = None
|
||||
def filter(self, *args):
|
||||
# extract id from the filter expression value
|
||||
for arg in args:
|
||||
try:
|
||||
self._filter_id = arg.right.value
|
||||
except Exception:
|
||||
pass
|
||||
return self
|
||||
def first(self):
|
||||
if self._model.__name__ == "Product" and self._filter_id in _FAKE_NAMES:
|
||||
class _P:
|
||||
name = _FAKE_NAMES[self._filter_id]
|
||||
return _P()
|
||||
return _orig_query(self._model).filter(self._model.id == self._filter_id).first()
|
||||
|
||||
class _PatchedDB:
|
||||
def query(self, model):
|
||||
from models.product import Product as _Product
|
||||
if model is _Product:
|
||||
return _FakeQuery(model)
|
||||
return _orig_query(model)
|
||||
# delegate everything else to real db
|
||||
def __getattr__(self, name):
|
||||
return getattr(db, name)
|
||||
|
||||
try:
|
||||
p = _get_printer(ip, port)
|
||||
_print_kitchen_ticket(p, _Order(), items, _PatchedDB())
|
||||
p.close()
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
logger.error("Test order print failed for %s:%s — %s", ip, port, e)
|
||||
return False, str(e)
|
||||
|
||||
|
||||
# ── Receipt formatting ───────────────────────────────────────────────────────
|
||||
|
||||
def _font(p: Network, byte_val: int, bold: bool = False):
|
||||
p._raw(bytes([0x1b, 0x21, byte_val]))
|
||||
p._raw(b'\x1b\x45\x01' if bold else b'\x1b\x45\x00')
|
||||
def _parse_options(item: OrderItem) -> dict:
|
||||
"""
|
||||
Parse selected_options JSON into grouped dict:
|
||||
{ 'quick': [(name, qty)], 'pref': [(name, sub|None)],
|
||||
'extra': [(name, sub|None, qty)], 'unknown': [name] }
|
||||
Falls back gracefully when type tags are absent (old data).
|
||||
"""
|
||||
result = {"quick": [], "pref": [], "extra": [], "unknown": []}
|
||||
if not item.selected_options:
|
||||
return result
|
||||
|
||||
try:
|
||||
raw = json.loads(item.selected_options)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return result
|
||||
|
||||
if not isinstance(raw, list):
|
||||
return result
|
||||
|
||||
i = 0
|
||||
while i < len(raw):
|
||||
entry = raw[i]
|
||||
if not isinstance(entry, dict):
|
||||
i += 1
|
||||
continue
|
||||
name = entry.get("name") or ""
|
||||
etype = entry.get("type")
|
||||
|
||||
# Peek at next entry to collect sub-choice
|
||||
sub = None
|
||||
if i + 1 < len(raw):
|
||||
nxt = raw[i + 1]
|
||||
if isinstance(nxt, dict) and nxt.get("type") in ("pref_sub", "extra_sub"):
|
||||
sub = nxt.get("name") or ""
|
||||
i += 1 # consume sub
|
||||
|
||||
if etype == "quick":
|
||||
# Collapse repeated quick entries into a single (name, qty) tuple
|
||||
existing = next((q for q in result["quick"] if q[0] == name), None)
|
||||
if existing:
|
||||
result["quick"][result["quick"].index(existing)] = (name, existing[1] + 1)
|
||||
else:
|
||||
result["quick"].append((name, 1))
|
||||
elif etype == "pref":
|
||||
result["pref"].append((name, sub))
|
||||
elif etype == "extra":
|
||||
# Collapse repeated extra entries (same name+sub) → (name, sub, qty)
|
||||
existing = next((e for e in result["extra"] if e[0] == name and e[1] == sub), None)
|
||||
if existing:
|
||||
result["extra"][result["extra"].index(existing)] = (name, sub, existing[2] + 1)
|
||||
else:
|
||||
result["extra"].append((name, sub, 1))
|
||||
else:
|
||||
# Legacy data without type tag — treat as unknown, display plainly
|
||||
if name:
|
||||
result["unknown"].append(name + (f" · {sub}" if sub else ""))
|
||||
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db: Session):
|
||||
fonts = _load_print_fonts(db)
|
||||
div = fonts["print.divider_style"]
|
||||
cfg = _load_print_settings(db)
|
||||
mode = cfg.get("print.ticket_mode", "detailed")
|
||||
div = cfg.get("print.divider_style", "dash")
|
||||
compact = (mode == "compact")
|
||||
|
||||
sz_order, bold_order = _decode_font(fonts["print.font_order_number"])
|
||||
sz_table, bold_table = _decode_font(fonts["print.font_table"])
|
||||
sz_item, bold_item = _decode_font(fonts["print.font_item_name"])
|
||||
sz_opt, bold_opt = _decode_font(fonts["print.font_options"])
|
||||
sz_ord, b_ord, c_ord = _decode_font(cfg["print.font_order_number"])
|
||||
sz_meta, b_meta, c_meta = _decode_font(cfg["print.font_meta"])
|
||||
sz_item, b_item, c_item = _decode_font(cfg["print.font_item_name"])
|
||||
sz_qk, b_qk, c_qk = _decode_font(cfg["print.font_quick"])
|
||||
sz_pr, b_pr, c_pr = _decode_font(cfg["print.font_pref"])
|
||||
sz_ex, b_ex, c_ex = _decode_font(cfg["print.font_extra"])
|
||||
sz_ing, b_ing, c_ing = _decode_font(cfg["print.font_ingredient"])
|
||||
sz_note, b_note, c_note = _decode_font(cfg["print.font_item_note"])
|
||||
sz_onote,b_onote,c_onote= _decode_font(cfg["print.font_order_note"])
|
||||
|
||||
# Header — order number
|
||||
p._raw(b'\x1b\x61\x01')
|
||||
_font(p, sz_order, bold_order)
|
||||
_raw_text(p, f"Παραγγελια #{order.id}\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
_divider(p, div)
|
||||
# Resolve display names
|
||||
table_name = order.table.label or str(order.table.number) if order.table else str(order.table_id)
|
||||
waiter_nick = (order.opener.nickname or order.opener.username) if order.opener else str(order.opened_by)
|
||||
now_str = _greek_date(datetime.datetime.now())
|
||||
|
||||
# Meta — table / waiter / time
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
_font(p, sz_table, bold_table)
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
_raw_text(p, f"Date: {now}\n")
|
||||
_raw_text(p, f"Table: {order.table_id}\n")
|
||||
_raw_text(p, f"Waiter: {order.opened_by}\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
_divider(p, div)
|
||||
# ── COMPACT header — single line ────────────────────────────────────────
|
||||
if compact:
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
_apply_font(p, sz_ord, b_ord)
|
||||
header = f"Παρ. #{order.id} | Τρ. {table_name} | {now_str} | {waiter_nick}"
|
||||
_raw_text(p, (header.upper() if c_ord else header) + "\n")
|
||||
_reset_font(p)
|
||||
_divider(p, div)
|
||||
|
||||
# ── DETAILED header ──────────────────────────────────────────────────────
|
||||
else:
|
||||
_print_line(p, f"Παραγγελια #{order.id}", sz_ord, b_ord, c_ord,
|
||||
align=b'\x1b\x61\x01')
|
||||
_divider(p, div)
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
_apply_font(p, sz_meta, b_meta)
|
||||
_raw_text(p, ("ΤΡΑΠΕΖΙ:" if c_meta else "Τραπεζι:") + f" Τραπεζι {table_name}\n")
|
||||
_raw_text(p, ("ΗΜΕΡΟΜΗΝΙΑ:" if c_meta else "Ημερομηνια:") + f" {now_str}\n")
|
||||
_raw_text(p, ("ΣΕΡΒΙΤΟΡΟΣ:" if c_meta else "Σερβιτορος:") + f" {waiter_nick}\n")
|
||||
_reset_font(p)
|
||||
_divider(p, div)
|
||||
|
||||
# ── Items ────────────────────────────────────────────────────────────────
|
||||
# Double-width fonts halve the effective character width
|
||||
item_line_width = LINE_WIDTH // 2 if sz_item in (32, 48) else LINE_WIDTH
|
||||
|
||||
# Items
|
||||
for item in items:
|
||||
product = db.query(Product).filter(Product.id == item.product_id).first()
|
||||
name = product.name if product else f"Product #{item.product_id}"
|
||||
raw_name = product.name if product else f"Product #{item.product_id}"
|
||||
item_name = raw_name.upper() if c_item else raw_name
|
||||
|
||||
_font(p, sz_item, bold_item)
|
||||
_raw_text(p, _item_line(name, item.quantity) + "\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
_apply_font(p, sz_item, b_item)
|
||||
_raw_text(p, _item_line(item_name, item.quantity, item_line_width) + "\n")
|
||||
_reset_font(p)
|
||||
|
||||
_font(p, sz_opt, bold_opt)
|
||||
opts = _parse_options(item)
|
||||
|
||||
# Quick options (* marker)
|
||||
if opts["quick"]:
|
||||
if compact:
|
||||
parts = []
|
||||
for name, qty in opts["quick"]:
|
||||
n = name.upper() if c_qk else name
|
||||
parts.append(f"{n} x{qty}" if qty > 1 else n)
|
||||
_apply_font(p, sz_qk, b_qk)
|
||||
_raw_text(p, "* " + " | ".join(parts) + "\n")
|
||||
_reset_font(p)
|
||||
else:
|
||||
for name, qty in opts["quick"]:
|
||||
n = name.upper() if c_qk else name
|
||||
line = f"* {n} x{qty}" if qty > 1 else f"* {n}"
|
||||
_apply_font(p, sz_qk, b_qk)
|
||||
_raw_text(p, line + "\n")
|
||||
_reset_font(p)
|
||||
|
||||
# Preferences (> marker)
|
||||
if opts["pref"]:
|
||||
if compact:
|
||||
parts = []
|
||||
for name, sub in opts["pref"]:
|
||||
n = name.upper() if c_pr else name
|
||||
s = (sub.upper() if c_pr else sub) if sub else None
|
||||
parts.append(f"{n} · {s}" if s else n)
|
||||
_apply_font(p, sz_pr, b_pr)
|
||||
_raw_text(p, "> " + " | ".join(parts) + "\n")
|
||||
_reset_font(p)
|
||||
else:
|
||||
for name, sub in opts["pref"]:
|
||||
n = name.upper() if c_pr else name
|
||||
s = (sub.upper() if c_pr else sub) if sub else None
|
||||
line = f"> {n} · {s}" if s else f"> {n}"
|
||||
_apply_font(p, sz_pr, b_pr)
|
||||
_raw_text(p, line + "\n")
|
||||
_reset_font(p)
|
||||
|
||||
# Extras (+ marker)
|
||||
if opts["extra"]:
|
||||
if compact:
|
||||
parts = []
|
||||
for name, sub, qty in opts["extra"]:
|
||||
n = name.upper() if c_ex else name
|
||||
s = (sub.upper() if c_ex else sub) if sub else None
|
||||
part = f"{n} · {s}" if s else n
|
||||
if qty > 1:
|
||||
part += f" · x{qty}"
|
||||
parts.append(part)
|
||||
_apply_font(p, sz_ex, b_ex)
|
||||
_raw_text(p, "+ " + " | ".join(parts) + "\n")
|
||||
_reset_font(p)
|
||||
else:
|
||||
for name, sub, qty in opts["extra"]:
|
||||
n = name.upper() if c_ex else name
|
||||
s = (sub.upper() if c_ex else sub) if sub else None
|
||||
line = f"+ {n}"
|
||||
if s:
|
||||
line += f" · {s}"
|
||||
if qty > 1:
|
||||
line += f" · x{qty}"
|
||||
_apply_font(p, sz_ex, b_ex)
|
||||
_raw_text(p, line + "\n")
|
||||
_reset_font(p)
|
||||
|
||||
# Legacy untagged options
|
||||
for entry in opts["unknown"]:
|
||||
_apply_font(p, sz_ex, b_ex)
|
||||
_raw_text(p, f"+ {entry}\n")
|
||||
_reset_font(p)
|
||||
|
||||
# Removed ingredients (- marker)
|
||||
if item.removed_ingredients:
|
||||
try:
|
||||
removed_ids = json.loads(item.removed_ingredients)
|
||||
if removed_ids:
|
||||
_raw_text(p, f" - χωρις: {', '.join(str(i) for i in removed_ids)}\n")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
if item.selected_options:
|
||||
try:
|
||||
option_ids = json.loads(item.selected_options)
|
||||
if option_ids:
|
||||
_raw_text(p, f" + επιλογες: {', '.join(str(i) for i in option_ids)}\n")
|
||||
removed = json.loads(item.removed_ingredients)
|
||||
if removed:
|
||||
names = [n.upper() if c_ing else n for n in removed]
|
||||
joined = " · ".join(names)
|
||||
_apply_font(p, sz_ing, b_ing)
|
||||
_raw_text(p, f"- ΧΩΡΙΣ: {joined}\n")
|
||||
_reset_font(p)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
# Per-item note
|
||||
if item.notes:
|
||||
_raw_text(p, f" (i) {item.notes}\n")
|
||||
note_text = item.notes.upper() if c_note else item.notes
|
||||
_apply_font(p, sz_note, b_note)
|
||||
if compact:
|
||||
_raw_text(p, f"! {note_text}\n")
|
||||
else:
|
||||
_raw_text(p, f"\n(!) {note_text}\n\n")
|
||||
_reset_font(p)
|
||||
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
# Blank line between items in detailed mode
|
||||
if not compact:
|
||||
p._raw(b'\n')
|
||||
|
||||
_divider(p, div)
|
||||
|
||||
# Order-level notes
|
||||
if order.notes:
|
||||
p._raw(b'\x1b\x21\x30')
|
||||
_raw_text(p, "Σημειωσεις:\n")
|
||||
p._raw(b'\x1b\x21\x10')
|
||||
_raw_text(p, f"{order.notes}\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p)
|
||||
note_text = order.notes.upper() if c_onote else order.notes
|
||||
_apply_font(p, sz_onote, b_onote)
|
||||
_raw_text(p, f"Σημ: {note_text}\n")
|
||||
_reset_font(p)
|
||||
if not compact:
|
||||
_divider(p, div)
|
||||
|
||||
# Footer (detailed only)
|
||||
if not compact:
|
||||
p._raw(b'\x1b\x61\x01')
|
||||
p._raw(b'\x1b\x21\x30')
|
||||
_raw_text(p, "Τελος Παραγγελιας\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
|
||||
p._raw(b'\x1b\x61\x01')
|
||||
p._raw(b'\x1b\x21\x30')
|
||||
_raw_text(p, "Τελος Παραγγελιας\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user