diff --git a/python/Bag3.py b/python/Bag3.py index 887c401..4ab1103 100644 --- a/python/Bag3.py +++ b/python/Bag3.py @@ -26,6 +26,7 @@ import tempfile import threading import time import tkinter as tk +from dataclasses import dataclass from datetime import date, datetime, timedelta from tkinter import messagebox, ttk from typing import Callable, Optional, Tuple @@ -1887,6 +1888,204 @@ def ask_bag_count(parent: tk.Tk) -> Optional[Tuple[int, bool]]: return result[0] +@dataclass(frozen=True) +class DataflexPrintSession: + """ + Snapshot taken when the user starts DataFlex print (especially C 連續印). + The worker must use only this object — not grid row index, scroll position, or selection. + """ + + job_order_id: Optional[int] + job_code: str + item_code: str + item_name: str + label_text: str + zpl: str + printer_ip: str + printer_port: int + batch_display: str + + +def build_dataflex_print_session( + jo: dict, + batch: str, + zpl: str, + label_text: str, + printer_ip: str, + printer_port: int, +) -> DataflexPrintSession: + jo_id = jo.get("id") + jo_code = (jo.get("code") or "").strip() + if not jo_code and jo_id is not None: + jo_code = f"#{jo_id}" + elif not jo_code: + jo_code = "—" + return DataflexPrintSession( + job_order_id=int(jo_id) if jo_id is not None else None, + job_code=jo_code, + item_code=(jo.get("itemCode") or "—").strip(), + item_name=(jo.get("itemName") or "—").strip(), + label_text=label_text, + zpl=zpl, + printer_ip=printer_ip, + printer_port=printer_port, + batch_display=(batch or "—").strip(), + ) + + +def run_dataflex_continuous_thread( + root: tk.Tk, + session: DataflexPrintSession, + stop_event: threading.Event, + stop_win: tk.Toplevel, + dataflex_lock: threading.Lock, + dataflex_busy_ref: list, + dataflex_stop_win_ref: list, + active_session_ref: list, + base_url: str, + set_status_message: Callable[[str, bool], None], + on_recorded: Callable[[], None], +) -> None: + """Send bags in a loop until stop_event; all payload comes from session (in-memory snapshot).""" + + def worker() -> None: + with dataflex_lock: + if dataflex_busy_ref[0]: + active_session_ref[0] = None + + def _abort_start() -> None: + messagebox.showwarning( + "打袋機", + "請等待目前列印完成或先停止連續列印。", + ) + dataflex_stop_win_ref[0] = None + try: + stop_win.destroy() + except tk.TclError: + pass + + root.after(0, _abort_start) + return + dataflex_busy_ref[0] = True + + ip = session.printer_ip + port = session.printer_port + zpl = session.zpl + label_text = session.label_text + printed = 0 + error_shown = False + try: + send_dataflex_start_job_reset(ip, port, force=True) + while not stop_event.is_set(): + send_dataflex_label_with_recovery(ip, port, zpl) + printed += 1 + if DATAFLEX_UI_PROGRESS_EVERY > 0 and ( + printed == 1 or printed % DATAFLEX_UI_PROGRESS_EVERY == 0 + ): + p = printed + root.after( + 0, + lambda p=p, jc=session.job_code: set_status_message( + f"連續打袋 · 工單 {jc}… 已印 {p} 張", + is_error=False, + ), + ) + if ( + DATAFLEX_VERIFY_EVERY_LABELS > 0 + and printed % DATAFLEX_VERIFY_EVERY_LABELS == 0 + ): + recover_dataflex_if_host_fault(ip, port) + if ( + DATAFLEX_COOLDOWN_EVERY_LABELS > 0 + and printed % DATAFLEX_COOLDOWN_EVERY_LABELS == 0 + ): + _sleep_interruptible(stop_event, max(0.0, DATAFLEX_COOLDOWN_SEC)) + if ( + DATAFLEX_THERMAL_REST_EVERY_LABELS > 0 + and printed % DATAFLEX_THERMAL_REST_EVERY_LABELS == 0 + ): + _sleep_interruptible(stop_event, max(0.0, DATAFLEX_THERMAL_REST_SEC)) + _sleep_interruptible(stop_event, DATAFLEX_INTER_LABEL_DELAY_SEC) + except ConnectionRefusedError: + error_shown = True + root.after( + 0, + lambda: set_status_message( + f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", + is_error=True, + ), + ) + except socket.timeout: + error_shown = True + root.after( + 0, + lambda: set_status_message( + f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", + is_error=True, + ), + ) + except OSError as err: + error_shown = True + root.after( + 0, + lambda e=err: set_status_message(f"列印失敗:{e}", is_error=True), + ) + except RuntimeError as err: + error_shown = True + root.after( + 0, + lambda e=err: set_status_message(f"打袋機錯誤:{e}", is_error=True), + ) + except Exception as err: + error_shown = True + root.after( + 0, + lambda e=err: set_status_message(f"打袋機例外:{e}", is_error=True), + ) + finally: + with dataflex_lock: + dataflex_busy_ref[0] = False + active_session_ref[0] = None + + 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: + pass + jc = session.job_code + if printed > 0: + set_status_message( + f"連續列印結束:工單 {jc} · {label_text},已印 {printed} 張", + is_error=False, + ) + if session.job_order_id is not None: + try: + submit_job_order_print_submit( + base_url, + session.job_order_id, + printed, + "DATAFLEX", + ) + on_recorded() + except requests.RequestException as ex: + messagebox.showwarning( + "打袋機", + f"列印可能已完成,但伺服器記錄失敗(可再試):{ex}", + ) + elif not error_shown: + set_status_message("連續列印未印出或已取消", is_error=True) + + root.after(0, _done) + + threading.Thread(target=worker, daemon=True).start() + + def _sleep_interruptible(stop_event: threading.Event, total_sec: float) -> None: """Sleep up to total_sec but return early if stop_event is set.""" end = time.perf_counter() + total_sec @@ -1903,16 +2102,18 @@ def open_dataflex_stop_window( parent: tk.Tk, stop_event: threading.Event, stop_win_ref: list, + session: DataflexPrintSession, ) -> 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. + Job details come from the in-memory session snapshot, not the grid selection. """ win = tk.Toplevel(parent) win.title("打袋機連續列印") - win.geometry("420x170") + win.geometry("480x240") # 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": @@ -1927,11 +2128,28 @@ def open_dataflex_stop_window( tk.Label( win, - text="連續列印進行中(與上方列印機選項無關),可隨時按下方停止。", + text="連續列印進行中(內容以按下 C 時的工單為準,與列表捲動/日期無關)", + font=get_font(FONT_SIZE_META), + bg=BG_TOP, + wraplength=440, + justify=tk.CENTER, + ).pack(pady=(12, 6)) + detail = ( + f"工單:{session.job_code}\n" + f"品號:{session.item_code}\n" + f"品名:{session.item_name}\n" + f"批次/批號:{session.label_text}" + ) + tk.Label( + win, + text=detail, font=get_font(FONT_SIZE), bg=BG_TOP, - wraplength=400, - ).pack(pady=(16, 8)) + fg="#111111", + wraplength=440, + justify=tk.LEFT, + anchor=tk.W, + ).pack(padx=16, pady=(0, 8), fill=tk.X) def clear_topmost() -> None: if os.name == "nt": @@ -2043,6 +2261,8 @@ def main() -> None: label_busy_ref: list = [False] # DataFlex continuous: stop Toplevel ref so we can lift it after other dialogs dataflex_stop_win_ref: list = [None] + # In-memory job snapshot for C 連續印 (not tied to grid row position after start) + active_dataflex_session_ref: list[Optional[DataflexPrintSession]] = [None] def lift_dataflex_stop_if_running() -> None: """After closing another dialog (e.g. 標籤印數), bring the stop panel forward again.""" @@ -2475,6 +2695,20 @@ def main() -> None: name_lbl.pack(anchor=tk.NW) def _on_click(e, j=jo, b=batch, r=row): + if ( + printer_var.get() == "打袋機 DataFlex" + and dataflex_busy_ref[0] + and active_dataflex_session_ref[0] is not None + ): + s = active_dataflex_session_ref[0] + messagebox.showwarning( + "打袋機", + f"連續列印進行中,請先按「停止列印」。\n\n" + f"工單:{s.job_code}\n" + f"品號:{s.item_code}\n" + f"品名:{s.item_name}", + ) + return if selected_row_holder[0] is not None: set_row_highlight(selected_row_holder[0], False) set_row_highlight(r, True) @@ -2513,158 +2747,43 @@ def main() -> None: ) label_text = (lot_no or b).strip() if continuous: + if dataflex_busy_ref[0]: + messagebox.showwarning( + "打袋機", + "請等待目前列印完成或先停止連續列印。", + ) + return + session = build_dataflex_print_session( + j, + b, + zpl, + label_text, + ip, + port, + ) + active_dataflex_session_ref[0] = session stop_ev = threading.Event() stop_win = open_dataflex_stop_window( - root, stop_ev, dataflex_stop_win_ref + root, + stop_ev, + dataflex_stop_win_ref, + session, + ) + run_dataflex_continuous_thread( + root=root, + session=session, + stop_event=stop_ev, + stop_win=stop_win, + dataflex_lock=dataflex_lock, + dataflex_busy_ref=dataflex_busy_ref, + dataflex_stop_win_ref=dataflex_stop_win_ref, + active_session_ref=active_dataflex_session_ref, + base_url=base_url_ref[0], + set_status_message=set_status_message, + on_recorded=lambda: load_job_orders( + from_user_date_change=False + ), ) - - 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_start_job_reset(ip, port, force=True) - while not stop_ev.is_set(): - send_dataflex_label_with_recovery(ip, port, zpl) - printed += 1 - if DATAFLEX_UI_PROGRESS_EVERY > 0 and ( - printed == 1 - or printed % DATAFLEX_UI_PROGRESS_EVERY == 0 - ): - p = printed - root.after( - 0, - lambda p=p: set_status_message( - f"連續打袋列印中… 已印 {p} 張", - is_error=False, - ), - ) - if ( - DATAFLEX_VERIFY_EVERY_LABELS > 0 - and printed % DATAFLEX_VERIFY_EVERY_LABELS == 0 - ): - recover_dataflex_if_host_fault(ip, port) - if ( - DATAFLEX_COOLDOWN_EVERY_LABELS > 0 - and printed % DATAFLEX_COOLDOWN_EVERY_LABELS == 0 - ): - _sleep_interruptible( - stop_ev, - max(0.0, DATAFLEX_COOLDOWN_SEC), - ) - if ( - DATAFLEX_THERMAL_REST_EVERY_LABELS > 0 - and printed % DATAFLEX_THERMAL_REST_EVERY_LABELS == 0 - ): - _sleep_interruptible( - stop_ev, - max(0.0, DATAFLEX_THERMAL_REST_SEC), - ) - _sleep_interruptible( - stop_ev, - DATAFLEX_INTER_LABEL_DELAY_SEC, - ) - except ConnectionRefusedError: - error_shown = True - root.after( - 0, - lambda: set_status_message( - f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。", - is_error=True, - ), - ) - except socket.timeout: - error_shown = True - root.after( - 0, - lambda: set_status_message( - f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。", - is_error=True, - ), - ) - except OSError as err: - error_shown = True - root.after( - 0, - lambda e=err: set_status_message( - f"列印失敗:{e}", - is_error=True, - ), - ) - except RuntimeError as err: - error_shown = True - root.after( - 0, - lambda e=err: set_status_message( - f"打袋機錯誤:{e}", - is_error=True, - ), - ) - except Exception as err: - error_shown = True - root.after( - 0, - lambda e=err: set_status_message( - f"打袋機例外:{e}", - is_error=True, - ), - ) - 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: - pass - if printed > 0: - set_status_message( - f"連續列印結束:批次 {label_text},已印 {printed} 張", - 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), - printed, - "DATAFLEX", - ) - load_job_orders(from_user_date_change=False) - except requests.RequestException as ex: - messagebox.showwarning( - "打袋機", - f"列印可能已完成,但伺服器記錄失敗(可再試):{ex}", - ) - elif not error_shown: - set_status_message( - "連續列印未印出或已取消", - is_error=True, - ) - - root.after(0, _done) - - threading.Thread(target=dflex_worker, daemon=True).start() else: run_dataflex_fixed_qty_thread( root=root, @@ -2862,5 +2981,41 @@ def main() -> None: root.mainloop() +def _startup_error_log_path() -> str: + if getattr(sys, "frozen", False): + base = os.path.dirname(sys.executable) + else: + base = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(base, "bag3_startup_error.log") + + if __name__ == "__main__": - main() \ No newline at end of file + try: + main() + except SystemExit: + raise + except Exception: + import traceback + + log_path = _startup_error_log_path() + try: + with open(log_path, "w", encoding="utf-8") as f: + traceback.print_exc(file=f) + except OSError: + log_path = "(could not write log file)" + msg = f"Bag3 啟動失敗,詳情已寫入:\n{log_path}" + print(msg, file=sys.stderr) + traceback.print_exc() + try: + _err_root = tk.Tk() + _err_root.withdraw() + messagebox.showerror("Bag3", msg) + _err_root.destroy() + except Exception: + pass + if getattr(sys, "frozen", False): + try: + input("按 Enter 關閉…") + except (EOFError, KeyboardInterrupt): + pass + sys.exit(1) \ No newline at end of file