[email protected] 1 месяц назад
Родитель
Сommit
6dc9d30292
1 измененных файлов: 309 добавлений и 154 удалений
  1. +309
    -154
      python/Bag3.py

+ 309
- 154
python/Bag3.py Просмотреть файл

@@ -26,6 +26,7 @@ import tempfile
import threading import threading
import time import time
import tkinter as tk import tkinter as tk
from dataclasses import dataclass
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import Callable, Optional, Tuple from typing import Callable, Optional, Tuple
@@ -1887,6 +1888,204 @@ def ask_bag_count(parent: tk.Tk) -> Optional[Tuple[int, bool]]:
return result[0] 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: def _sleep_interruptible(stop_event: threading.Event, total_sec: float) -> None:
"""Sleep up to total_sec but return early if stop_event is set.""" """Sleep up to total_sec but return early if stop_event is set."""
end = time.perf_counter() + total_sec end = time.perf_counter() + total_sec
@@ -1903,16 +2102,18 @@ def open_dataflex_stop_window(
parent: tk.Tk, parent: tk.Tk,
stop_event: threading.Event, stop_event: threading.Event,
stop_win_ref: list, stop_win_ref: list,
session: DataflexPrintSession,
) -> tk.Toplevel: ) -> tk.Toplevel:
""" """
Small window with 停止列印 for DataFlex continuous mode (non-modal so stop stays usable). 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, 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. 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 = tk.Toplevel(parent)
win.title("打袋機連續列印") win.title("打袋機連續列印")
win.geometry("420x170")
win.geometry("480x240")
# On Windows, transient(root) can hide this Toplevel when the menubutton / printer row # On Windows, transient(root) can hide this Toplevel when the menubutton / printer row
# updates (e.g. switching to 激光機); keep transient only on non-Windows. # updates (e.g. switching to 激光機); keep transient only on non-Windows.
if os.name != "nt": if os.name != "nt":
@@ -1927,11 +2128,28 @@ def open_dataflex_stop_window(


tk.Label( tk.Label(
win, 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), font=get_font(FONT_SIZE),
bg=BG_TOP, 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: def clear_topmost() -> None:
if os.name == "nt": if os.name == "nt":
@@ -2043,6 +2261,8 @@ def main() -> None:
label_busy_ref: list = [False] label_busy_ref: list = [False]
# DataFlex continuous: stop Toplevel ref so we can lift it after other dialogs # DataFlex continuous: stop Toplevel ref so we can lift it after other dialogs
dataflex_stop_win_ref: list = [None] 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: def lift_dataflex_stop_if_running() -> None:
"""After closing another dialog (e.g. 標籤印數), bring the stop panel forward again.""" """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) name_lbl.pack(anchor=tk.NW)


def _on_click(e, j=jo, b=batch, r=row): 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: if selected_row_holder[0] is not None:
set_row_highlight(selected_row_holder[0], False) set_row_highlight(selected_row_holder[0], False)
set_row_highlight(r, True) set_row_highlight(r, True)
@@ -2513,158 +2747,43 @@ def main() -> None:
) )
label_text = (lot_no or b).strip() label_text = (lot_no or b).strip()
if continuous: 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_ev = threading.Event()
stop_win = open_dataflex_stop_window( 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: else:
run_dataflex_fixed_qty_thread( run_dataflex_fixed_qty_thread(
root=root, root=root,
@@ -2862,5 +2981,41 @@ def main() -> None:
root.mainloop() 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__": if __name__ == "__main__":
main()
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)

Загрузка…
Отмена
Сохранить