| @@ -26,6 +26,11 @@ try: | |||||
| except ImportError: | except ImportError: | ||||
| serial = None # type: ignore | 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") | 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 | # When run as PyInstaller exe, save settings next to the exe; otherwise next to script | ||||
| if getattr(sys, "frozen", False): | if getattr(sys, "frozen", False): | ||||
| @@ -42,7 +47,8 @@ DEFAULT_SETTINGS = { | |||||
| "dabag_port": "3008", | "dabag_port": "3008", | ||||
| "laser_ip": "192.168.17.10", | "laser_ip": "192.168.17.10", | ||||
| "laser_port": "45678", | "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): | except (socket.error, ValueError, OSError): | ||||
| return False | return False | ||||
| if printer_name == "標簽機": | if printer_name == "標簽機": | ||||
| if serial is None: | |||||
| target = (sett.get("label_com") or "").strip() | |||||
| if not target: | |||||
| return False | 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 | return False | ||||
| try: | try: | ||||
| ser = serial.Serial(com, timeout=1) | |||||
| ser = serial.Serial(target, timeout=1) | |||||
| ser.close() | ser.close() | ||||
| return True | return True | ||||
| except (serial.SerialException, OSError): | except (serial.SerialException, OSError): | ||||
| @@ -190,12 +209,12 @@ def generate_zpl_label_small( | |||||
| item_id: Optional[int] = None, | item_id: Optional[int] = None, | ||||
| stock_in_line_id: Optional[int] = None, | stock_in_line_id: Optional[int] = None, | ||||
| lot_no: Optional[str] = None, | lot_no: Optional[str] = None, | ||||
| font: str = "ARIALR.TTF", | |||||
| font: str = "MingLiUHKSCS", | |||||
| ) -> str: | ) -> str: | ||||
| """ | """ | ||||
| ZPL for 標簽機. Row 1: item name. Row 2: QR left | item code + lot no (or batch) right. | 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. | 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()) | desc = _zpl_escape((item_name or "—").strip()) | ||||
| code = _zpl_escape((item_code 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() | 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: | if serial is None: | ||||
| raise RuntimeError("pyserial not installed. Run: pip install pyserial") | raise RuntimeError("pyserial not installed. Run: pip install pyserial") | ||||
| ser = serial.Serial(com_port, timeout=5) | |||||
| ser = serial.Serial(dest, timeout=5) | |||||
| try: | try: | ||||
| ser.write(zpl.encode("utf-8")) | |||||
| ser.write(raw_bytes) | |||||
| finally: | finally: | ||||
| ser.close() | ser.close() | ||||
| @@ -733,9 +781,17 @@ def main() -> None: | |||||
| ) | ) | ||||
| grid_row[0] += 1 | grid_row[0] += 1 | ||||
| if key_single: | 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, "")) | 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) | e.grid(row=grid_row[0], column=1, sticky=tk.W, pady=2) | ||||
| _ensure_dot_in_entry(e) | _ensure_dot_in_entry(e) | ||||
| grid_row[0] += 1 | 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("API 伺服器", "api_ip", "api_port", None)) | ||||
| all_vars.extend(add_section("打袋機 DataFlex", "dabag_ip", "dabag_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("激光機", "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(): | def on_save(): | ||||
| for key, var in all_vars: | for key, var in all_vars: | ||||
| @@ -933,7 +989,7 @@ def main() -> None: | |||||
| elif printer_var.get() == "標簽機": | elif printer_var.get() == "標簽機": | ||||
| com = (settings.get("label_com") or "").strip() | com = (settings.get("label_com") or "").strip() | ||||
| if not com: | if not com: | ||||
| messagebox.showerror("標簽機", "請在設定中填寫標簽機 COM 埠。") | |||||
| messagebox.showerror("標簽機", "請在設定中填寫標簽機名稱 (例如:TSC TTP-246M Pro)。") | |||||
| else: | else: | ||||
| count = ask_label_count(root) | count = ask_label_count(root) | ||||
| if count is not None: | if count is not None: | ||||