From 1b9a7219f8af6ae1c3bc6775843ef0167d910c1c Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Sun, 23 Aug 2026 15:00:17 +0800 Subject: [PATCH] no message --- python/Bag4.py | 88 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/python/Bag4.py b/python/Bag4.py index 388d64d..8ad51cc 100644 --- a/python/Bag4.py +++ b/python/Bag4.py @@ -4,7 +4,7 @@ Bag4 v4.0 – FPSMS job orders by plan date, with DataFlex expiry under Lot. Uses the public API GET /py/job-orders and POST /py/job-order-print-submit (no login required). Same as Bag3, plus expiry (today + defaultShelfLifeDays) on DataFlex ZPL -and as the 4th laser TCP param (EZCAD job must bind that field). +and as the 4th laser TCP param, always printed as `Expiry Date 20260826`. Bag3 remains the line without expiry on DataFlex; do not share settings files. @@ -430,33 +430,63 @@ def _dataflex_shutdown_write_maybe(sock: socket.socket) -> None: raise +EXPIRY_PRINT_PREFIX = "Expiry Date " +EXPIRY_LABEL_PREFIX = "Expiry " + + def format_expiry_print_label(d: date) -> str: - """Printed bag wording, e.g. Expiry Date 20260821.""" - return f"Expiry Date {d.strftime('%Y%m%d')}" + """DataFlex / laser wording: `Expiry Date 20260826`.""" + return f"{EXPIRY_PRINT_PREFIX}{d.strftime('%Y%m%d')}" + + +def format_expiry_label_compact(d: date) -> str: + """標簽機 wording (narrow): `Expiry 20260826`.""" + return f"{EXPIRY_LABEL_PREFIX}{d.strftime('%Y%m%d')}" def _parse_api_expiry_date(raw) -> Optional[date]: - """ISO `yyyy-MM-dd` or Jackson date-array `[yyyy,M,d]` from @EnableWebMvc.""" + """Accept ISO `yyyy-MM-dd`, compact `yyyyMMdd`, `Expiry Date yyyyMMdd`, or `[yyyy,M,d]`.""" if raw is None or raw == "": return None + if isinstance(raw, datetime): + return raw.date() + if isinstance(raw, date): + return raw if isinstance(raw, (list, tuple)) and len(raw) >= 3: try: return date(int(raw[0]), int(raw[1]), int(raw[2])) except (TypeError, ValueError): return None - s = str(raw).strip() - if s.startswith("[") and "," in s: - return None - iso = s[:10] - try: - y, m, day = (int(p) for p in iso.split("-")) - return date(y, m, day) - except ValueError: + s = str(raw).strip().replace(";", ",") + if not s: return None + if s.lower().startswith(EXPIRY_PRINT_PREFIX.lower()): + s = s[len(EXPIRY_PRINT_PREFIX):].strip() + elif s.lower().startswith(EXPIRY_LABEL_PREFIX.lower()): + s = s[len(EXPIRY_LABEL_PREFIX):].strip() + digits = "".join(ch for ch in s if ch.isdigit()) + if len(digits) >= 8: + try: + return date(int(digits[0:4]), int(digits[4:6]), int(digits[6:8])) + except ValueError: + return None + return None + + +def standardize_expiry_print_label(raw) -> str: + """Normalize any expiry input to `Expiry Date 20260826`, or empty if unknown.""" + parsed = _parse_api_expiry_date(raw) + return format_expiry_print_label(parsed) if parsed else "" + + +def standardize_expiry_label_compact(raw) -> str: + """Normalize any expiry input to `Expiry 20260826` for 標簽機, or empty if unknown.""" + parsed = _parse_api_expiry_date(raw) + return format_expiry_label_compact(parsed) if parsed else "" def job_expiry_zpl_text(jo: dict) -> Optional[str]: - """Expiry yyyyMMdd from print-time shelf life days, else API expiryDate.""" + """Expiry as `Expiry Date yyyyMMdd` from shelf-life days, else API expiryDate.""" days = jo.get("defaultShelfLifeDays") if isinstance(days, bool): days = None @@ -464,12 +494,12 @@ def job_expiry_zpl_text(jo: dict) -> Optional[str]: days = int(days) if isinstance(days, int) and days > 0: return format_expiry_print_label(date.today() + timedelta(days=days)) - parsed = _parse_api_expiry_date(jo.get("expiryDate")) - return format_expiry_print_label(parsed) if parsed else None + label = standardize_expiry_print_label(jo.get("expiryDate")) + return label or None def job_expiry_laser_param(jo: dict) -> str: - """Same expiry as DataFlex ZPL, sanitized for `;`-separated laser TCP.""" + """Same `Expiry Date yyyyMMdd` as DataFlex, sanitized for `;`-separated laser TCP.""" return (job_expiry_zpl_text(jo) or "").replace(";", ",") @@ -496,7 +526,7 @@ def generate_zpl_dataflex( code = _zpl_escape((item_code or "—").strip()) label_line = (lot_no or batch_no or "").strip() label_esc = _zpl_escape(label_line) - exp_raw = (expiry_text or "").strip() + exp_raw = standardize_expiry_print_label(expiry_text) exp_esc = _zpl_escape(exp_raw) if exp_raw else "" # QR payload: prefer JSON {"itemId":..., "stockInLineId":...} when both present; else fall back to lot/batch text if item_id is not None and stock_in_line_id is not None: @@ -651,10 +681,12 @@ def generate_zpl_label_small( item_id: Optional[int] = None, stock_in_line_id: Optional[int] = None, lot_no: Optional[str] = None, + expiry_text: Optional[str] = None, font: str = "MingLiUHKSCS", ) -> 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 + expiry right. + Expiry is always `Expiry yyyyMMdd` when present (narrow label). QR contains {"itemId": xxx, "stockInLineId": xxx} when both present; else batch_no. Unicode (^CI28); font set for Big-5 (e.g. MingLiUHKSCS). """ @@ -662,10 +694,14 @@ def generate_zpl_label_small( code = _zpl_escape((item_code or "—").strip()) label_line2 = (lot_no or batch_no or "—").strip() label_line2_esc = _zpl_escape(label_line2) + exp_esc = _zpl_escape(standardize_expiry_label_compact(expiry_text)) if item_id is not None and stock_in_line_id is not None: qr_data = _zpl_escape(json.dumps({"itemId": item_id, "stockInLineId": stock_in_line_id})) else: qr_data = f"QA,{batch_no}" + exp_zpl = f""" +^FO150,230 +^A@N,36,36,{font}^FD{exp_esc}^FS""" if exp_esc else "" return f"""^XA ^CI28 ^PW500 @@ -678,7 +714,7 @@ def generate_zpl_label_small( ^FO150,110 ^A@N,48,48,{font}^FD{code}^FS ^FO150,175 -^A@N,40,40,{font}^FD{label_line2_esc}^FS +^A@N,40,40,{font}^FD{label_line2_esc}^FS{exp_zpl} ^XZ""" @@ -690,6 +726,7 @@ LABEL_PADDING = 23 LABEL_FONT_NAME_SIZE = 42 LABEL_FONT_CODE_SIZE = 49 LABEL_FONT_BATCH_SIZE = 34 +LABEL_FONT_EXPIRY_SIZE = 34 LABEL_QR_SIZE = 210 @@ -745,9 +782,11 @@ def render_label_to_image( item_id: Optional[int] = None, stock_in_line_id: Optional[int] = None, lot_no: Optional[str] = None, + expiry_text: Optional[str] = None, ) -> "Image.Image": """ Render 標簽機 label as a PIL Image (white bg, black text + QR). + Lot line is followed by `Expiry yyyyMMdd` when [expiry_text] is set. Use this image for printing so Chinese displays correctly; words are drawn bigger. Requires Pillow and qrcode. Raises RuntimeError if not available. """ @@ -772,6 +811,7 @@ def render_label_to_image( font_name = _get_chinese_font(LABEL_FONT_NAME_SIZE) font_code = _get_chinese_font(LABEL_FONT_CODE_SIZE) font_batch = _get_chinese_font(LABEL_FONT_BATCH_SIZE) + font_expiry = _get_chinese_font(LABEL_FONT_EXPIRY_SIZE) x_right = LABEL_PADDING + LABEL_QR_SIZE + LABEL_PADDING y_line = LABEL_PADDING # Line 1: item name (wrap within remaining width) @@ -852,6 +892,13 @@ def render_label_to_image( draw.text((x_right, y_line), batch_str, font=font_batch, fill="black") else: draw.text((x_right, y_line), batch_str, fill="black") + y_line += LABEL_FONT_BATCH_SIZE + 6 + exp_str = standardize_expiry_label_compact(expiry_text) + if exp_str: + if font_expiry: + draw.text((x_right, y_line), exp_str, font=font_expiry, fill="black") + else: + draw.text((x_right, y_line), exp_str, fill="black") return img @@ -1270,7 +1317,7 @@ def send_job_to_laser( """ code_str = (item_code or "").strip().replace(";", ",") name_str = (item_name or "").strip().replace(";", ",") - exp_str = (expiry_text or "").strip().replace(";", ",") + exp_str = standardize_expiry_print_label(expiry_text).replace(";", ",") if item_id is not None and stock_in_line_id is not None: # Use compact JSON so device-side parser doesn't get spaces. @@ -2905,6 +2952,7 @@ def main() -> None: b, item_code, item_name, item_id=item_id, stock_in_line_id=stock_in_line_id, lot_no=lot_no, + expiry_text=job_expiry_zpl_text(j), ) zpl_img = _image_to_zpl_gfa(label_img) run_label_print_batch_thread(