general fixes and ordering display overhaul
This commit is contained in:
@@ -177,6 +177,10 @@ def _run_migrations():
|
||||
"ALTER TABLE categories ADD COLUMN general_sort_order INTEGER NOT NULL DEFAULT 0",
|
||||
# Auto-expand flag for sub-categories on the PWA accordion
|
||||
"ALTER TABLE categories ADD COLUMN auto_expanded INTEGER NOT NULL DEFAULT 0",
|
||||
# Printer protocol field
|
||||
"ALTER TABLE printers ADD COLUMN protocol VARCHAR NOT NULL DEFAULT 'escpos_tcp'",
|
||||
# Compact (half-width) display flag for quick options
|
||||
"ALTER TABLE product_quick_options ADD COLUMN is_compact INTEGER NOT NULL DEFAULT 0",
|
||||
]
|
||||
for sql in migrations:
|
||||
try:
|
||||
|
||||
@@ -11,6 +11,7 @@ class Printer(Base):
|
||||
ip_address = Column(String, nullable=False)
|
||||
port = Column(Integer, default=9100, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
protocol = Column(String, default="escpos_tcp", nullable=False)
|
||||
|
||||
products = relationship("Product", back_populates="printer_zone")
|
||||
print_logs = relationship("PrintLog", back_populates="printer")
|
||||
|
||||
@@ -72,6 +72,7 @@ class ProductQuickOption(Base):
|
||||
sort_order = Column(Integer, default=0, nullable=False)
|
||||
is_favorite = Column(Boolean, default=False, nullable=False)
|
||||
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
||||
is_compact = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
product = relationship("Product", back_populates="quick_options")
|
||||
|
||||
|
||||
@@ -1,305 +1,343 @@
|
||||
"""
|
||||
Printer font & symbol test script.
|
||||
Usage (inside Docker): python print_test.py [IP] [PORT]
|
||||
Defaults to 10.98.20.25:9100
|
||||
Printer comprehensive test script — Jolimark TP850UE
|
||||
Usage: python print_test.py [IP] [PORT]
|
||||
Default: 10.98.20.25:9100
|
||||
|
||||
Prints 6 pages:
|
||||
Page 1 — ESC ! modes, Font A, English
|
||||
Page 2 — ESC ! modes, Font B, English
|
||||
Page 3 — ESC ! modes, Font A, Greek
|
||||
Page 4 — ESC ! modes, Font B, Greek
|
||||
Page 5 — GS ! character size multipliers (both fonts)
|
||||
Page 6 — Beep tests + misc (underline, invert, symbols)
|
||||
|
||||
ESC ! (0x1B 0x21 n) correct bit map for TP850UE:
|
||||
Bit 0 (0x01) — Font B instead of Font A
|
||||
Bit 3 (0x08) — Emphasize / Bold
|
||||
Bit 4 (0x10) — Double-height
|
||||
Bit 5 (0x20) — Double-width
|
||||
Bit 7 (0x80) — Underline
|
||||
|
||||
GS ! (0x1D 0x21 n) character size multiplier:
|
||||
Low nibble (bits 0-3): height multiplier (0=1x, 1=2x, 2=3x … 7=8x)
|
||||
High nibble (bits 4-7): width multiplier (0=1x, 1=2x, 2=3x … 7=8x)
|
||||
e.g. n=0x00 → 1×1, n=0x11 → 2×2, n=0x22 → 3×3, n=0x77 → 8×8
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
|
||||
PRINTER_IP = sys.argv[1] if len(sys.argv) > 1 else "10.98.20.25"
|
||||
PRINTER_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 9100
|
||||
|
||||
from escpos.printer import Network
|
||||
|
||||
|
||||
# ── Low-level helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def _gr(text: str) -> bytes:
|
||||
return text.encode('cp737', errors='replace')
|
||||
|
||||
def _open():
|
||||
p = Network(PRINTER_IP, PRINTER_PORT, timeout=10)
|
||||
p._raw(b'\x1b\x40') # ESC @ reset
|
||||
p._raw(b'\x1b\x74\x1d') # CP737 Greek code page
|
||||
p._raw(b'\x1b\x40') # ESC @ — full reset
|
||||
p._raw(b'\x1b\x74\x1d') # ESC t 29 — CP737 Greek code page
|
||||
return p
|
||||
|
||||
def _t(p, text: str):
|
||||
p._raw(_gr(text))
|
||||
|
||||
def _reset(p):
|
||||
"""Reset to: Font A, normal size, no bold, left-align."""
|
||||
p._raw(b'\x1b\x4d\x00') # ESC M 0 — Font A
|
||||
p._raw(b'\x1b\x21\x00') # ESC ! 0 — normal
|
||||
p._raw(b'\x1d\x21\x00') # GS ! 0 — 1×1 size
|
||||
p._raw(b'\x1b\x45\x00') # ESC E 0 — bold off
|
||||
p._raw(b'\x1b\x61\x00') # ESC a 0 — left align
|
||||
|
||||
def _center(p): p._raw(b'\x1b\x61\x01')
|
||||
def _left(p): p._raw(b'\x1b\x61\x00')
|
||||
|
||||
def _divider(p, char="-", width=48):
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
_left(p)
|
||||
_t(p, char * width + "\n")
|
||||
|
||||
def _center(p):
|
||||
p._raw(b'\x1b\x61\x01')
|
||||
def _page_header(p, title: str):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x28') # double-width + bold (bits 3+5 = 0x28)
|
||||
_t(p, title + "\n")
|
||||
_reset(p)
|
||||
_divider(p, "=")
|
||||
|
||||
def _left(p):
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
|
||||
# ── ESC ! n byte reference ──────────────────────────────────────────────────
|
||||
# Bit 0 → underline (not tested, minor)
|
||||
# Bit 1 → double-strike (bold)
|
||||
# Bit 3 → double-height
|
||||
# Bit 4 → double-width
|
||||
# Bit 5 → delete-line
|
||||
# Bit 7 → bold (ESC E alias in some models)
|
||||
# Common combos used here:
|
||||
# 0x00 = normal
|
||||
# 0x08 = double-height only (48 chars wide)
|
||||
# 0x10 = double-height only (alt bit) (48 chars wide)
|
||||
# 0x18 = double-height + bold
|
||||
# 0x20 = double-width only (24 chars wide)
|
||||
# 0x30 = double-width + double-height (24 chars wide)
|
||||
# 0x38 = double-width + double-height + bold
|
||||
# 0x48 = double-height (bit 6 combo — some printers)
|
||||
# ── ESC ! mode table ───────────────────────────────────────────────────────────
|
||||
#
|
||||
# Each entry: (esc_bang_byte, esc_e_bold, label)
|
||||
# esc_bang_byte sets the mode via ESC ! n
|
||||
# esc_e_bold adds ESC E on top (independent bold layer)
|
||||
# We test every useful combination so you can see the exact visual result.
|
||||
|
||||
MODES = [
|
||||
(0x00, "Normal (0x00)"),
|
||||
(0x08, "Double-height bit3 (0x08)"),
|
||||
(0x10, "Double-height bit4 (0x10)"),
|
||||
(0x18, "Double-height + Bold (0x18)"),
|
||||
(0x20, "Double-width (0x20)"),
|
||||
(0x30, "Double-width + Double-height (0x30)"),
|
||||
(0x38, "Double-width + Double-height + Bold (0x38)"),
|
||||
ESC_BANG_MODES = [
|
||||
# (byte, extra_bold, label)
|
||||
(0x00, False, "0x00 Normal"),
|
||||
(0x00, True, "0x00 +ESC E Normal + Bold (ESC E)"),
|
||||
(0x08, False, "0x08 Bold only (bit3)"),
|
||||
(0x10, False, "0x10 Double-height (bit4)"),
|
||||
(0x10, True, "0x10 +ESC E Double-height + Bold"),
|
||||
(0x18, False, "0x18 Double-height + Bold (bits 3+4)"),
|
||||
(0x20, False, "0x20 Double-width (bit5)"),
|
||||
(0x20, True, "0x20 +ESC E Double-width + Bold"),
|
||||
(0x28, False, "0x28 Double-width + Bold (bits 3+5)"),
|
||||
(0x30, False, "0x30 Double-width + Double-height (bits 4+5)"),
|
||||
(0x38, False, "0x38 Double-width + Double-height + Bold (bits 3+4+5)"),
|
||||
]
|
||||
|
||||
# ── Section 1 — Font sizes & styles, English ───────────────────────────────
|
||||
|
||||
def section_english(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x38')
|
||||
_t(p, "=== FONT SIZES (EN) ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p, "=")
|
||||
def _esc_bang_section(p, english: bool):
|
||||
lang = "EN" if english else "GR"
|
||||
sample_normal = "TEST PRINT Hello 123" if english else "ΔΟΚΙΜΗ ΕΚΤΥΠΩΣΗΣ"
|
||||
sample_lower = "test print hello 123" if english else "δοκιμη εκτυπωσης"
|
||||
|
||||
for code, label in MODES:
|
||||
for (byte_val, extra_bold, label) in ESC_BANG_MODES:
|
||||
_left(p)
|
||||
_t(p, f"[{label}]\n")
|
||||
p._raw(bytes([0x1b, 0x21, code]))
|
||||
_t(p, "TEST PRINT Hello World Abc123\n")
|
||||
# Print the label in small normal text first
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
# bold on/off via ESC E
|
||||
_t(p, " -> bold ON: ")
|
||||
p._raw(b'\x1b\x45\x01')
|
||||
p._raw(bytes([0x1b, 0x21, code]))
|
||||
_t(p, "Bold Sample\n")
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_t(p, f"[{label}]\n")
|
||||
|
||||
# Apply mode
|
||||
p._raw(bytes([0x1b, 0x21, byte_val]))
|
||||
if extra_bold:
|
||||
p._raw(b'\x1b\x45\x01')
|
||||
|
||||
_t(p, sample_normal + "\n")
|
||||
_t(p, sample_lower + "\n")
|
||||
|
||||
# Reset
|
||||
_reset(p)
|
||||
_t(p, "\n")
|
||||
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Section 2 — Font sizes & styles, Greek ─────────────────────────────────
|
||||
|
||||
def section_greek(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x38')
|
||||
_t(p, "=== FONT SIZES (GR) ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p, "=")
|
||||
# ── Pages 1–4: ESC ! modes ────────────────────────────────────────────────────
|
||||
|
||||
for code, label in MODES:
|
||||
def page_esc_bang(font_b: bool, english: bool):
|
||||
font_label = "Font B (8x16 small)" if font_b else "Font A (12x24 standard)"
|
||||
lang_label = "GREEK" if not english else "ENGLISH"
|
||||
p = _open()
|
||||
|
||||
# Select font
|
||||
p._raw(b'\x1b\x4d\x01' if font_b else b'\x1b\x4d\x00')
|
||||
|
||||
_page_header(p, f"ESC! MODES — {lang_label} — {font_label[:6]}")
|
||||
_t(p, f"Font: {font_label}\n")
|
||||
_divider(p)
|
||||
|
||||
_esc_bang_section(p, english)
|
||||
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
|
||||
|
||||
# ── Page 5: GS ! size multipliers ─────────────────────────────────────────────
|
||||
|
||||
# Combinations worth seeing: square multipliers + some asymmetric
|
||||
GS_SIZES = [
|
||||
(0x00, "1x1 normal"),
|
||||
(0x01, "1w x 2h"),
|
||||
(0x10, "2w x 1h"),
|
||||
(0x11, "2x2"),
|
||||
(0x22, "3x3"),
|
||||
(0x33, "4x4"),
|
||||
(0x44, "5x5"),
|
||||
(0x55, "6x6"),
|
||||
(0x02, "1w x 3h"),
|
||||
(0x20, "3w x 1h"),
|
||||
(0x21, "3w x 2h"),
|
||||
(0x12, "2w x 3h"),
|
||||
]
|
||||
|
||||
def page_gs_sizes():
|
||||
p = _open()
|
||||
_page_header(p, "GS! SIZE MULTIPLIERS")
|
||||
_t(p, "GS ! n (0x1D 0x21 n)\n")
|
||||
_t(p, "Low nibble=height, High nibble=width\n")
|
||||
_divider(p)
|
||||
|
||||
for (byte_val, label) in GS_SIZES:
|
||||
_left(p)
|
||||
_t(p, f"[{label}]\n")
|
||||
p._raw(bytes([0x1b, 0x21, code]))
|
||||
_t(p, "ΔΟΚΙΜΑΣΤΙΚΗ ΕΚΤΥΠΩΣΗ\n")
|
||||
_t(p, "δοκιμαστικη εκτυπωση\n") # lowercase
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x01')
|
||||
p._raw(bytes([0x1b, 0x21, code]))
|
||||
_t(p, "Bold: Καλημερα Κοσμε\n")
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
# Label in tiny normal text
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1d\x21\x00')
|
||||
_t(p, f"[n=0x{byte_val:02X} {label}]\n")
|
||||
|
||||
# Font A sample
|
||||
p._raw(b'\x1b\x4d\x00')
|
||||
p._raw(bytes([0x1d, 0x21, byte_val]))
|
||||
_t(p, "Aa SAMPLE\n")
|
||||
p._raw(b'\x1d\x21\x00')
|
||||
|
||||
# Font B sample on same size
|
||||
p._raw(b'\x1b\x4d\x01')
|
||||
p._raw(bytes([0x1d, 0x21, byte_val]))
|
||||
_t(p, "Bb SMALL\n")
|
||||
p._raw(b'\x1d\x21\x00')
|
||||
p._raw(b'\x1b\x4d\x00') # back to Font A
|
||||
|
||||
_t(p, "\n")
|
||||
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Section 3 — All printable ASCII symbols ────────────────────────────────
|
||||
|
||||
def section_ascii_symbols(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x18') # double-height bold for header
|
||||
_t(p, "=== ASCII SYMBOLS ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p, "=")
|
||||
_left(p)
|
||||
|
||||
# Printable ASCII 0x20 – 0x7E, 16 per line
|
||||
chars = [chr(c) for c in range(0x20, 0x7F)]
|
||||
line = ""
|
||||
for i, ch in enumerate(chars):
|
||||
line += ch + " "
|
||||
if (i + 1) % 24 == 0:
|
||||
_t(p, line + "\n")
|
||||
line = ""
|
||||
if line:
|
||||
_t(p, line + "\n")
|
||||
|
||||
# Also show GS ! combined with ESC ! bold
|
||||
_t(p, "\n")
|
||||
_t(p, "Notable:\n")
|
||||
_t(p, " Bullets : * + - # @ ! ? > < | / \\ ^ ~ _\n")
|
||||
_t(p, " Framing : [ ] { } ( ) = : ; , . \"\n")
|
||||
_t(p, " Currency : $ %\n")
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Section 4 — CP737 extended chars (0x80–0xFF) ───────────────────────────
|
||||
|
||||
def section_extended(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x18')
|
||||
_t(p, "=== CP737 EXTENDED (0x80-FF) ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p, "=")
|
||||
_left(p)
|
||||
_t(p, "Hex offset rows x16 columns\n\n")
|
||||
|
||||
for row in range(8): # 0x80, 0x90 ... 0xF0
|
||||
base = 0x80 + row * 16
|
||||
row_bytes = bytes(range(base, base + 16))
|
||||
label = f"0x{base:02X}: "
|
||||
p._raw(_gr(label))
|
||||
p._raw(row_bytes)
|
||||
p._raw(b'\n')
|
||||
|
||||
_t(p, "\n")
|
||||
_t(p, "Key CP737 specials:\n")
|
||||
specials = [
|
||||
(0xB3, "─ thin horiz line"),
|
||||
(0xC4, "─ double line"),
|
||||
(0xBA, "│ vert line"),
|
||||
(0xBB, "┐ corner"),
|
||||
(0xBC, "┘ corner"),
|
||||
(0xC9, "╔ corner"),
|
||||
(0xCA, "╩ junction"),
|
||||
(0xCB, "╦ junction"),
|
||||
(0xCC, "╠ junction"),
|
||||
(0xCD, "═ double horiz"),
|
||||
(0xCE, "╬ cross"),
|
||||
(0xC8, "╚ corner"),
|
||||
(0xBB, "╗ corner"),
|
||||
(0xBC, "╝ corner"),
|
||||
(0xDB, "█ full block"),
|
||||
(0xDC, "▄ lower block"),
|
||||
(0xDF, "▀ upper block"),
|
||||
(0xB0, "░ light shade"),
|
||||
(0xB1, "▒ medium shade"),
|
||||
(0xB2, "▓ dark shade"),
|
||||
(0xF8, "° degree"),
|
||||
(0xF9, "· middle dot"),
|
||||
(0xFA, "· bullet dot"),
|
||||
(0xFB, "√ check / tick"),
|
||||
(0xFE, "■ filled square"),
|
||||
]
|
||||
for code, desc in specials:
|
||||
row_bytes = bytes([code, 0x20]) # char + space
|
||||
p._raw(row_bytes)
|
||||
_t(p, f" {desc}\n")
|
||||
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Section 5 — Underline ──────────────────────────────────────────────────
|
||||
|
||||
def section_underline(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x18')
|
||||
_t(p, "=== UNDERLINE TEST ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_t(p, "GS! + ESC E bold combined:\n")
|
||||
_divider(p, "=")
|
||||
_left(p)
|
||||
for (byte_val, label) in [(0x11,"2x2"), (0x22,"3x3"), (0x33,"4x4")]:
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1d\x21\x00')
|
||||
_t(p, f"[{label} + bold]\n")
|
||||
p._raw(b'\x1b\x45\x01')
|
||||
p._raw(bytes([0x1d, 0x21, byte_val]))
|
||||
_t(p, "BOLD LARGE\n")
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
p._raw(b'\x1d\x21\x00')
|
||||
_t(p, "\n")
|
||||
|
||||
# ESC - n : underline 0=off, 1=thin, 2=thick
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
|
||||
|
||||
# ── Page 6: Beep + misc ────────────────────────────────────────────────────────
|
||||
|
||||
def page_beep_misc():
|
||||
p = _open()
|
||||
_page_header(p, "BEEP + MISC TESTS")
|
||||
|
||||
# ── Beep section ──
|
||||
_t(p, "BEEP TESTS\n")
|
||||
_divider(p, "-")
|
||||
_t(p, "Sending beeps now...\n\n")
|
||||
|
||||
# BEL — single beep (0x07)
|
||||
_t(p, "[1] BEL single beep (0x07)\n")
|
||||
p._raw(b'\x07')
|
||||
time.sleep(0.5)
|
||||
|
||||
# ESC BEL n1 n2 n3 — beep for appointment
|
||||
# n1=beep length (100ms units), n2=intermission (100ms), n3=count
|
||||
_t(p, "[2] ESC BEL: 1 beep, 200ms long\n")
|
||||
p._raw(bytes([0x1b, 0x07, 2, 2, 1])) # 200ms on, 200ms off, 1 beep
|
||||
time.sleep(0.8)
|
||||
|
||||
_t(p, "[3] ESC BEL: 3 short beeps\n")
|
||||
p._raw(bytes([0x1b, 0x07, 1, 1, 3])) # 100ms on, 100ms off, 3 beeps
|
||||
time.sleep(1.5)
|
||||
|
||||
_t(p, "[4] ESC BEL: 1 long beep (500ms)\n")
|
||||
p._raw(bytes([0x1b, 0x07, 5, 2, 1])) # 500ms on, 200ms off, 1 beep
|
||||
time.sleep(1.2)
|
||||
|
||||
_t(p, "[5] GS BEL: 2 beeps\n")
|
||||
p._raw(bytes([0x1d, 0x07, 2, 3, 2])) # 2 beeps, 300ms long, 200ms off
|
||||
time.sleep(1.5)
|
||||
|
||||
_t(p, "Beep tests done.\n")
|
||||
_divider(p)
|
||||
|
||||
# ── Underline ──
|
||||
_t(p, "\nUNDERLINE\n")
|
||||
_divider(p, "-")
|
||||
for ul in [1, 2]:
|
||||
p._raw(bytes([0x1b, 0x2d, ul]))
|
||||
_t(p, f"Underline mode {ul}: TEST PRINT Abc123\n")
|
||||
p._raw(b'\x1b\x2d\x00') # off
|
||||
_t(p, f"Underline mode {ul}: Hello World 123\n")
|
||||
p._raw(b'\x1b\x2d\x00')
|
||||
_t(p, "\n")
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Section 6 — Inverted / white-on-black ─────────────────────────────────
|
||||
|
||||
def section_invert(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x18')
|
||||
_t(p, "=== INVERT (white-on-black) ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p, "=")
|
||||
_left(p)
|
||||
|
||||
# GS B n — invert
|
||||
# ── White-on-black invert ──
|
||||
_t(p, "\nWHITE-ON-BLACK (GS B)\n")
|
||||
_divider(p, "-")
|
||||
p._raw(b'\x1d\x42\x01')
|
||||
_t(p, " INVERTED TEXT SAMPLE \n")
|
||||
_t(p, " INVERTED NORMAL \n")
|
||||
p._raw(b'\x1d\x21\x11') # 2x2 inverted
|
||||
_t(p, " INVERTED 2x2 \n")
|
||||
p._raw(b'\x1d\x21\x00')
|
||||
p._raw(b'\x1d\x42\x00')
|
||||
_t(p, "Normal text after invert\n")
|
||||
_t(p, "\n")
|
||||
_t(p, "Normal after invert\n")
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Section 7 — QR Code sample ────────────────────────────────────────────
|
||||
|
||||
def section_qr(p):
|
||||
_center(p)
|
||||
p._raw(b'\x1b\x21\x18')
|
||||
_t(p, "=== QR CODE SAMPLE ===\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p, "=")
|
||||
|
||||
data = b"https://pos.test"
|
||||
# GS ( k — QR store data
|
||||
store_len = len(data) + 3
|
||||
p._raw(b'\x1d\x28\x6b' + bytes([store_len & 0xFF, (store_len >> 8) & 0xFF, 0x31, 0x50, 0x30]) + data)
|
||||
# GS ( k — set size (module=6)
|
||||
p._raw(b'\x1d\x28\x6b\x03\x00\x31\x43\x06')
|
||||
# GS ( k — error correction level M
|
||||
p._raw(b'\x1d\x28\x6b\x03\x00\x31\x45\x31')
|
||||
# GS ( k — print
|
||||
p._raw(b'\x1d\x28\x6b\x03\x00\x31\x51\x30')
|
||||
|
||||
_t(p, "\nhttps://pos.test\n\n")
|
||||
# ── 90-degree rotation ──
|
||||
_t(p, "\n90-DEGREE ROTATION (ESC V)\n")
|
||||
_divider(p, "-")
|
||||
p._raw(b'\x1b\x56\x01')
|
||||
_t(p, "ROTATED TEXT\n")
|
||||
p._raw(b'\x1b\x56\x00')
|
||||
_t(p, "Normal again\n")
|
||||
_divider(p)
|
||||
p._raw(b'\n')
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────
|
||||
# ── CP737 useful symbols at normal size ──
|
||||
_t(p, "\nUSEFUL CP737 SYMBOLS\n")
|
||||
_divider(p, "-")
|
||||
symbols = [
|
||||
(0xFB, "tick / checkmark"),
|
||||
(0xFE, "filled square"),
|
||||
(0xF9, "middle dot"),
|
||||
(0xFA, "small bullet"),
|
||||
(0xF8, "degree"),
|
||||
(0xDB, "full block"),
|
||||
(0xDC, "lower half block"),
|
||||
(0xDF, "upper half block"),
|
||||
(0xB0, "light shade"),
|
||||
(0xB1, "medium shade"),
|
||||
(0xB2, "dark shade"),
|
||||
(0xC4, "thin horiz line"),
|
||||
(0xCD, "double horiz line"),
|
||||
(0xBA, "vertical bar"),
|
||||
(0xC9, "top-left corner dbl"),
|
||||
(0xBB, "top-right corner dbl"),
|
||||
(0xC8, "bot-left corner dbl"),
|
||||
(0xBC, "bot-right corner dbl"),
|
||||
]
|
||||
for code, desc in symbols:
|
||||
p._raw(bytes([code, 0x20, code, 0x20, code, 0x20]))
|
||||
_t(p, f" {desc}\n")
|
||||
|
||||
_divider(p)
|
||||
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
print(f"Connecting to {PRINTER_IP}:{PRINTER_PORT} ...")
|
||||
print(f"Connecting to {PRINTER_IP}:{PRINTER_PORT}")
|
||||
print("Printing 6 pages...\n")
|
||||
|
||||
# ---- PAGE 1: English fonts ----
|
||||
p = _open()
|
||||
section_english(p)
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
print("Page 1 sent (English fonts)")
|
||||
page_esc_bang(font_b=False, english=True)
|
||||
print("Page 1 done — ESC! modes, Font A, English")
|
||||
|
||||
# ---- PAGE 2: Greek fonts ----
|
||||
p = _open()
|
||||
section_greek(p)
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
print("Page 2 sent (Greek fonts)")
|
||||
page_esc_bang(font_b=True, english=True)
|
||||
print("Page 2 done — ESC! modes, Font B, English")
|
||||
|
||||
# ---- PAGE 3: Symbols & special chars ----
|
||||
p = _open()
|
||||
section_ascii_symbols(p)
|
||||
section_extended(p)
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
print("Page 3 sent (ASCII + CP737 extended)")
|
||||
page_esc_bang(font_b=False, english=False)
|
||||
print("Page 3 done — ESC! modes, Font A, Greek")
|
||||
|
||||
# ---- PAGE 4: Underline + Invert + QR ----
|
||||
p = _open()
|
||||
section_underline(p)
|
||||
section_invert(p)
|
||||
section_qr(p)
|
||||
p._raw(b'\n\n\n')
|
||||
p.cut()
|
||||
p.close()
|
||||
print("Page 4 sent (underline / invert / QR)")
|
||||
page_esc_bang(font_b=True, english=False)
|
||||
print("Page 4 done — ESC! modes, Font B, Greek")
|
||||
|
||||
print("Done — 4 pages printed.")
|
||||
page_gs_sizes()
|
||||
print("Page 5 done — GS! size multipliers")
|
||||
|
||||
page_beep_misc()
|
||||
print("Page 6 done — Beep tests + misc")
|
||||
|
||||
print("\nAll done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -35,6 +35,7 @@ def _replace_quick_options(db, product, quick_options):
|
||||
sort_order=qo.sort_order if qo.sort_order else i,
|
||||
is_favorite=qo.is_favorite,
|
||||
favorite_sort_order=qo.favorite_sort_order,
|
||||
is_compact=qo.is_compact,
|
||||
))
|
||||
|
||||
|
||||
@@ -206,6 +207,7 @@ def create_product(body: ProductCreate, db: Session = Depends(get_db), user: Use
|
||||
sort_order=qo.sort_order if qo.sort_order else i,
|
||||
is_favorite=qo.is_favorite,
|
||||
favorite_sort_order=qo.favorite_sort_order,
|
||||
is_compact=qo.is_compact,
|
||||
))
|
||||
for opt in body.options:
|
||||
sub_json = json.dumps([s.model_dump() for s in opt.sub_choices]) if opt.sub_choices else None
|
||||
|
||||
@@ -17,6 +17,13 @@ VALID_SETTINGS = {
|
||||
"system.timezone": "IANA timezone name used by the backend container (e.g. Europe/Athens). Requires container restart to take effect.",
|
||||
"ui.table_colours": "JSON blob of table card colour scheme (light + dark modes) for the Waiter PWA.",
|
||||
"dev.spoof_printing": "When enabled, all print jobs are silently dropped. Devices behave as if printing succeeded.",
|
||||
# Print font settings — values are "SIZE:BOLD" where SIZE is ESC ! base byte (0/16/32/48) and BOLD is 0 or 1
|
||||
"print.font_item_name": "Font for item name lines: SIZE:BOLD (e.g. '16:0')",
|
||||
"print.font_options": "Font for option/modifier lines: SIZE:BOLD",
|
||||
"print.font_table": "Font for table/waiter header lines: SIZE:BOLD",
|
||||
"print.font_order_number": "Font for order number header: SIZE:BOLD",
|
||||
"print.font_header": "Font for top header block: SIZE:BOLD",
|
||||
"print.divider_style": "Divider character used between sections: dash, equals, star, or empty",
|
||||
}
|
||||
|
||||
DEFAULTS = {
|
||||
@@ -26,6 +33,12 @@ DEFAULTS = {
|
||||
"system.timezone": "Europe/Athens",
|
||||
"ui.table_colours": "",
|
||||
"dev.spoof_printing": "false",
|
||||
"print.font_item_name": "16:0", # double-height, no bold
|
||||
"print.font_options": "0:0", # normal
|
||||
"print.font_table": "16:0", # double-height
|
||||
"print.font_order_number": "48:1", # double-height + double-width + bold
|
||||
"print.font_header": "48:1", # double-height + double-width + bold
|
||||
"print.divider_style": "dash",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List
|
||||
|
||||
from database import get_db
|
||||
from models.printer import Printer
|
||||
from schemas.printer import PrinterUpdate, PrinterOut
|
||||
from schemas.printer import PrinterCreate, PrinterUpdate, PrinterOut
|
||||
from routers.deps import get_current_user, require_manager, require_sysadmin
|
||||
from models.user import User
|
||||
from services import printer_service
|
||||
@@ -40,7 +40,16 @@ def system_status(db: Session = Depends(get_db), user: User = Depends(get_curren
|
||||
|
||||
@router.get("/printers", response_model=List[PrinterOut])
|
||||
def list_printers(db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
||||
return db.query(Printer).filter(Printer.is_active == True).all()
|
||||
return db.query(Printer).all()
|
||||
|
||||
|
||||
@router.post("/printers", response_model=PrinterOut)
|
||||
def create_printer(body: PrinterCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
||||
printer = Printer(**body.model_dump())
|
||||
db.add(printer)
|
||||
db.commit()
|
||||
db.refresh(printer)
|
||||
return printer
|
||||
|
||||
|
||||
@router.post("/printers/test")
|
||||
@@ -53,7 +62,7 @@ def test_printer(printer_id: int, db: Session = Depends(get_db), user: User = De
|
||||
|
||||
|
||||
@router.put("/printers/{printer_id}", response_model=PrinterOut)
|
||||
def update_printer(printer_id: int, body: PrinterUpdate, db: Session = Depends(get_db), user: User = Depends(require_sysadmin)):
|
||||
def update_printer(printer_id: int, body: PrinterUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
||||
printer = db.query(Printer).filter(Printer.id == printer_id).first()
|
||||
if not printer:
|
||||
raise HTTPException(status_code=404, detail="Printer not found")
|
||||
@@ -64,6 +73,16 @@ def update_printer(printer_id: int, body: PrinterUpdate, db: Session = Depends(g
|
||||
return printer
|
||||
|
||||
|
||||
@router.delete("/printers/{printer_id}")
|
||||
def delete_printer(printer_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
||||
printer = db.query(Printer).filter(Printer.id == printer_id).first()
|
||||
if not printer:
|
||||
raise HTTPException(status_code=404, detail="Printer not found")
|
||||
db.delete(printer)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/lock")
|
||||
def lock_system(token: str, user: User = Depends(require_sysadmin)):
|
||||
license_state["locked"] = True
|
||||
|
||||
@@ -2,11 +2,19 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
PROTOCOLS = ["escpos_tcp"] # extend later as needed
|
||||
|
||||
|
||||
class PrinterBase(BaseModel):
|
||||
name: str
|
||||
ip_address: str
|
||||
port: int = 9100
|
||||
is_active: bool = True
|
||||
protocol: str = "escpos_tcp"
|
||||
|
||||
|
||||
class PrinterCreate(PrinterBase):
|
||||
pass
|
||||
|
||||
|
||||
class PrinterUpdate(BaseModel):
|
||||
@@ -14,6 +22,7 @@ class PrinterUpdate(BaseModel):
|
||||
ip_address: Optional[str] = None
|
||||
port: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
protocol: Optional[str] = None
|
||||
|
||||
|
||||
class PrinterOut(PrinterBase):
|
||||
|
||||
@@ -57,6 +57,7 @@ class ProductQuickOptionCreate(BaseModel):
|
||||
sort_order: int = 0
|
||||
is_favorite: bool = False
|
||||
favorite_sort_order: int = 0
|
||||
is_compact: bool = False
|
||||
|
||||
|
||||
class ProductQuickOptionOut(BaseModel):
|
||||
@@ -68,6 +69,7 @@ class ProductQuickOptionOut(BaseModel):
|
||||
sort_order: int = 0
|
||||
is_favorite: bool = False
|
||||
favorite_sort_order: int = 0
|
||||
is_compact: bool = False
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@@ -47,9 +47,57 @@ def _raw_text(p: Network, text: str):
|
||||
p._raw(_gr(text))
|
||||
|
||||
|
||||
def _divider(p: Network):
|
||||
_DIVIDER_CHARS = {
|
||||
"dash": "-",
|
||||
"equals": "=",
|
||||
"star": "*",
|
||||
"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.divider_style": "dash",
|
||||
}
|
||||
|
||||
# SIZE byte values (ESC ! base, no bold bit):
|
||||
# 0 = normal
|
||||
# 16 = double-height (bit4)
|
||||
# 32 = double-width (bit5)
|
||||
# 48 = double-height + double-width (bits 4+5)
|
||||
# Bold is applied separately via ESC E.
|
||||
|
||||
def _decode_font(value: str) -> tuple[int, bool]:
|
||||
"""Parse 'SIZE:BOLD' string → (esc_bang_byte, bold_flag)."""
|
||||
try:
|
||||
parts = str(value).split(":")
|
||||
size = int(parts[0])
|
||||
bold = len(parts) > 1 and parts[1] == "1"
|
||||
return size, bold
|
||||
except (ValueError, AttributeError):
|
||||
return 0, False
|
||||
|
||||
|
||||
def _load_print_fonts(db: Session) -> dict:
|
||||
rows = db.query(PosSettings).filter(
|
||||
PosSettings.key.in_(_PRINT_FONT_DEFAULTS.keys())
|
||||
).all()
|
||||
fonts = dict(_PRINT_FONT_DEFAULTS)
|
||||
for row in rows:
|
||||
fonts[row.key] = row.value
|
||||
return fonts
|
||||
|
||||
|
||||
def _divider(p: Network, style: str = "dash"):
|
||||
char = _DIVIDER_CHARS.get(style, "-")
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
p._raw(_gr("-" * LINE_WIDTH + "\n"))
|
||||
if char:
|
||||
p._raw(_gr(char * LINE_WIDTH + "\n"))
|
||||
else:
|
||||
p._raw(b'\n')
|
||||
|
||||
|
||||
def _item_line(name: str, qty: int) -> str:
|
||||
@@ -106,34 +154,50 @@ def send_test_print(ip: str, port: int, name: str) -> Tuple[bool, str]:
|
||||
|
||||
# ── 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 _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db: Session):
|
||||
# Header
|
||||
fonts = _load_print_fonts(db)
|
||||
div = fonts["print.divider_style"]
|
||||
|
||||
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"])
|
||||
|
||||
# Header — order number
|
||||
p._raw(b'\x1b\x61\x01')
|
||||
p._raw(b'\x1b\x21\x38') # bold + double height + double width
|
||||
_font(p, sz_order, bold_order)
|
||||
_raw_text(p, f"Παραγγελια #{order.id}\n")
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
_divider(p)
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
_divider(p, div)
|
||||
|
||||
# Meta
|
||||
# Meta — table / waiter / time
|
||||
p._raw(b'\x1b\x61\x00')
|
||||
p._raw(b'\x1b\x21\x10') # double height only — keeps 48-char width
|
||||
_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')
|
||||
_divider(p)
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
_divider(p, div)
|
||||
|
||||
# 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}"
|
||||
|
||||
p._raw(b'\x1b\x21\x10')
|
||||
p._raw(b'\x1b\x45\x01') # bold on
|
||||
_font(p, sz_item, bold_item)
|
||||
_raw_text(p, _item_line(name, item.quantity) + "\n")
|
||||
p._raw(b'\x1b\x45\x00') # bold off
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
|
||||
_font(p, sz_opt, bold_opt)
|
||||
if item.removed_ingredients:
|
||||
try:
|
||||
removed_ids = json.loads(item.removed_ingredients)
|
||||
@@ -154,8 +218,9 @@ def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db:
|
||||
_raw_text(p, f" (i) {item.notes}\n")
|
||||
|
||||
p._raw(b'\x1b\x21\x00')
|
||||
p._raw(b'\x1b\x45\x00')
|
||||
|
||||
_divider(p)
|
||||
_divider(p, div)
|
||||
|
||||
if order.notes:
|
||||
p._raw(b'\x1b\x21\x30')
|
||||
|
||||
Reference in New Issue
Block a user