| @@ -35,7 +35,7 @@ DEFAULT_SETTINGS = { | |||||
| "api_ip": "localhost", | "api_ip": "localhost", | ||||
| "api_port": "8090", | "api_port": "8090", | ||||
| "dabag_ip": "", | "dabag_ip": "", | ||||
| "dabag_port": "9100", | |||||
| "dabag_port": "3008", | |||||
| "laser_ip": "", | "laser_ip": "", | ||||
| "laser_port": "9100", | "laser_port": "9100", | ||||
| "label_com": "COM3", | "label_com": "COM3", | ||||
| @@ -132,6 +132,47 @@ REFRESH_MS = 60 * 1000 # 60 seconds refresh when connected | |||||
| PRINTER_CHECK_MS = 60 * 1000 # 1 minute when printer OK | PRINTER_CHECK_MS = 60 * 1000 # 1 minute when printer OK | ||||
| PRINTER_RETRY_MS = 30 * 1000 # 30 seconds when printer failed | PRINTER_RETRY_MS = 30 * 1000 # 30 seconds when printer failed | ||||
| PRINTER_SOCKET_TIMEOUT = 3 | PRINTER_SOCKET_TIMEOUT = 3 | ||||
| DATAFLEX_SEND_TIMEOUT = 10 # seconds when sending ZPL to DataFlex | |||||
| def _zpl_escape(s: str) -> str: | |||||
| """Escape text for ZPL ^FD...^FS (backslash and caret).""" | |||||
| return s.replace("\\", "\\\\").replace("^", "\\^") | |||||
| def generate_zpl_dataflex(batch_no: str, item_code: str, item_name: str) -> str: | |||||
| """ | |||||
| Generate ZPL for DataFlex label (53 mm media, 90° rotated). | |||||
| Uses UTF-8 (^CI28) and fonts msjh.ttc / msjhbd.ttc for Chinese/English. | |||||
| """ | |||||
| desc = _zpl_escape((item_name or "—").strip()) | |||||
| code = _zpl_escape((item_code or "—").strip()) | |||||
| batch = _zpl_escape(batch_no.strip()) | |||||
| return f"""^XA | |||||
| ^PW420 | |||||
| ^LL780 | |||||
| ^PO N | |||||
| ^CI28 | |||||
| ^FO70,70 | |||||
| ^A@R,60,60,E:MSJH.TTC^FD{desc}^FS | |||||
| ^FO220,70 | |||||
| ^A@R,50,50,E:MSJHBD.TTC^FD{code}^FS | |||||
| ^FO310,70 | |||||
| ^A@R,45,45,E:MSJHBD.TTC^FD批次: {batch}^FS | |||||
| ^FO150,420 | |||||
| ^BQN,2,6^FDQA,{batch_no}^FS | |||||
| ^XZ""" | |||||
| def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: | |||||
| """Send ZPL to DataFlex printer via TCP. Raises on connection/send error.""" | |||||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
| sock.settimeout(DATAFLEX_SEND_TIMEOUT) | |||||
| try: | |||||
| sock.connect((ip, port)) | |||||
| sock.sendall(zpl.encode("utf-8")) | |||||
| finally: | |||||
| sock.close() | |||||
| def format_qty(val) -> str: | def format_qty(val) -> str: | ||||
| @@ -551,7 +592,29 @@ def main() -> None: | |||||
| set_row_highlight(r, True) | set_row_highlight(r, True) | ||||
| selected_row_holder[0] = r | selected_row_holder[0] = r | ||||
| selected_jo_id_ref[0] = j.get("id") | selected_jo_id_ref[0] = j.get("id") | ||||
| if printer_var.get() == "標簽機": | |||||
| if printer_var.get() == "打袋機 DataFlex": | |||||
| ip = (settings.get("dabag_ip") or "").strip() | |||||
| port_str = (settings.get("dabag_port") or "3008").strip() | |||||
| try: | |||||
| port = int(port_str) | |||||
| except ValueError: | |||||
| port = 3008 | |||||
| if not ip: | |||||
| messagebox.showerror("打袋機", "請在設定中填寫打袋機 DataFlex 的 IP。") | |||||
| else: | |||||
| item_code = j.get("itemCode") or "—" | |||||
| item_name = j.get("itemName") or "—" | |||||
| zpl = generate_zpl_dataflex(b, item_code, item_name) | |||||
| try: | |||||
| send_zpl_to_dataflex(ip, port, zpl) | |||||
| messagebox.showinfo("打袋機", f"已送出列印:批次 {b}") | |||||
| except ConnectionRefusedError: | |||||
| messagebox.showerror("打袋機", f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。") | |||||
| except socket.timeout: | |||||
| messagebox.showerror("打袋機", f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。") | |||||
| except OSError as err: | |||||
| messagebox.showerror("打袋機", f"列印失敗:{err}") | |||||
| elif printer_var.get() == "標簽機": | |||||
| count = ask_label_count(root) | count = ask_label_count(root) | ||||
| if count is not None: | if count is not None: | ||||
| if count == "C": | if count == "C": | ||||
| @@ -22,6 +22,7 @@ Set the backend base URL (optional, default below): | |||||
| |--------|-------------| | |--------|-------------| | ||||
| | `Bag1.py` | **GUI**: date selector (default today) and job orders as buttons; click shows "Clicked on Job Order code XXXX item xxxx". Run: `python Bag1.py` | | | `Bag1.py` | **GUI**: date selector (default today) and job orders as buttons; click shows "Clicked on Job Order code XXXX item xxxx". Run: `python Bag1.py` | | ||||
| | `fetch_job_orders.py` | CLI: fetches job orders by plan date from `GET /py/job-orders` | | | `fetch_job_orders.py` | CLI: fetches job orders by plan date from `GET /py/job-orders` | | ||||
| | `label_zpl.py` | ZPL label generator (90° rotated, UTF-8 Chinese, QR). `generate_zpl(batch_no, item_code, chinese_desc)`, `send_zpl(zpl, host, port)`. Run: `python label_zpl.py` to print one test label. | | |||||
| ## Building Bag1 as a standalone .exe | ## Building Bag1 as a standalone .exe | ||||
| @@ -0,0 +1,97 @@ | |||||
| #!/usr/bin/env python3 | |||||
| """ | |||||
| ZPL label generator and TCP send for Zebra label printer (標簽機). | |||||
| - Rotated 90° clockwise for ~53 mm media width | |||||
| - UTF-8 (^CI28) for Chinese | |||||
| - Large fonts, QR code mag 6 | |||||
| Standalone: python label_zpl.py | |||||
| Or import generate_zpl() / send_zpl() and call from Bag1. | |||||
| """ | |||||
| import socket | |||||
| # Default printer (override via args or Bag1 settings) | |||||
| DEFAULT_PRINTER_IP = "192.168.17.27" | |||||
| DEFAULT_PRINTER_PORT = 3008 | |||||
| SOCKET_TIMEOUT = 10 | |||||
| def generate_zpl( | |||||
| batch_no: str, | |||||
| item_code: str = "PP2238-02", | |||||
| chinese_desc: str = "(餐廳用)凍咖啡底P+10(0.91L包)", | |||||
| ) -> str: | |||||
| """ | |||||
| Generates ZPL label: | |||||
| - Rotated 90° clockwise to fit ~53 mm media width | |||||
| - UTF-8 mode (^CI28) for correct Chinese display | |||||
| - Larger fonts | |||||
| - Bigger QR code (mag 6) | |||||
| """ | |||||
| return f"""^XA | |||||
| ^PW420 ^# Fits ~53 mm width (~420 dots @ 203 dpi) | |||||
| ^LL780 ^# Taller label after rotation + bigger elements | |||||
| ^PO N ^# Normal — change to ^POI if upside-down | |||||
| ^CI28 ^# Enable UTF-8 / Unicode for Chinese (critical fix for boxes) | |||||
| ^FO70,70 | |||||
| ^A@R,60,60,E:SIMSUN.FNT^FD{chinese_desc}^FS | |||||
| ^# Very large Chinese text, rotated | |||||
| ^FO220,70 | |||||
| ^A0R,50,50^FD{item_code}^FS | |||||
| ^# Larger item code | |||||
| ^FO310,70 | |||||
| ^A0R,45,45^FDBatch: {batch_no}^FS | |||||
| ^# Larger batch text | |||||
| ^FO150,420 | |||||
| ^BQN,2,6^FDQA,{batch_no}^FS | |||||
| ^# Bigger QR code (magnification 6), lower position for space | |||||
| ^XZ""" | |||||
| def send_zpl( | |||||
| zpl: str, | |||||
| host: str = DEFAULT_PRINTER_IP, | |||||
| port: int = DEFAULT_PRINTER_PORT, | |||||
| timeout: float = SOCKET_TIMEOUT, | |||||
| ) -> None: | |||||
| """Send ZPL to printer over TCP. Raises on connection/send error.""" | |||||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
| sock.settimeout(timeout) | |||||
| sock.connect((host, port)) | |||||
| sock.sendall(zpl.encode("utf-8")) | |||||
| sock.close() | |||||
| # ──────────────────────────────────────────────── | |||||
| # Example usage — prints one label | |||||
| # ──────────────────────────────────────────────── | |||||
| if __name__ == "__main__": | |||||
| test_batch = "2025121209" | |||||
| zpl = generate_zpl(test_batch) | |||||
| print("Sending ZPL (90° rotated, UTF-8 Chinese, bigger QR):") | |||||
| print("-" * 90) | |||||
| print(zpl) | |||||
| print("-" * 90) | |||||
| try: | |||||
| send_zpl(zpl) | |||||
| print("Label sent successfully!") | |||||
| print("→ Check Chinese — should show real characters (not 口口 or symbols)") | |||||
| print("→ QR is now bigger (mag 6) — test scan with phone") | |||||
| print("→ If upside-down: edit ^PO N → ^POI") | |||||
| print("→ If still boxes: SimSun font may be missing — reinstall via Zebra Setup Utilities") | |||||
| except ConnectionRefusedError: | |||||
| print(f"Cannot connect to {DEFAULT_PRINTER_IP}:{DEFAULT_PRINTER_PORT} — printer off or wrong IP?") | |||||
| except socket.timeout: | |||||
| print("Connection timeout — check printer/network/port") | |||||
| except Exception as e: | |||||
| print(f"Error: {e}") | |||||