|
- #!/usr/bin/env python3
- """
- Bag1 – GUI to show FPSMS job orders by plan date.
- Uses the public API GET /py/job-orders (no login required).
- UI tuned for aged users: larger font, Traditional Chinese labels, prev/next date.
-
- Run: python Bag1.py
- """
-
- import json
- import os
- import socket
- import sys
- import tkinter as tk
- from datetime import date, timedelta
- from tkinter import messagebox, ttk
- from typing import Optional
-
- import requests
-
- try:
- import serial
- except ImportError:
- serial = None # type: ignore
-
- 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):
- _SETTINGS_DIR = os.path.dirname(sys.executable)
- else:
- _SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__))
- SETTINGS_FILE = os.path.join(_SETTINGS_DIR, "bag1_settings.json")
-
- DEFAULT_SETTINGS = {
- "api_ip": "localhost",
- "api_port": "8090",
- "dabag_ip": "",
- "dabag_port": "3008",
- "laser_ip": "",
- "laser_port": "9100",
- "label_com": "COM3",
- }
-
-
- def load_settings() -> dict:
- """Load settings from JSON file; return defaults if missing or invalid."""
- try:
- if os.path.isfile(SETTINGS_FILE):
- with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
- data = json.load(f)
- return {**DEFAULT_SETTINGS, **data}
- except Exception:
- pass
- return dict(DEFAULT_SETTINGS)
-
-
- def save_settings(settings: dict) -> None:
- """Save settings to JSON file."""
- with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
- json.dump(settings, f, indent=2, ensure_ascii=False)
-
-
- def build_base_url(api_ip: str, api_port: str) -> str:
- ip = (api_ip or "localhost").strip()
- port = (api_port or "8090").strip()
- return f"http://{ip}:{port}/api"
-
-
- def try_printer_connection(printer_name: str, sett: dict) -> bool:
- """Try to connect to the selected printer (TCP IP:port or COM). Returns True if OK."""
- if printer_name == "打袋機 DataFlex":
- ip = (sett.get("dabag_ip") or "").strip()
- port_str = (sett.get("dabag_port") or "9100").strip()
- if not ip:
- return False
- try:
- port = int(port_str)
- s = socket.create_connection((ip, port), timeout=PRINTER_SOCKET_TIMEOUT)
- s.close()
- return True
- except (socket.error, ValueError, OSError):
- return False
- if printer_name == "激光機":
- ip = (sett.get("laser_ip") or "").strip()
- port_str = (sett.get("laser_port") or "9100").strip()
- if not ip:
- return False
- try:
- port = int(port_str)
- s = socket.create_connection((ip, port), timeout=PRINTER_SOCKET_TIMEOUT)
- s.close()
- return True
- except (socket.error, ValueError, OSError):
- return False
- if printer_name == "標簽機":
- if serial is None:
- return False
- com = (sett.get("label_com") or "").strip()
- if not com:
- return False
- try:
- ser = serial.Serial(com, timeout=1)
- ser.close()
- return True
- except (serial.SerialException, OSError):
- return False
- return False
-
- # Larger font for aged users (point size)
- FONT_SIZE = 16
- FONT_SIZE_BUTTONS = 15
- FONT_SIZE_QTY = 12 # smaller for 數量 under batch no.
- FONT_SIZE_ITEM = 20 # item code and item name (larger for readability)
- FONT_FAMILY = "Microsoft JhengHei UI" # Traditional Chinese; fallback to TkDefaultFont
- # Column widths: item code own column; item name at least double, wraps in its column
- ITEM_CODE_WRAP = 140 # item code column width (long codes wrap under code only)
- ITEM_NAME_WRAP = 640 # item name column (double width), wraps under name only
-
- # Light blue theme (softer than pure grey)
- BG_TOP = "#E8F4FC"
- BG_LIST = "#D4E8F7"
- BG_ROOT = "#E1F0FF"
- BG_ROW = "#C5E1F5"
- BG_ROW_SELECTED = "#6BB5FF" # highlighted when selected (for printing)
- # Connection status bar
- BG_STATUS_ERROR = "#FFCCCB" # red background when disconnected
- FG_STATUS_ERROR = "#B22222" # red text
- BG_STATUS_OK = "#90EE90" # light green when connected
- FG_STATUS_OK = "#006400" # green text
- RETRY_MS = 30 * 1000 # 30 seconds reconnect
- REFRESH_MS = 60 * 1000 # 60 seconds refresh when connected
- 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
-
-
- def _zpl_escape(s: str) -> str:
- """Escape text for ZPL ^FD...^FS (backslash and caret)."""
- return s.replace("\\", "\\\\").replace("^", "\\^")
-
-
- def generate_zpl_dataflex(
- batch_no: str,
- item_code: str,
- item_name: str,
- font_regular: str = "E:STXihei.ttf",
- font_bold: str = "E:STXihei.ttf",
- ) -> str:
- """
- Generate ZPL for DataFlex label (53 mm media, 90° rotated).
- Uses UTF-8 (^CI28) and configurable .TTF fonts for Chinese (e.g. E:STXihei.ttf).
- """
- desc = _zpl_escape((item_name or "—").strip())
- code = _zpl_escape((item_code or "—").strip())
- batch = _zpl_escape(batch_no.strip())
- return f"""^XA
- ^PW420
- ^LL780
- ^PO N
- ^CI28
- ^FO70,70
- ^A@R,60,60,{font_regular}^FD{desc}^FS
- ^FO220,70
- ^A@R,50,50,{font_bold}^FD{code}^FS
- ^FO310,70
- ^A@R,45,45,{font_bold}^FD批次: {batch}^FS
- ^FO150,420
- ^BQN,2,6^FDQA,{batch_no}^FS
- ^XZ"""
-
-
- def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None:
- """Send ZPL to DataFlex printer via TCP. Raises on connection/send error."""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.settimeout(DATAFLEX_SEND_TIMEOUT)
- try:
- sock.connect((ip, port))
- sock.sendall(zpl.encode("utf-8"))
- finally:
- sock.close()
-
-
- def format_qty(val) -> str:
- """Format quantity: integer without .0, with thousand separator."""
- if val is None:
- return "—"
- try:
- n = float(val)
- if n == int(n):
- return f"{int(n):,}"
- return f"{n:,.2f}".rstrip("0").rstrip(".")
- except (TypeError, ValueError):
- return str(val)
-
-
- def batch_no(year: int, job_order_id: int) -> str:
- """Batch no.: B + 4-digit year + jobOrderId zero-padded to 6 digits."""
- return f"B{year}{job_order_id:06d}"
-
-
- def get_font(size: int = FONT_SIZE, bold: bool = False) -> tuple:
- try:
- return (FONT_FAMILY, size, "bold" if bold else "normal")
- except Exception:
- return ("TkDefaultFont", size, "bold" if bold else "normal")
-
-
- def fetch_job_orders(base_url: str, plan_start: date) -> list:
- """Call GET /py/job-orders and return the JSON list."""
- url = f"{base_url.rstrip('/')}/py/job-orders"
- params = {"planStart": plan_start.isoformat()}
- resp = requests.get(url, params=params, timeout=30)
- resp.raise_for_status()
- return resp.json()
-
-
- def set_row_highlight(row_frame: tk.Frame, selected: bool) -> None:
- """Set row and all its child widgets to selected or normal background."""
- bg = BG_ROW_SELECTED if selected else BG_ROW
- row_frame.configure(bg=bg)
- for w in row_frame.winfo_children():
- if isinstance(w, (tk.Frame, tk.Label)):
- w.configure(bg=bg)
- for c in w.winfo_children():
- if isinstance(c, tk.Label):
- c.configure(bg=bg)
-
-
- def on_job_order_click(jo: dict, batch: str) -> None:
- """Show message and highlight row (keeps printing to selected printer)."""
- item_code = jo.get("itemCode") or "—"
- item_name = jo.get("itemName") or "—"
- messagebox.showinfo(
- "工單",
- f'已點選:批次 {batch}\n品號 {item_code} {item_name}',
- )
-
-
- def ask_label_count(parent: tk.Tk) -> Optional[str]:
- """
- When printer is 標簽機, ask how many labels to print.
- Returns "1", "10", "50", "100", "C" (continuous), or None if cancelled.
- """
- result = [None] # mutable so inner callback can set it
-
- win = tk.Toplevel(parent)
- win.title("標簽列印數量")
- win.geometry("360x180")
- win.transient(parent)
- win.grab_set()
- win.configure(bg=BG_TOP)
- ttk.Label(win, text="列印多少張標簽?", font=get_font(FONT_SIZE)).pack(pady=(16, 12))
- btn_frame = tk.Frame(win, bg=BG_TOP)
- btn_frame.pack(pady=8)
- for label, value in [("1", "1"), ("10", "10"), ("50", "50"), ("100", "100"), ("連續 (C)", "C")]:
- def make_cmd(v):
- def cmd():
- result[0] = v
- win.destroy()
- return cmd
- ttk.Button(btn_frame, text=label, command=make_cmd(value), width=10).pack(side=tk.LEFT, padx=4)
- win.protocol("WM_DELETE_WINDOW", win.destroy)
- win.wait_window()
- return result[0]
-
-
- def main() -> None:
- settings = load_settings()
- base_url_ref = [build_base_url(settings["api_ip"], settings["api_port"])]
-
- root = tk.Tk()
- root.title("FP-MTMS Bag v1.1 打袋機")
- root.geometry("1120x960")
- root.minsize(480, 360)
- root.configure(bg=BG_ROOT)
-
- # Style: larger font for aged users; light blue theme
- style = ttk.Style()
- try:
- style.configure(".", font=get_font(FONT_SIZE), background=BG_TOP)
- style.configure("TButton", font=get_font(FONT_SIZE_BUTTONS), background=BG_TOP)
- style.configure("TLabel", font=get_font(FONT_SIZE), background=BG_TOP)
- style.configure("TEntry", font=get_font(FONT_SIZE))
- style.configure("TFrame", background=BG_TOP)
- except tk.TclError:
- pass
-
- # Status bar at top: connection state (no popup on error)
- status_frame = tk.Frame(root, bg=BG_STATUS_ERROR, padx=12, pady=6)
- status_frame.pack(fill=tk.X)
- status_lbl = tk.Label(
- status_frame,
- text="連接不到服務器",
- font=get_font(FONT_SIZE_BUTTONS),
- bg=BG_STATUS_ERROR,
- fg=FG_STATUS_ERROR,
- anchor=tk.CENTER,
- )
- status_lbl.pack(fill=tk.X)
-
- def set_status_ok():
- status_frame.configure(bg=BG_STATUS_OK)
- status_lbl.configure(text="連接正常", bg=BG_STATUS_OK, fg=FG_STATUS_OK)
-
- def set_status_error():
- status_frame.configure(bg=BG_STATUS_ERROR)
- status_lbl.configure(text="連接不到服務器", bg=BG_STATUS_ERROR, fg=FG_STATUS_ERROR)
-
- # Top: left [前一天] [date] [後一天] | right [printer dropdown]
- top = tk.Frame(root, padx=12, pady=12, bg=BG_TOP)
- top.pack(fill=tk.X)
-
- date_var = tk.StringVar(value=date.today().isoformat())
- printer_options = ["打袋機 DataFlex", "標簽機", "激光機"]
- printer_var = tk.StringVar(value=printer_options[0])
-
- def go_prev_day() -> None:
- try:
- d = date.fromisoformat(date_var.get().strip())
- date_var.set((d - timedelta(days=1)).isoformat())
- load_job_orders(from_user_date_change=True)
- except ValueError:
- date_var.set(date.today().isoformat())
- load_job_orders(from_user_date_change=True)
-
- def go_next_day() -> None:
- try:
- d = date.fromisoformat(date_var.get().strip())
- date_var.set((d + timedelta(days=1)).isoformat())
- load_job_orders(from_user_date_change=True)
- except ValueError:
- date_var.set(date.today().isoformat())
- load_job_orders(from_user_date_change=True)
-
- # 前一天 (previous day) with left arrow icon
- btn_prev = ttk.Button(top, text="◀ 前一天", command=go_prev_day)
- btn_prev.pack(side=tk.LEFT, padx=(0, 8))
-
- # Date field (no "日期:" label); shorter width
- date_entry = tk.Entry(
- top,
- textvariable=date_var,
- font=get_font(FONT_SIZE),
- width=10,
- bg="white",
- )
- date_entry.pack(side=tk.LEFT, padx=(0, 8), ipady=4)
-
- # 後一天 (next day) with right arrow icon
- btn_next = ttk.Button(top, text="後一天 ▶", command=go_next_day)
- btn_next.pack(side=tk.LEFT, padx=(0, 8))
-
- # Top right: Setup button + printer selection
- right_frame = tk.Frame(top, bg=BG_TOP)
- right_frame.pack(side=tk.RIGHT)
- ttk.Button(right_frame, text="設定", command=lambda: open_setup_window(root, settings, base_url_ref)).pack(
- side=tk.LEFT, padx=(0, 12)
- )
- # 列印機 label: green when printer connected, red when not (checked periodically)
- printer_status_lbl = tk.Label(
- right_frame,
- text="列印機:",
- font=get_font(FONT_SIZE),
- bg=BG_STATUS_ERROR,
- fg="black",
- padx=6,
- pady=2,
- )
- printer_status_lbl.pack(side=tk.LEFT, padx=(0, 4))
- printer_combo = ttk.Combobox(
- right_frame,
- textvariable=printer_var,
- values=printer_options,
- state="readonly",
- width=14,
- font=get_font(FONT_SIZE),
- )
- printer_combo.pack(side=tk.LEFT)
-
- printer_after_ref = [None]
-
- def set_printer_status_ok():
- printer_status_lbl.configure(bg=BG_STATUS_OK, fg=FG_STATUS_OK)
-
- def set_printer_status_error():
- printer_status_lbl.configure(bg=BG_STATUS_ERROR, fg=FG_STATUS_ERROR)
-
- def check_printer() -> None:
- if printer_after_ref[0] is not None:
- root.after_cancel(printer_after_ref[0])
- printer_after_ref[0] = None
- ok = try_printer_connection(printer_var.get(), settings)
- if ok:
- set_printer_status_ok()
- printer_after_ref[0] = root.after(PRINTER_CHECK_MS, check_printer)
- else:
- set_printer_status_error()
- printer_after_ref[0] = root.after(PRINTER_RETRY_MS, check_printer)
-
- def on_printer_selection_changed(*args) -> None:
- check_printer()
-
- printer_var.trace_add("write", on_printer_selection_changed)
-
- def open_setup_window(parent_win: tk.Tk, sett: dict, base_url_ref_list: list) -> None:
- """Modal setup: API IP/port, 打袋機/激光機 IP+port, 標簽機 COM port."""
- d = tk.Toplevel(parent_win)
- d.title("設定")
- d.geometry("440x520")
- d.transient(parent_win)
- d.grab_set()
- d.configure(bg=BG_TOP)
- f = tk.Frame(d, padx=16, pady=16, bg=BG_TOP)
- f.pack(fill=tk.BOTH, expand=True)
- grid_row = [0] # use list so inner function can update
-
- def _ensure_dot_in_entry(entry: tk.Entry) -> None:
- """Allow typing dot (.) in Entry when IME or layout blocks it (e.g. 192.168.17.27)."""
- def on_key(event):
- if event.keysym in ("period", "decimal"):
- pos = entry.index(tk.INSERT)
- entry.insert(tk.INSERT, ".")
- return "break"
- entry.bind("<KeyPress>", on_key)
-
- def add_section(label_text: str, key_ip: str | None, key_port: str | None, key_single: str | None):
- out = []
- ttk.Label(f, text=label_text, font=get_font(FONT_SIZE_BUTTONS)).grid(
- row=grid_row[0], column=0, columnspan=2, sticky=tk.W, pady=(8, 2)
- )
- grid_row[0] += 1
- if key_single:
- ttk.Label(f, text="COM:").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.grid(row=grid_row[0], column=1, sticky=tk.W, pady=2)
- _ensure_dot_in_entry(e)
- grid_row[0] += 1
- return [(key_single, var)]
- if key_ip:
- ttk.Label(f, text="IP:").grid(row=grid_row[0], column=0, sticky=tk.W, pady=2)
- var_ip = tk.StringVar(value=sett.get(key_ip, ""))
- e_ip = tk.Entry(f, textvariable=var_ip, width=22, font=get_font(FONT_SIZE), bg="white")
- e_ip.grid(row=grid_row[0], column=1, sticky=tk.W, pady=2)
- _ensure_dot_in_entry(e_ip)
- grid_row[0] += 1
- out.append((key_ip, var_ip))
- if key_port:
- ttk.Label(f, text="Port:").grid(row=grid_row[0], column=0, sticky=tk.W, pady=2)
- var_port = tk.StringVar(value=sett.get(key_port, ""))
- e_port = tk.Entry(f, textvariable=var_port, width=12, font=get_font(FONT_SIZE), bg="white")
- e_port.grid(row=grid_row[0], column=1, sticky=tk.W, pady=2)
- _ensure_dot_in_entry(e_port)
- grid_row[0] += 1
- out.append((key_port, var_port))
- return out
-
- all_vars = []
- 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"))
-
- def on_save():
- for key, var in all_vars:
- sett[key] = var.get().strip()
- save_settings(sett)
- base_url_ref_list[0] = build_base_url(sett["api_ip"], sett["api_port"])
- d.destroy()
-
- btn_f = tk.Frame(d, bg=BG_TOP)
- btn_f.pack(pady=12)
- ttk.Button(btn_f, text="儲存", command=on_save).pack(side=tk.LEFT, padx=4)
- ttk.Button(btn_f, text="取消", command=d.destroy).pack(side=tk.LEFT, padx=4)
- d.wait_window()
-
- job_orders_frame = tk.Frame(root, bg=BG_LIST)
- job_orders_frame.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)
-
- # Scrollable area for buttons
- canvas = tk.Canvas(job_orders_frame, highlightthickness=0, bg=BG_LIST)
- scrollbar = ttk.Scrollbar(job_orders_frame, orient=tk.VERTICAL, command=canvas.yview)
- inner = tk.Frame(canvas, bg=BG_LIST)
-
- win_id = canvas.create_window((0, 0), window=inner, anchor=tk.NW)
- canvas.configure(yscrollcommand=scrollbar.set)
-
- def _on_inner_configure(event):
- canvas.configure(scrollregion=canvas.bbox("all"))
-
- def _on_canvas_configure(event):
- canvas.itemconfig(win_id, width=event.width)
-
- inner.bind("<Configure>", _on_inner_configure)
- canvas.bind("<Configure>", _on_canvas_configure)
-
- # Mouse wheel: make scroll work when hovering over canvas or the list (inner/buttons)
- def _on_mousewheel(event):
- if getattr(event, "delta", None) is not None:
- canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
- elif event.num == 5:
- canvas.yview_scroll(1, "units")
- elif event.num == 4:
- canvas.yview_scroll(-1, "units")
-
- canvas.bind("<MouseWheel>", _on_mousewheel)
- inner.bind("<MouseWheel>", _on_mousewheel)
- canvas.bind("<Button-4>", _on_mousewheel)
- canvas.bind("<Button-5>", _on_mousewheel)
- inner.bind("<Button-4>", _on_mousewheel)
- inner.bind("<Button-5>", _on_mousewheel)
-
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
- canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
-
- # Track which row is highlighted (selected for printing) and which job id
- selected_row_holder = [None] # [tk.Frame | None]
- selected_jo_id_ref = [None] # [int | None] job order id for selection preservation
- last_data_ref = [None] # [list | None] last successful fetch for current date
- after_id_ref = [None] # [str | None] root.after id to cancel retry/refresh
-
- def _data_equal(a: Optional[list], b: Optional[list]) -> bool:
- if a is None or b is None:
- return a is b
- if len(a) != len(b):
- return False
- ids_a = [x.get("id") for x in a]
- ids_b = [x.get("id") for x in b]
- return ids_a == ids_b
-
- def _build_list_from_data(data: list, plan_start: date, preserve_selection: bool) -> None:
- selected_row_holder[0] = None
- year = plan_start.year
- selected_id = selected_jo_id_ref[0] if preserve_selection else None
- found_row = None
- for jo in data:
- jo_id = jo.get("id")
- batch = batch_no(year, jo_id) if jo_id is not None else "—"
- item_code = jo.get("itemCode") or "—"
- item_name = jo.get("itemName") or "—"
- req_qty = jo.get("reqQty")
- qty_str = format_qty(req_qty)
- # Three columns: batch+數量 | item code (own column) | item name (≥2× width, wraps in column)
- row = tk.Frame(inner, bg=BG_ROW, relief=tk.RAISED, bd=2, cursor="hand2", padx=12, pady=10)
- row.pack(fill=tk.X, pady=4)
-
- left = tk.Frame(row, bg=BG_ROW)
- left.pack(side=tk.LEFT, anchor=tk.NW)
- batch_lbl = tk.Label(
- left,
- text=batch,
- font=get_font(FONT_SIZE_BUTTONS),
- bg=BG_ROW,
- fg="black",
- )
- batch_lbl.pack(anchor=tk.W)
- qty_lbl = None
- if qty_str != "—":
- qty_lbl = tk.Label(
- left,
- text=f"數量:{qty_str}",
- font=get_font(FONT_SIZE_QTY),
- bg=BG_ROW,
- fg="black",
- )
- qty_lbl.pack(anchor=tk.W)
-
- # Column 2: item code only, bigger font, wraps in its own column
- code_lbl = tk.Label(
- row,
- text=item_code,
- font=get_font(FONT_SIZE_ITEM),
- bg=BG_ROW,
- fg="black",
- wraplength=ITEM_CODE_WRAP,
- justify=tk.LEFT,
- anchor=tk.NW,
- )
- code_lbl.pack(side=tk.LEFT, anchor=tk.NW, padx=(12, 8))
-
- # Column 3: item name only, same bigger font, at least double width, wraps under its own column
- name_lbl = tk.Label(
- row,
- text=item_name or "—",
- font=get_font(FONT_SIZE_ITEM),
- bg=BG_ROW,
- fg="black",
- wraplength=ITEM_NAME_WRAP,
- justify=tk.LEFT,
- anchor=tk.NW,
- )
- name_lbl.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, anchor=tk.NW)
-
- def _on_click(e, j=jo, b=batch, r=row):
- if selected_row_holder[0] is not None:
- set_row_highlight(selected_row_holder[0], False)
- set_row_highlight(r, True)
- selected_row_holder[0] = r
- selected_jo_id_ref[0] = j.get("id")
- if printer_var.get() == "打袋機 DataFlex":
- ip = (settings.get("dabag_ip") or "").strip()
- port_str = (settings.get("dabag_port") or "3008").strip()
- try:
- port = int(port_str)
- except ValueError:
- port = 3008
- if not ip:
- messagebox.showerror("打袋機", "請在設定中填寫打袋機 DataFlex 的 IP。")
- else:
- item_code = j.get("itemCode") or "—"
- item_name = j.get("itemName") or "—"
- zpl = generate_zpl_dataflex(b, item_code, item_name)
- try:
- send_zpl_to_dataflex(ip, port, zpl)
- messagebox.showinfo("打袋機", f"已送出列印:批次 {b}")
- except ConnectionRefusedError:
- messagebox.showerror("打袋機", f"無法連線至 {ip}:{port},請確認印表機已開機且 IP 正確。")
- except socket.timeout:
- messagebox.showerror("打袋機", f"連線逾時 ({ip}:{port}),請檢查網路與連接埠。")
- except OSError as err:
- messagebox.showerror("打袋機", f"列印失敗:{err}")
- elif printer_var.get() == "標簽機":
- count = ask_label_count(root)
- if count is not None:
- if count == "C":
- msg = "已選擇連續列印標簽"
- else:
- msg = f"將列印 {count} 張標簽"
- messagebox.showinfo("標簽機", msg)
- on_job_order_click(j, b)
-
- for w in (row, left, batch_lbl, code_lbl, name_lbl):
- w.bind("<Button-1>", _on_click)
- w.bind("<MouseWheel>", _on_mousewheel)
- w.bind("<Button-4>", _on_mousewheel)
- w.bind("<Button-5>", _on_mousewheel)
- if qty_lbl is not None:
- qty_lbl.bind("<Button-1>", _on_click)
- qty_lbl.bind("<MouseWheel>", _on_mousewheel)
- qty_lbl.bind("<Button-4>", _on_mousewheel)
- qty_lbl.bind("<Button-5>", _on_mousewheel)
- if preserve_selection and selected_id is not None and jo.get("id") == selected_id:
- found_row = row
- if found_row is not None:
- set_row_highlight(found_row, True)
- selected_row_holder[0] = found_row
-
- def load_job_orders(from_user_date_change: bool = False) -> None:
- if after_id_ref[0] is not None:
- root.after_cancel(after_id_ref[0])
- after_id_ref[0] = None
- date_str = date_var.get().strip()
- try:
- plan_start = date.fromisoformat(date_str)
- except ValueError:
- messagebox.showerror("日期錯誤", f"請使用 yyyy-MM-dd 格式。目前:{date_str}")
- return
- if from_user_date_change:
- selected_row_holder[0] = None
- selected_jo_id_ref[0] = None
- try:
- data = fetch_job_orders(base_url_ref[0], plan_start)
- except requests.RequestException:
- set_status_error()
- after_id_ref[0] = root.after(RETRY_MS, lambda: load_job_orders(from_user_date_change=False))
- return
- set_status_ok()
- old_data = last_data_ref[0]
- last_data_ref[0] = data
- data_changed = not _data_equal(old_data, data)
- if data_changed or from_user_date_change:
- # Rebuild list: clear and rebuild from current data (last_data_ref already updated)
- for w in inner.winfo_children():
- w.destroy()
- preserve = not from_user_date_change
- _build_list_from_data(data, plan_start, preserve_selection=preserve)
- if from_user_date_change:
- canvas.yview_moveto(0)
- after_id_ref[0] = root.after(REFRESH_MS, lambda: load_job_orders(from_user_date_change=False))
-
- # Load default (today) on start; then start printer connection check
- root.after(100, lambda: load_job_orders(from_user_date_change=True))
- root.after(300, check_printer)
-
- root.mainloop()
-
-
- if __name__ == "__main__":
- main()
|