Ver a proveniência

no message

fix負數倉
Fai Luk há 4 semanas
ascendente
cometimento
1b9a7219f8
1 ficheiros alterados com 68 adições e 20 eliminações
  1. +68
    -20
      python/Bag4.py

+ 68
- 20
python/Bag4.py Ver ficheiro

@@ -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(


Carregando…
Cancelar
Guardar