#!/usr/bin/env python3 """Generate Liquibase seed SQL from 工埸產品保質期 Excel (joExpiry.xlsx). Stores both chilled defaultDays and minus18Days. useMinus18 = 1 only when the row has no non-18 shelf life (frozen-only). Flip useMinus18 in DB later for items that should print -18 days. Example: py scripts/generate_item_default_shelf_life_liquibase.py py scripts/generate_item_default_shelf_life_liquibase.py "C:\\Users\\Administrator\\Downloads\\joExpiry.xlsx" """ from __future__ import annotations import argparse import datetime as dt from pathlib import Path import openpyxl PREFERRED_SHEET = "XXXXXXXX" OUT_SQL = Path( "src/main/resources/db/changelog/changes/20260820_item_default_shelf_life/" "05_seed_item_default_shelf_life_minus18_flag.sql" ) def sql_str(value: str | None) -> str: if value is None: return "NULL" return "'" + value.replace("\\", "\\\\").replace("'", "''") + "'" def sql_int(value: int | None) -> str: return "NULL" if value is None else str(value) def norm_temp(raw) -> str | None: if raw is None: return None s = str(raw).strip().replace("℃", "").replace("°C", "").replace(" ", "") if not s or s.lower() in ("n/a", "none", "null", "-", "nan"): return None return s def is_minus18(temp: str | None) -> bool: return temp in ("-18", "-18.0") def to_days(raw) -> int | None: if raw is None or raw == "": return None if isinstance(raw, str) and raw.strip().lower() in ("n/a", "-", "none", "null"): return None try: n = int(round(float(raw))) except (TypeError, ValueError): return None return n if n > 0 else None def pick_days(t1, d1, t2, d2) -> tuple[int | None, str | None, int | None]: """Return (chilledDays, chilledTemp, minus18Days).""" chilled_days = None chilled_temp = None minus18_days = None for temp, days in ((norm_temp(t1), to_days(d1)), (norm_temp(t2), to_days(d2))): if not temp or not days: continue if is_minus18(temp): if minus18_days is None: minus18_days = days elif chilled_days is None: chilled_days = days chilled_temp = temp return chilled_days, chilled_temp, minus18_days def load_rows(xlsx: Path) -> tuple[str, list[dict]]: wb = openpyxl.load_workbook(xlsx, data_only=True) sheet_name = PREFERRED_SHEET if PREFERRED_SHEET in wb.sheetnames else wb.sheetnames[0] ws = wb[sheet_name] kept: list[dict] = [] seen: set[str] = set() for row in ws.iter_rows(min_row=4, values_only=True): code_raw = row[3] if not code_raw: continue code = str(code_raw).strip().upper() if not code or code in seen: continue chilled, storage, minus18 = pick_days(row[5], row[6], row[7], row[8]) if chilled is None and minus18 is None: continue seen.add(code) use_minus18 = 1 if chilled is None else 0 name = str(row[4]).strip() if row[4] else "" cat = str(row[2]).strip() if row[2] else "" remark = f"{cat} {name}".strip()[:255] kept.append( { "itemCode": code, "defaultDays": chilled, "minus18Days": minus18, "useMinus18": use_minus18, "storageC": "-18" if use_minus18 else storage, "remarks": remark, } ) return sheet_name, kept def render_sql(sheet_name: str, kept: list[dict], source_name: str) -> str: today = dt.date.today().isoformat() n_flag = sum(1 for r in kept if r["useMinus18"] == 1) lines = [ "--liquibase formatted sql", "", "--changeset fpsms:seed_item_default_shelf_life_minus18_flag", f"--comment: Upsert joExpiry.xlsx sheet {sheet_name}: defaultDays + minus18Days + useMinus18 flag", f"-- generated {today}; rows={len(kept)} useMinus18=1 (frozen-only default)={n_flag}", "INSERT INTO `item_default_shelf_life`", "(`created`, `createdBy`, `version`, `modified`, `modifiedBy`, `deleted`,", " `itemCode`, `defaultDays`, `minus18Days`, `useMinus18`, `openedDays`, `storageC`, `remarks`)", "VALUES", ] value_rows = [] for r in kept: value_rows.append( "(" + ", ".join( [ "NOW()", "'system'", "0", "NOW()", "'system'", "0", sql_str(r["itemCode"]), sql_int(r["defaultDays"]), sql_int(r["minus18Days"]), str(r["useMinus18"]), "NULL", sql_str(r["storageC"]), sql_str(r["remarks"]), ] ) + ")" ) lines.append(",\n".join(value_rows)) lines.append("ON DUPLICATE KEY UPDATE") lines.append(" `defaultDays` = VALUES(`defaultDays`),") lines.append(" `minus18Days` = VALUES(`minus18Days`),") lines.append(" `useMinus18` = VALUES(`useMinus18`),") lines.append(" `openedDays` = VALUES(`openedDays`),") lines.append(" `storageC` = VALUES(`storageC`),") lines.append(" `remarks` = VALUES(`remarks`),") lines.append(" `modified` = NOW(),") lines.append(" `modifiedBy` = 'system',") lines.append(" `deleted` = 0;") lines.append("") return "\n".join(lines) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( "xlsx", nargs="?", default=str(Path.home() / "Downloads" / "joExpiry.xlsx"), ) args = parser.parse_args() xlsx = Path(args.xlsx) if not xlsx.is_file(): raise SystemExit(f"Excel not found: {xlsx}") sheet_name, kept = load_rows(xlsx) sql = render_sql(sheet_name, kept, xlsx.name) OUT_SQL.parent.mkdir(parents=True, exist_ok=True) OUT_SQL.write_text(sql, encoding="utf-8") n_flag = sum(1 for r in kept if r["useMinus18"] == 1) print(f"sheet={sheet_name} rows={len(kept)} useMinus18=1={n_flag} useMinus18=0={len(kept) - n_flag}") print(f"wrote {OUT_SQL}") if __name__ == "__main__": main()