| @@ -26,6 +26,11 @@ try: | |||
| except ImportError: | |||
| serial = None # type: ignore | |||
| try: | |||
| import win32print # type: ignore[import] | |||
| except ImportError: | |||
| win32print = None # type: ignore[assignment] | |||
| DEFAULT_BASE_URL = os.environ.get("FPSMS_BASE_URL", "http://localhost:8090/api") | |||
| # When run as PyInstaller exe, save settings next to the exe; otherwise next to script | |||
| if getattr(sys, "frozen", False): | |||
| @@ -42,7 +47,8 @@ DEFAULT_SETTINGS = { | |||
| "dabag_port": "3008", | |||
| "laser_ip": "192.168.17.10", | |||
| "laser_port": "45678", | |||
| "label_com": "COM3", | |||
| # For 標簽機 on Windows, this is the Windows printer name, e.g. "TSC TTP-246M Pro" | |||
| "label_com": "TSC TTP-246M Pro", | |||
| } | |||
| @@ -97,13 +103,26 @@ def try_printer_connection(printer_name: str, sett: dict) -> bool: | |||
| except (socket.error, ValueError, OSError): | |||
| return False | |||
| if printer_name == "標簽機": | |||
| if serial is None: | |||
| target = (sett.get("label_com") or "").strip() | |||
| if not target: | |||
| return False | |||
| com = (sett.get("label_com") or "").strip() | |||
| if not com: | |||
| # On Windows, allow using a Windows printer name (e.g. "TSC TTP-246M Pro") | |||
| # as an alternative to a COM port. If it doesn't look like a COM port, | |||
| # try opening it via the Windows print spooler. | |||
| if os.name == "nt" and not target.upper().startswith("COM"): | |||
| if win32print is None: | |||
| return False | |||
| try: | |||
| handle = win32print.OpenPrinter(target) | |||
| win32print.ClosePrinter(handle) | |||
| return True | |||
| except Exception: | |||
| return False | |||
| # Fallback: treat as serial COM port (original behaviour) | |||
| if serial is None: | |||
| return False | |||
| try: | |||
| ser = serial.Serial(com, timeout=1) | |||
| ser = serial.Serial(target, timeout=1) | |||
| ser.close() | |||
| return True | |||
| except (serial.SerialException, OSError): | |||
| @@ -190,12 +209,12 @@ def generate_zpl_label_small( | |||
| item_id: Optional[int] = None, | |||
| stock_in_line_id: Optional[int] = None, | |||
| lot_no: Optional[str] = None, | |||
| font: str = "ARIALR.TTF", | |||
| font: str = "MingLiUHKSCS", | |||
| ) -> str: | |||
| """ | |||
| ZPL for 標簽機. Row 1: item name. Row 2: QR left | item code + lot no (or batch) right. | |||
| QR contains {"itemId": xxx, "stockInLineId": xxx} when both present; else batch_no. | |||
| Font: ARIALR.TTF. | |||
| Unicode (^CI28); font set for Big-5 (e.g. MingLiUHKSCS). | |||
| """ | |||
| desc = _zpl_escape((item_name or "—").strip()) | |||
| code = _zpl_escape((item_code or "—").strip()) | |||
| @@ -232,13 +251,42 @@ def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: | |||
| sock.close() | |||
| def send_zpl_to_label_printer(com_port: str, zpl: str) -> None: | |||
| """Send ZPL to 標簽機 via serial COM port. Raises on error.""" | |||
| def send_zpl_to_label_printer(target: str, zpl: str) -> None: | |||
| """ | |||
| Send ZPL to 標簽機. | |||
| On Windows, if target is not a COM port (e.g. "TSC TTP-246M Pro"), | |||
| send raw ZPL to the named Windows printer via the spooler. | |||
| Otherwise, treat target as a serial COM port (original behaviour). | |||
| """ | |||
| dest = (target or "").strip() | |||
| if not dest: | |||
| raise ValueError("Label printer destination is empty.") | |||
| # Unicode (^CI28); send UTF-8 to 標簽機 | |||
| raw_bytes = zpl.encode("utf-8") | |||
| # Windows printer name path (USB printer installed as normal printer) | |||
| if os.name == "nt" and not dest.upper().startswith("COM"): | |||
| if win32print is None: | |||
| raise RuntimeError("pywin32 not installed. Run: pip install pywin32") | |||
| handle = win32print.OpenPrinter(dest) | |||
| try: | |||
| job = win32print.StartDocPrinter(handle, 1, ("FPSMS Label", None, "RAW")) | |||
| win32print.StartPagePrinter(handle) | |||
| win32print.WritePrinter(handle, raw_bytes) | |||
| win32print.EndPagePrinter(handle) | |||
| win32print.EndDocPrinter(handle) | |||
| finally: | |||
| win32print.ClosePrinter(handle) | |||
| return | |||
| # Fallback: serial COM port | |||
| if serial is None: | |||
| raise RuntimeError("pyserial not installed. Run: pip install pyserial") | |||
| ser = serial.Serial(com_port, timeout=5) | |||
| ser = serial.Serial(dest, timeout=5) | |||
| try: | |||
| ser.write(zpl.encode("utf-8")) | |||
| ser.write(raw_bytes) | |||
| finally: | |||
| ser.close() | |||
| @@ -733,9 +781,17 @@ def main() -> None: | |||
| ) | |||
| grid_row[0] += 1 | |||
| if key_single: | |||
| ttk.Label(f, text="COM:").grid(row=grid_row[0], column=0, sticky=tk.W, pady=2) | |||
| ttk.Label( | |||
| f, | |||
| text="列印機名稱 (Windows):", | |||
| ).grid( | |||
| row=grid_row[0], | |||
| column=0, | |||
| sticky=tk.W, | |||
| pady=2, | |||
| ) | |||
| var = tk.StringVar(value=sett.get(key_single, "")) | |||
| e = tk.Entry(f, textvariable=var, width=14, font=get_font(FONT_SIZE), bg="white") | |||
| e = tk.Entry(f, textvariable=var, width=22, font=get_font(FONT_SIZE), bg="white") | |||
| e.grid(row=grid_row[0], column=1, sticky=tk.W, pady=2) | |||
| _ensure_dot_in_entry(e) | |||
| grid_row[0] += 1 | |||
| @@ -762,7 +818,7 @@ def main() -> None: | |||
| all_vars.extend(add_section("API 伺服器", "api_ip", "api_port", None)) | |||
| all_vars.extend(add_section("打袋機 DataFlex", "dabag_ip", "dabag_port", None)) | |||
| all_vars.extend(add_section("激光機", "laser_ip", "laser_port", None)) | |||
| all_vars.extend(add_section("標簽機 COM 埠", None, None, "label_com")) | |||
| all_vars.extend(add_section("標簽機 (USB)", None, None, "label_com")) | |||
| def on_save(): | |||
| for key, var in all_vars: | |||
| @@ -933,7 +989,7 @@ def main() -> None: | |||
| elif printer_var.get() == "標簽機": | |||
| com = (settings.get("label_com") or "").strip() | |||
| if not com: | |||
| messagebox.showerror("標簽機", "請在設定中填寫標簽機 COM 埠。") | |||
| messagebox.showerror("標簽機", "請在設定中填寫標簽機名稱 (例如:TSC TTP-246M Pro)。") | |||
| else: | |||
| count = ask_label_count(root) | |||
| if count is not None: | |||