| @@ -197,6 +197,21 @@ PRINTER_CHECK_MS = 60 * 1000 # 1 minute when printer OK | |||
| PRINTER_RETRY_MS = 30 * 1000 # 30 seconds when printer failed | |||
| PRINTER_SOCKET_TIMEOUT = 3 | |||
| DATAFLEX_SEND_TIMEOUT = 10 # seconds when sending ZPL to DataFlex | |||
| # Gap between bag labels on DataFlex (one TCP session per batch; tune if bags are skipped) | |||
| DATAFLEX_INTER_LABEL_DELAY_SEC = 2.5 | |||
| # After ~JA/~RO/~JR recovery, let firmware finish before sending label ZPL (avoids stuck E1005) | |||
| DATAFLEX_POST_RECOVERY_DELAY_SEC = 1.5 | |||
| # Zebra ~RO only (used when FPSMS_DATAFLEX_NO_JR is set) | |||
| DATAFLEX_RESET_BYTES = b"~RO1\r\n~RO2\r\n" | |||
| # Full host recovery: ~JA clear buffers, ~RO counters, ~JR soft reset (clears latched errors without power cycle) | |||
| DATAFLEX_FULL_RECOVERY_BYTES = b"~JA\r\n~RO1\r\n~RO2\r\n~JR\r\n" | |||
| def _dataflex_recovery_payload() -> bytes: | |||
| """~JA+~RO+~JR by default; set env FPSMS_DATAFLEX_NO_JR=1 to skip ~JR if firmware rejects it.""" | |||
| if os.environ.get("FPSMS_DATAFLEX_NO_JR", "").strip().lower() in ("1", "true", "yes"): | |||
| return b"~JA\r\n" + DATAFLEX_RESET_BYTES | |||
| return DATAFLEX_FULL_RECOVERY_BYTES | |||
| def _zpl_escape(s: str) -> str: | |||
| @@ -218,7 +233,7 @@ def generate_zpl_dataflex( | |||
| Row 1 (from zero): QR code, then item name (rotated 90°). | |||
| Row 2: Batch/lot (left), item code (right). | |||
| Label and QR use lotNo from API when present, else batch_no (Bxxxxx). | |||
| Job counter reset (~RO) is sent separately via [send_dataflex_job_counter_reset] before labels. | |||
| Host recovery (~JA/~RO/~JR) is sent separately via [send_dataflex_job_counter_reset] before labels. | |||
| """ | |||
| desc = _zpl_escape((item_name or "—").strip()) | |||
| code = _zpl_escape((item_code or "—").strip()) | |||
| @@ -230,7 +245,10 @@ def generate_zpl_dataflex( | |||
| else: | |||
| qr_payload = label_line if label_line else batch_no.strip() | |||
| qr_value = _zpl_escape(qr_payload) | |||
| # Explicit ^PQ1: each ^XA…^XZ is exactly one bag. Avoids E1005 "over quantity" on some Zebra/DataFlex | |||
| # firmware when many labels are sent on one TCP session without a per-job quantity. | |||
| return f"""^XA | |||
| ^PQ1,0,1,N | |||
| ^CI28 | |||
| ^PW700 | |||
| ^LL500 | |||
| @@ -248,19 +266,62 @@ def generate_zpl_dataflex( | |||
| def send_dataflex_job_counter_reset(ip: str, port: int) -> None: | |||
| """ | |||
| Reset printer job / label counters when switching to a new job (new row). | |||
| Prepare DataFlex for a new job without power cycling: ~JA (clear host buffer / cancel pending), | |||
| ~RO1/~RO2 (counters), ~JR (soft reset — clears latched E1005 / “over qty” on many Zebra-class units). | |||
| Brief pause after send so ~JR can finish before label data. Set FPSMS_DATAFLEX_NO_JR=1 to omit ~JR. | |||
| """ | |||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| try: | |||
| sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) | |||
| except OSError: | |||
| pass | |||
| sock.settimeout(DATAFLEX_SEND_TIMEOUT) | |||
| try: | |||
| sock.connect((ip, port)) | |||
| sock.sendall(_dataflex_recovery_payload()) | |||
| time.sleep(DATAFLEX_POST_RECOVERY_DELAY_SEC) | |||
| try: | |||
| sock.shutdown(socket.SHUT_WR) | |||
| except OSError: | |||
| pass | |||
| finally: | |||
| sock.close() | |||
| Zebra: ~RO1 / ~RO2 reset counters 1 and 2. Sent as a *standalone* TCP write *before* any ^XA…^XZ | |||
| label, because many firmwares ignore ~ commands inside a format block. | |||
| Uses CRLF line endings (common Zebra expectation). Raises on TCP error like [send_zpl_to_dataflex]. | |||
| def send_dataflex_reset_and_labels( | |||
| ip: str, | |||
| port: int, | |||
| zpl: str, | |||
| copies: int, | |||
| delay_sec: float, | |||
| ) -> None: | |||
| """ | |||
| One TCP connection: send ~RO reset, then `copies` identical ZPL labels with delay_sec between | |||
| copies (not after the last). Avoids rapid connect/disconnect per bag, which can cause skipped | |||
| prints on some DataFlex/Zebra TCP hosts. | |||
| """ | |||
| raw = "~RO1\r\n~RO2\r\n".encode("ascii") | |||
| if copies < 1: | |||
| return | |||
| raw_zpl = zpl.encode("utf-8") | |||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| try: | |||
| sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) | |||
| except OSError: | |||
| pass | |||
| sock.settimeout(DATAFLEX_SEND_TIMEOUT) | |||
| try: | |||
| sock.connect((ip, port)) | |||
| sock.sendall(raw) | |||
| sock.sendall(_dataflex_recovery_payload()) | |||
| time.sleep(DATAFLEX_POST_RECOVERY_DELAY_SEC) | |||
| for i in range(copies): | |||
| sock.sendall(raw_zpl) | |||
| if i < copies - 1: | |||
| time.sleep(delay_sec) | |||
| try: | |||
| sock.shutdown(socket.SHUT_WR) | |||
| except OSError: | |||
| pass | |||
| finally: | |||
| sock.close() | |||
| @@ -502,6 +563,35 @@ def _image_to_zpl_gfa(pil_image: "Image.Image") -> str: | |||
| ^XZ""" | |||
| def zpl_apply_print_quantity(zpl: str, copies: int) -> str: | |||
| """ | |||
| Ask the printer to output `copies` identical labels from one ZPL format by inserting ^PQ after ^XA. | |||
| Results in a **single** spool job / one write — no N separate Windows jobs, no chained ^XA blocks | |||
| (which broke some TSC drivers with white-on-white ^GFA output). | |||
| """ | |||
| if copies <= 1: | |||
| return zpl | |||
| first_fmt = zpl.split("^XZ", 1)[0] if "^XZ" in zpl else zpl | |||
| if "^PQ" in first_fmt.upper(): | |||
| return zpl | |||
| lines = zpl.splitlines() | |||
| new_lines: list[str] = [] | |||
| inserted = False | |||
| for line in lines: | |||
| new_lines.append(line) | |||
| if not inserted and line.strip() == "^XA": | |||
| # ZPL II: q labels, 0 pause between, 1 replicate (non-serial), N = default options. | |||
| # Same graphic (^GFA) is repeated q times — e.g. 需求數量 150 → 150 identical labels, one spool job. | |||
| new_lines.append(f"^PQ{copies},0,1,N") | |||
| inserted = True | |||
| if not inserted: | |||
| raise RuntimeError( | |||
| "標籤 ZPL 無法插入 ^PQ(格式非預期)。請聯絡程式維護。" | |||
| ) | |||
| ending = "\n" if zpl.endswith("\n") else "" | |||
| return "\n".join(new_lines) + ending | |||
| def send_image_to_label_printer(printer_name: str, pil_image: "Image.Image") -> None: | |||
| """ | |||
| Send a PIL Image to 標簽機 via Windows GDI (so Chinese and graphics print correctly). | |||
| @@ -571,10 +661,18 @@ def send_image_to_label_printer(printer_name: str, pil_image: "Image.Image") -> | |||
| def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: | |||
| """Send ZPL label (^XA…^XZ) to DataFlex printer via TCP. Raises on connection/send error.""" | |||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| try: | |||
| sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) | |||
| except OSError: | |||
| pass | |||
| sock.settimeout(DATAFLEX_SEND_TIMEOUT) | |||
| try: | |||
| sock.connect((ip, port)) | |||
| sock.sendall(zpl.encode("utf-8")) | |||
| try: | |||
| sock.shutdown(socket.SHUT_WR) | |||
| except OSError: | |||
| pass | |||
| finally: | |||
| sock.close() | |||
| @@ -619,6 +717,18 @@ def send_zpl_to_label_printer(target: str, zpl: str) -> None: | |||
| ser.close() | |||
| def send_zpl_to_label_printer_batch(target: str, zpl: str, copies: int) -> None: | |||
| """ | |||
| Print multiple identical ZPL labels in **exactly one** Windows spool job (or one COM write). | |||
| Uses ZPL ^PQ so the printer firmware repeats the format N times — never one job per label. | |||
| """ | |||
| if copies < 1: | |||
| return | |||
| zpl_out = zpl_apply_print_quantity(zpl, copies) | |||
| send_zpl_to_label_printer(target, zpl_out) | |||
| def load_laser_last_count() -> tuple[int, Optional[str]]: | |||
| """Load last batch count and date from laser counter file. Returns (count, date_str).""" | |||
| if not os.path.exists(LASER_COUNTER_FILE): | |||
| @@ -810,6 +920,7 @@ def run_laser_row_send_thread( | |||
| After success, POST LASER qty to API when job_order_id and base_url are set. | |||
| """ | |||
| if laser_busy_ref[0]: | |||
| messagebox.showwarning("激光機", "請等待目前激光發送完成。") | |||
| return | |||
| laser_busy_ref[0] = True | |||
| @@ -864,6 +975,163 @@ def run_laser_row_send_thread( | |||
| threading.Thread(target=worker, daemon=True).start() | |||
| def run_dataflex_fixed_qty_thread( | |||
| root: tk.Tk, | |||
| dataflex_lock: threading.Lock, | |||
| dataflex_busy_ref: list, | |||
| ip: str, | |||
| port: int, | |||
| n: int, | |||
| zpl: str, | |||
| label_text: str, | |||
| jo_id: Optional[int], | |||
| base_url: str, | |||
| set_status_message: Callable[[str, bool], None], | |||
| on_recorded: Callable[[], None], | |||
| ) -> None: | |||
| """ | |||
| Send n DataFlex labels with delay between copies. Runs off the Tk main thread so the UI | |||
| stays responsive (printer dropdown, other controls) during printing. | |||
| """ | |||
| def worker() -> None: | |||
| with dataflex_lock: | |||
| if dataflex_busy_ref[0]: | |||
| root.after( | |||
| 0, | |||
| lambda: messagebox.showwarning( | |||
| "打袋機", | |||
| "請等待目前列印完成或先停止連續列印。", | |||
| ), | |||
| ) | |||
| return | |||
| dataflex_busy_ref[0] = True | |||
| try: | |||
| send_dataflex_reset_and_labels( | |||
| ip, | |||
| port, | |||
| zpl, | |||
| n, | |||
| DATAFLEX_INTER_LABEL_DELAY_SEC, | |||
| ) | |||
| root.after( | |||
| 0, | |||
| lambda: set_status_message( | |||
| f"已送出列印:批次 {label_text} x {n} 張", | |||
| is_error=False, | |||
| ), | |||
| ) | |||
| if jo_id is not None: | |||
| try: | |||
| submit_job_order_print_submit(base_url, int(jo_id), n, "DATAFLEX") | |||
| root.after(0, on_recorded) | |||
| except requests.RequestException as ex: | |||
| root.after( | |||
| 0, | |||
| lambda err=str(ex): messagebox.showwarning( | |||
| "打袋機", | |||
| f"已送出 {n} 張,但伺服器記錄失敗:{err}", | |||
| ), | |||
| ) | |||
| except ConnectionRefusedError: | |||
| root.after( | |||
| 0, | |||
| lambda: set_status_message( | |||
| f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", | |||
| is_error=True, | |||
| ), | |||
| ) | |||
| except socket.timeout: | |||
| root.after( | |||
| 0, | |||
| lambda: set_status_message( | |||
| f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", | |||
| is_error=True, | |||
| ), | |||
| ) | |||
| except OSError as err: | |||
| root.after( | |||
| 0, | |||
| lambda e=err: set_status_message(f"列印失敗:{e}", is_error=True), | |||
| ) | |||
| finally: | |||
| with dataflex_lock: | |||
| dataflex_busy_ref[0] = False | |||
| threading.Thread(target=worker, daemon=True).start() | |||
| def run_label_print_batch_thread( | |||
| root: tk.Tk, | |||
| label_lock: threading.Lock, | |||
| label_busy_ref: list, | |||
| com: str, | |||
| zpl_img: str, | |||
| n: int, | |||
| jo_id: Optional[int], | |||
| base_url: str, | |||
| set_status_message: Callable[[str, bool], None], | |||
| on_recorded: Callable[[], None], | |||
| ) -> None: | |||
| """ | |||
| Send n label copies off the main thread so DataFlex / laser / UI stay usable in parallel. | |||
| """ | |||
| def worker() -> None: | |||
| with label_lock: | |||
| if label_busy_ref[0]: | |||
| root.after( | |||
| 0, | |||
| lambda: messagebox.showwarning( | |||
| "標籤機", | |||
| "請等待目前標籤列印完成。", | |||
| ), | |||
| ) | |||
| return | |||
| label_busy_ref[0] = True | |||
| try: | |||
| send_zpl_to_label_printer_batch(com, zpl_img, n) | |||
| root.after( | |||
| 0, | |||
| lambda: set_status_message(f"已送出列印:標籤 x {n} 張", is_error=False), | |||
| ) | |||
| if jo_id is not None: | |||
| try: | |||
| submit_job_order_print_submit(base_url, int(jo_id), n, "LABEL") | |||
| root.after(0, on_recorded) | |||
| root.after( | |||
| 0, | |||
| lambda: messagebox.showinfo( | |||
| "標籤機", | |||
| f"已送出列印:{n} 張標籤(已記錄)", | |||
| ), | |||
| ) | |||
| except requests.RequestException as ex: | |||
| root.after( | |||
| 0, | |||
| lambda err=str(ex): messagebox.showwarning( | |||
| "標籤機", | |||
| f"標籤已列印 {n} 張,但伺服器記錄失敗:{err}", | |||
| ), | |||
| ) | |||
| else: | |||
| root.after( | |||
| 0, | |||
| lambda: messagebox.showwarning( | |||
| "標籤機", | |||
| f"已送出列印:{n} 張標籤(無工單 id,無法寫入伺服器記錄)", | |||
| ), | |||
| ) | |||
| except Exception as err: | |||
| root.after( | |||
| 0, | |||
| lambda e=str(err): messagebox.showerror("標籤機", f"列印失敗:{e}"), | |||
| ) | |||
| finally: | |||
| with label_lock: | |||
| label_busy_ref[0] = False | |||
| threading.Thread(target=worker, daemon=True).start() | |||
| def _printed_qty_int(raw) -> int: | |||
| """Parse API printed qty field (may be float JSON) to int.""" | |||
| try: | |||
| @@ -966,7 +1234,8 @@ def on_job_order_click(jo: dict, batch: str) -> None: | |||
| def ask_label_count(parent: tk.Tk) -> Optional[int]: | |||
| """ | |||
| When printer is 標簽機, ask how many labels to print: | |||
| optional direct qty in text field, +50/+10/+5/+1, 重置, then 確認送出. | |||
| optional direct qty in text field (e.g. 150), +50/+10/+5/+1, 重置, then 確認送出. | |||
| That count becomes ZPL ^PQ in one job — 150 → 150 identical labels. | |||
| Returns count (>= 1), or None if cancelled. | |||
| """ | |||
| result: list[Optional[int]] = [None] | |||
| @@ -1148,28 +1417,69 @@ def _sleep_interruptible(stop_event: threading.Event, total_sec: float) -> None: | |||
| time.sleep(min(0.05, remaining)) | |||
| def open_dataflex_stop_window(parent: tk.Tk, stop_event: threading.Event) -> tk.Toplevel: | |||
| """Small window with 停止列印 for DataFlex continuous mode (non-modal so stop stays usable).""" | |||
| def open_dataflex_stop_window( | |||
| parent: tk.Tk, | |||
| stop_event: threading.Event, | |||
| stop_win_ref: list, | |||
| ) -> tk.Toplevel: | |||
| """ | |||
| Small window with 停止列印 for DataFlex continuous mode (non-modal so stop stays usable). | |||
| Stays above other dialogs (e.g. 標籤機 quantity) via periodic lift + optional topmost on Windows, | |||
| so switching printer and printing labels does not hide the stop control. Ref is cleared on destroy. | |||
| """ | |||
| win = tk.Toplevel(parent) | |||
| win.title("打袋機連續列印") | |||
| win.geometry("420x170") | |||
| win.transient(parent) | |||
| # On Windows, transient(root) can hide this Toplevel when the menubutton / printer row | |||
| # updates (e.g. switching to 激光機); keep transient only on non-Windows. | |||
| if os.name != "nt": | |||
| win.transient(parent) | |||
| win.configure(bg=BG_TOP) | |||
| stop_win_ref[0] = win | |||
| if os.name == "nt": | |||
| try: | |||
| win.attributes("-topmost", True) | |||
| except tk.TclError: | |||
| pass | |||
| tk.Label( | |||
| win, | |||
| text="連續列印進行中,可隨時按下方停止。", | |||
| text="連續列印進行中(與上方列印機選項無關),可隨時按下方停止。", | |||
| font=get_font(FONT_SIZE), | |||
| bg=BG_TOP, | |||
| wraplength=400, | |||
| ).pack(pady=(16, 8)) | |||
| def clear_topmost() -> None: | |||
| if os.name == "nt": | |||
| try: | |||
| win.attributes("-topmost", False) | |||
| except tk.TclError: | |||
| pass | |||
| def stop() -> None: | |||
| stop_event.set() | |||
| stop_win_ref[0] = None | |||
| clear_topmost() | |||
| try: | |||
| win.destroy() | |||
| except tk.TclError: | |||
| pass | |||
| def periodic_lift() -> None: | |||
| if stop_win_ref[0] is not win: | |||
| return | |||
| try: | |||
| if not win.winfo_exists(): | |||
| return | |||
| win.lift() | |||
| if os.name == "nt": | |||
| win.attributes("-topmost", True) | |||
| except tk.TclError: | |||
| return | |||
| parent.after(4000, periodic_lift) | |||
| tk.Button( | |||
| win, | |||
| text="停止列印", | |||
| @@ -1181,6 +1491,7 @@ def open_dataflex_stop_window(parent: tk.Tk, stop_event: threading.Event) -> tk. | |||
| pady=10, | |||
| ).pack(pady=12) | |||
| win.protocol("WM_DELETE_WINDOW", stop) | |||
| parent.after(500, periodic_lift) | |||
| return win | |||
| @@ -1240,6 +1551,27 @@ def main() -> None: | |||
| # Laser: keep connection open for repeated sends; close when switching away | |||
| laser_conn_ref: list = [None] | |||
| laser_send_busy_ref: list = [False] | |||
| # DataFlex: shared lock so fixed-qty and continuous jobs do not overlap (independent of laser/label) | |||
| dataflex_lock = threading.Lock() | |||
| dataflex_busy_ref: list = [False] | |||
| # 標籤機: own lock so label jobs do not overlap; does not block DataFlex or laser | |||
| label_lock = threading.Lock() | |||
| label_busy_ref: list = [False] | |||
| # DataFlex continuous: stop Toplevel ref so we can lift it after other dialogs | |||
| dataflex_stop_win_ref: list = [None] | |||
| def lift_dataflex_stop_if_running() -> None: | |||
| """After closing another dialog (e.g. 標籤印數), bring the stop panel forward again.""" | |||
| w = dataflex_stop_win_ref[0] | |||
| if w is None: | |||
| return | |||
| try: | |||
| if w.winfo_exists(): | |||
| w.lift() | |||
| if os.name == "nt": | |||
| w.attributes("-topmost", True) | |||
| except tk.TclError: | |||
| pass | |||
| # Top: left [前一天] [date] [後一天] | right [printer dropdown] | |||
| top = tk.Frame(root, padx=12, pady=12, bg=BG_TOP) | |||
| @@ -1291,6 +1623,44 @@ def main() -> None: | |||
| ttk.Button(right_frame, text="設定", command=lambda: open_setup_window(root, settings, base_url_ref)).pack( | |||
| side=tk.LEFT, padx=(0, 12) | |||
| ) | |||
| def on_dataflex_host_reset() -> None: | |||
| """Send ~JA/~RO/~JR to clear E1005 latch without turning the printer off.""" | |||
| ip = (settings.get("dabag_ip") or "").strip() | |||
| port_str = (settings.get("dabag_port") or "3008").strip() | |||
| if not ip: | |||
| messagebox.showwarning("打袋機", "請先在「設定」填寫打袋機 IP。") | |||
| return | |||
| try: | |||
| port = int(port_str) | |||
| except ValueError: | |||
| port = 3008 | |||
| def worker() -> None: | |||
| try: | |||
| send_dataflex_job_counter_reset(ip, port) | |||
| root.after( | |||
| 0, | |||
| lambda: messagebox.showinfo( | |||
| "打袋機", | |||
| "已送出主機重設(緩衝清除/計數/軟重設)。\n" | |||
| "若畫面仍顯示 E1005,請再按一次或關機重開。", | |||
| ), | |||
| ) | |||
| except OSError as ex: | |||
| root.after( | |||
| 0, | |||
| lambda e=str(ex): messagebox.showerror( | |||
| "打袋機", | |||
| f"連線失敗,無法重設:{e}", | |||
| ), | |||
| ) | |||
| threading.Thread(target=worker, daemon=True).start() | |||
| ttk.Button(right_frame, text="打袋重設", command=on_dataflex_host_reset).pack( | |||
| side=tk.LEFT, padx=(0, 8) | |||
| ) | |||
| # 列印機 label: green when printer connected, red when not (checked periodically) | |||
| printer_status_lbl = tk.Label( | |||
| right_frame, | |||
| @@ -1350,6 +1720,8 @@ def main() -> None: | |||
| except Exception: | |||
| pass | |||
| laser_conn_ref[0] = None | |||
| # DataFlex continuous stop panel can drop behind after OptionMenu closes; re-lift for any choice. | |||
| root.after(100, lift_dataflex_stop_if_running) | |||
| printer_var.trace_add("write", on_printer_selection_changed) | |||
| @@ -1638,17 +2010,35 @@ def main() -> None: | |||
| label_text = (lot_no or b).strip() | |||
| if continuous: | |||
| stop_ev = threading.Event() | |||
| stop_win = open_dataflex_stop_window(root, stop_ev) | |||
| stop_win = open_dataflex_stop_window( | |||
| root, stop_ev, dataflex_stop_win_ref | |||
| ) | |||
| def dflex_worker() -> None: | |||
| with dataflex_lock: | |||
| if dataflex_busy_ref[0]: | |||
| root.after( | |||
| 0, | |||
| lambda: messagebox.showwarning( | |||
| "打袋機", | |||
| "請等待目前列印完成或先停止連續列印。", | |||
| ), | |||
| ) | |||
| return | |||
| dataflex_busy_ref[0] = True | |||
| printed = 0 | |||
| error_shown = False | |||
| try: | |||
| # One TCP job per bag (not one endless stream). Persistent socket | |||
| # caused E1005 over-qty on some DataFlex units after a few labels. | |||
| send_dataflex_job_counter_reset(ip, port) | |||
| while not stop_ev.is_set(): | |||
| send_zpl_to_dataflex(ip, port, zpl) | |||
| printed += 1 | |||
| _sleep_interruptible(stop_ev, 2.0) | |||
| _sleep_interruptible( | |||
| stop_ev, | |||
| DATAFLEX_INTER_LABEL_DELAY_SEC, | |||
| ) | |||
| except ConnectionRefusedError: | |||
| error_shown = True | |||
| root.after( | |||
| @@ -1677,8 +2067,16 @@ def main() -> None: | |||
| ), | |||
| ) | |||
| finally: | |||
| with dataflex_lock: | |||
| dataflex_busy_ref[0] = False | |||
| def _done() -> None: | |||
| dataflex_stop_win_ref[0] = None | |||
| try: | |||
| if os.name == "nt": | |||
| stop_win.attributes("-topmost", False) | |||
| except tk.TclError: | |||
| pass | |||
| try: | |||
| stop_win.destroy() | |||
| except tk.TclError: | |||
| @@ -1713,37 +2111,29 @@ def main() -> None: | |||
| threading.Thread(target=dflex_worker, daemon=True).start() | |||
| else: | |||
| try: | |||
| send_dataflex_job_counter_reset(ip, port) | |||
| for i in range(n): | |||
| send_zpl_to_dataflex(ip, port, zpl) | |||
| if i < n - 1: | |||
| time.sleep(2) | |||
| set_status_message(f"已送出列印:批次 {label_text} x {n} 張", is_error=False) | |||
| jo_id = j.get("id") | |||
| if jo_id is not None: | |||
| try: | |||
| submit_job_order_print_submit( | |||
| base_url_ref[0], int(jo_id), n, "DATAFLEX" | |||
| ) | |||
| load_job_orders(from_user_date_change=False) | |||
| except requests.RequestException as ex: | |||
| messagebox.showwarning( | |||
| "打袋機", | |||
| f"已送出 {n} 張,但伺服器記錄失敗:{ex}", | |||
| ) | |||
| except ConnectionRefusedError: | |||
| set_status_message(f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", is_error=True) | |||
| except socket.timeout: | |||
| set_status_message(f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", is_error=True) | |||
| except OSError as err: | |||
| set_status_message(f"列印失敗:{err}", is_error=True) | |||
| run_dataflex_fixed_qty_thread( | |||
| root=root, | |||
| dataflex_lock=dataflex_lock, | |||
| dataflex_busy_ref=dataflex_busy_ref, | |||
| ip=ip, | |||
| port=port, | |||
| n=n, | |||
| zpl=zpl, | |||
| label_text=label_text, | |||
| jo_id=j.get("id"), | |||
| base_url=base_url_ref[0], | |||
| set_status_message=set_status_message, | |||
| on_recorded=lambda: load_job_orders( | |||
| from_user_date_change=False | |||
| ), | |||
| ) | |||
| elif printer_var.get() == "標簽機": | |||
| com = (settings.get("label_com") or "").strip() | |||
| if not com: | |||
| messagebox.showerror("標簽機", "請在設定中填寫標簽機名稱 (例如:TSC TTP-246M Pro)。") | |||
| else: | |||
| count = ask_label_count(root) | |||
| lift_dataflex_stop_if_running() | |||
| if count is not None: | |||
| item_code = j.get("itemCode") or "—" | |||
| item_name = j.get("itemName") or "—" | |||
| @@ -1762,31 +2152,20 @@ def main() -> None: | |||
| lot_no=lot_no, | |||
| ) | |||
| zpl_img = _image_to_zpl_gfa(label_img) | |||
| for i in range(n): | |||
| send_zpl_to_label_printer(com, zpl_img) | |||
| if i < n - 1: | |||
| time.sleep(0.5) | |||
| jo_id = j.get("id") | |||
| if jo_id is not None: | |||
| try: | |||
| submit_job_order_print_submit( | |||
| base_url_ref[0], int(jo_id), n, "LABEL" | |||
| ) | |||
| load_job_orders(from_user_date_change=False) | |||
| messagebox.showinfo( | |||
| "標籤機", | |||
| f"已送出列印:{n} 張標籤(已記錄)", | |||
| ) | |||
| except requests.RequestException as ex: | |||
| messagebox.showwarning( | |||
| "標籤機", | |||
| f"標籤已列印 {n} 張,但伺服器記錄失敗:{ex}", | |||
| ) | |||
| else: | |||
| messagebox.showwarning( | |||
| "標籤機", | |||
| f"已送出列印:{n} 張標籤(無工單 id,無法寫入伺服器記錄)", | |||
| ) | |||
| run_label_print_batch_thread( | |||
| root=root, | |||
| label_lock=label_lock, | |||
| label_busy_ref=label_busy_ref, | |||
| com=com, | |||
| zpl_img=zpl_img, | |||
| n=n, | |||
| jo_id=j.get("id"), | |||
| base_url=base_url_ref[0], | |||
| set_status_message=set_status_message, | |||
| on_recorded=lambda: load_job_orders( | |||
| from_user_date_change=False | |||
| ), | |||
| ) | |||
| except Exception as err: | |||
| messagebox.showerror("標簽機", f"列印失敗:{err}") | |||
| elif printer_var.get() == "激光機": | |||