| @@ -1,418 +0,0 @@ | |||||
| #!/usr/bin/env python3 | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| 31/8 night stock-ledger-fix timeline (InventoryId match vs not match). | |||||
| Usage (from repo root): | |||||
| python scripts/export_stock_ledger_fix_timeline.py | |||||
| Output: | |||||
| docs/deploy/20260831_stock_ledger_fix_timeline.xlsx | |||||
| """ | |||||
| from __future__ import annotations | |||||
| from pathlib import Path | |||||
| from openpyxl import Workbook | |||||
| from openpyxl.styles import Alignment, Border, Font, PatternFill, Side | |||||
| from openpyxl.utils import get_column_letter | |||||
| from openpyxl.worksheet.worksheet import Worksheet | |||||
| ROOT = Path(__file__).resolve().parents[1] | |||||
| OUT = ROOT / "docs" / "deploy" / "20260831_stock_ledger_fix_timeline.xlsx" | |||||
| THIN = Border( | |||||
| left=Side(style="thin", color="B0B0B0"), | |||||
| right=Side(style="thin", color="B0B0B0"), | |||||
| top=Side(style="thin", color="B0B0B0"), | |||||
| bottom=Side(style="thin", color="B0B0B0"), | |||||
| ) | |||||
| WRAP = Alignment(wrap_text=True, vertical="center") | |||||
| WRAP_CENTER = Alignment(wrap_text=True, vertical="center", horizontal="center") | |||||
| FILL_TITLE = PatternFill("solid", fgColor="1F4E79") | |||||
| FILL_MATCH = PatternFill("solid", fgColor="548235") | |||||
| FILL_NOMATCH = PatternFill("solid", fgColor="C45911") | |||||
| FILL_HEAD = PatternFill("solid", fgColor="D9E2F3") | |||||
| FILL_HEAD_M = PatternFill("solid", fgColor="C6E0B4") | |||||
| FILL_HEAD_N = PatternFill("solid", fgColor="F8CBAD") | |||||
| FILL_GATE = PatternFill("solid", fgColor="FFF2CC") | |||||
| FILL_WAIT = PatternFill("solid", fgColor="DDEBF7") | |||||
| FILL_STOP = PatternFill("solid", fgColor="F4B183") | |||||
| FILL_NOTE = PatternFill("solid", fgColor="F2F2F2") | |||||
| WHITE = Font(bold=True, color="FFFFFF", name="Calibri", size=14) | |||||
| WHITE11 = Font(bold=True, color="FFFFFF", name="Calibri", size=11) | |||||
| BOLD = Font(bold=True, name="Calibri", size=11) | |||||
| NORMAL = Font(name="Calibri", size=10) | |||||
| ITALIC = Font(italic=True, color="666666", name="Calibri", size=9) | |||||
| def set_col_widths(ws: Worksheet, widths: dict[str, float]) -> None: | |||||
| for col, w in widths.items(): | |||||
| ws.column_dimensions[col].width = w | |||||
| def paint(cell, value, fill=None, font=None, align=WRAP, merge=None) -> None: | |||||
| cell.value = value | |||||
| cell.alignment = align | |||||
| cell.border = THIN | |||||
| if fill: | |||||
| cell.fill = fill | |||||
| if font: | |||||
| cell.font = font | |||||
| def write_table(ws: Worksheet, start_row: int, start_col: int, headers: list[str], rows: list[list[str]], header_fill: PatternFill) -> int: | |||||
| for i, h in enumerate(headers): | |||||
| c = ws.cell(start_row, start_col + i) | |||||
| paint(c, h, header_fill, BOLD, WRAP_CENTER) | |||||
| r = start_row + 1 | |||||
| for row in rows: | |||||
| row_fill = None | |||||
| row_font = NORMAL | |||||
| cells: list[str] = [] | |||||
| for val in row: | |||||
| if val.startswith("GATE:"): | |||||
| row_fill, row_font = FILL_GATE, BOLD | |||||
| cells.append(val[5:].strip()) | |||||
| elif val.startswith("WAIT:"): | |||||
| row_fill = FILL_WAIT | |||||
| cells.append(val[5:].strip()) | |||||
| elif val.startswith("STOP:"): | |||||
| row_fill, row_font = FILL_STOP, BOLD | |||||
| cells.append(val[5:].strip()) | |||||
| else: | |||||
| cells.append(val) | |||||
| for i, text in enumerate(cells): | |||||
| c = ws.cell(r, start_col + i) | |||||
| font = row_font if i == 0 or row_fill in (FILL_GATE, FILL_STOP) else NORMAL | |||||
| paint(c, text, row_fill, font) | |||||
| if i == 0: | |||||
| c.alignment = WRAP_CENTER | |||||
| r += 1 | |||||
| return r - 1 | |||||
| def sheet_main(wb: Workbook) -> None: | |||||
| ws = wb.active | |||||
| ws.title = "31-8夜_主流程" | |||||
| ws.sheet_view.showGridLines = False | |||||
| ws.freeze_panes = "A8" | |||||
| ws.row_dimensions[1].height = 24 | |||||
| ws.page_setup.orientation = "landscape" | |||||
| ws.page_setup.fitToPage = True | |||||
| ws.page_setup.fitToWidth = 1 | |||||
| ws.page_setup.fitToHeight = 1 | |||||
| ws.page_setup.paperSize = ws.PAPERSIZE_A4 | |||||
| ws.print_title_rows = "1:7" | |||||
| ws.page_setup.horizontalCentered = True | |||||
| ws.sheet_properties.pageSetUpPr.fitToPage = True | |||||
| ws.merge_cells("A1:I1") | |||||
| paint(ws["A1"], "31/8 夜 stock-ledger-fix:兩條流程(InventoryId match / not match)", FILL_TITLE, WHITE, WRAP_CENTER) | |||||
| for col in range(2, 10): | |||||
| ws.cell(1, col).fill = FILL_TITLE | |||||
| ws.cell(1, col).border = THIN | |||||
| ws.merge_cells("A2:I2") | |||||
| paint( | |||||
| ws["A2"], | |||||
| "前提:白天鏈已備好 0315–0830 SQL;dump 31/8 22:30 起 freeze。" | |||||
| " 程式已支援修/匯出「今天」與 2.7 adjDate=今天(UI 選今天或 API 傳 adjDate)。" | |||||
| " 23:05 Preview 決定走左欄或右欄,不要兩條一起做。" | |||||
| " 時間由預計分鐘累加(樂觀);逾時整串延後。", | |||||
| FILL_NOTE, | |||||
| ITALIC, | |||||
| ) | |||||
| ws.row_dimensions[2].height = 48 | |||||
| for col in range(2, 10): | |||||
| ws.cell(2, col).fill = FILL_NOTE | |||||
| ws.cell(2, col).border = THIN | |||||
| ws.merge_cells("A3:D3") | |||||
| paint( | |||||
| ws["A3"], | |||||
| "流程 A MATCH(31/8 dump 套檔前 inventory 頂 = 30/8 修帳前的頂)", | |||||
| FILL_MATCH, | |||||
| WHITE11, | |||||
| WRAP_CENTER, | |||||
| ) | |||||
| for col in range(2, 5): | |||||
| ws.cell(3, col).fill = FILL_MATCH | |||||
| ws.cell(3, col).border = THIN | |||||
| ws.merge_cells("F3:I3") | |||||
| paint( | |||||
| ws["F3"], | |||||
| "流程 B NOT MATCH(31/8 dump 套檔前 inventory 頂 ≠ 30/8 修帳前)", | |||||
| FILL_NOMATCH, | |||||
| WHITE11, | |||||
| WRAP_CENTER, | |||||
| ) | |||||
| for col in range(7, 10): | |||||
| ws.cell(3, col).fill = FILL_NOMATCH | |||||
| ws.cell(3, col).border = THIN | |||||
| ws.merge_cells("A4:D4") | |||||
| paint( | |||||
| ws["A4"], | |||||
| "套 0315–0830 完整 SQL(含 inventory + inventoryId)。不要長 2.3。約 23:30 套完正式庫。", | |||||
| FILL_HEAD_M, | |||||
| NORMAL, | |||||
| ) | |||||
| ws.row_dimensions[4].height = 36 | |||||
| for col in range(2, 5): | |||||
| ws.cell(4, col).fill = FILL_HEAD_M | |||||
| ws.cell(4, col).border = THIN | |||||
| ws.merge_cells("F4:I4") | |||||
| paint( | |||||
| ws["F4"], | |||||
| "套 0315–0830 不完整 SQL(無 inventory/inventoryId)→ 本 dump 1.0 → 長 2.3。" | |||||
| " 禁止套舊 1.0 INSERT。約 23:53 套完正式庫。", | |||||
| FILL_HEAD_N, | |||||
| NORMAL, | |||||
| ) | |||||
| for col in range(7, 10): | |||||
| ws.cell(4, col).fill = FILL_HEAD_N | |||||
| ws.cell(4, col).border = THIN | |||||
| match_rows = [ | |||||
| ["22:30", "1", "dump + freeze", "停 DN/GRN、lot expiry、日結、GRN sync。記下 I0=MAX(inventory.id)、N=MAX(stock_ledger.id)"], | |||||
| ["22:31", "5", "zip/下載完", "逾 15 分當異常"], | |||||
| ["22:36", "25", "import 完", "最易爆;爆了後面全停"], | |||||
| ["GATE:23:05", "—", "Preview:InventoryId MATCH", "I0 = 30/8 記住的頂 → 走本欄。新 UOM 仍可 MATCH"], | |||||
| ["23:05", "10", "套 0315–0830 完整 SQL", "inventory(寫死 id)→ ledger(含 inventoryId)→ 日結。舊 qty 非最終"], | |||||
| ["23:15", "<10s", "再跑 1.0", "新 UOM 才 INSERT(例 1051);qty 刷成 31/8 dump 當下"], | |||||
| ["23:15", "2", "短區間 2.1–2.6:0830–0831", "可修今天。不要重跑 0315–0829,不要長 2.3"], | |||||
| ["23:17", "3", "2.7 ADJ(adjDate=31/8)", "UI 選「今天」或 API adjDate=今天。勿用預設昨天(會 ADJ 到 30/8)"], | |||||
| ["23:20", "10", "匯出完整檔 0315–0831", "inventory → ledger.inventoryId → 日結;2.7 新列(id>N)要 INSERT"], | |||||
| ["WAIT:23:25", "5", "正式庫 pull code + 開後端", "freeze 仍在;I0/N 未變"], | |||||
| ["23:30", "5", "正式庫套 0315–0831 完整 SQL", "先 inventory、再 ledger、再日結。套完再開 expiry/日結"], | |||||
| ] | |||||
| nomatch_rows = [ | |||||
| ["22:30", "1", "dump + freeze", "同左。記下實際 I0(已不是舊頂)、N"], | |||||
| ["22:31", "5", "zip/下載完", "同左"], | |||||
| ["22:36", "25", "import 完", "同左"], | |||||
| ["GATE:23:05", "—", "Preview:NOT MATCH", "禁止套 0830 inventory/inventoryId"], | |||||
| ["23:05", "10", "套 0315–0830 不完整 SQL", "只套 ledger 計算欄+日結(若有)。不要套舊 1.0 INSERT"], | |||||
| ["23:15", "1", "再跑 1.0", "本 dump 新 id 從 I0+1 起;與 0830 檔不是同一套"], | |||||
| ["23:16", "15", "長區間 2.3:0315–0830", "本夜最長段。逾 23:50 則本夜不套正式庫"], | |||||
| ["23:31", "2", "短區間 2.1–2.6:0830–0831", "同左:可修今天"], | |||||
| ["23:33", "3", "2.7 ADJ(adjDate=31/8)", "同左:選今天"], | |||||
| ["23:43", "10", "匯出完整檔 0315–0831", "id 同源於本 dump 1.0,不是 0830 檔"], | |||||
| ["WAIT:23:48", "5", "正式庫 pull code + 開後端", "1:00 master 維持停到套完"], | |||||
| ["23:53", "5", "正式庫套完整 SQL", "先 inventory、再 ledger、再日結"], | |||||
| ["STOP:(風險)", "—", "長 2.3 拖過", "優先本夜 freeze 修完再上,或取消套檔改白天"], | |||||
| ] | |||||
| write_table(ws, 6, 1, ["時間", "預計(分)", "動作", "備註"], match_rows, FILL_HEAD_M) | |||||
| write_table(ws, 6, 6, ["時間", "預計(分)", "動作", "備註"], nomatch_rows, FILL_HEAD_N) | |||||
| ws.column_dimensions["E"].width = 2.5 | |||||
| note_row = 21 | |||||
| ws.merge_cells(start_row=note_row, start_column=1, end_row=note_row, end_column=9) | |||||
| paint( | |||||
| ws.cell(note_row, 1), | |||||
| "顏色:黃=Preview;藍=等待;橙=風險。" | |||||
| " MATCH 不要跑長 2.3。" | |||||
| " NOT MATCH 不要套 0830 inventory/inventoryId。" | |||||
| " 2.7 必須 adjDate=dump 日(今天)。" | |||||
| " writeExportSql 完整檔(含 inventory/inventoryId/2.7 INSERT)若尚未改完,匯出前先確認。", | |||||
| FILL_NOTE, | |||||
| ITALIC, | |||||
| ) | |||||
| ws.row_dimensions[note_row].height = 48 | |||||
| for col in range(2, 10): | |||||
| ws.cell(note_row, col).fill = FILL_NOTE | |||||
| ws.cell(note_row, col).border = THIN | |||||
| set_col_widths( | |||||
| ws, | |||||
| { | |||||
| "A": 12, | |||||
| "B": 10, | |||||
| "C": 28, | |||||
| "D": 38, | |||||
| "E": 2.5, | |||||
| "F": 12, | |||||
| "G": 10, | |||||
| "H": 30, | |||||
| "I": 38, | |||||
| }, | |||||
| ) | |||||
| for r in range(7, 20): | |||||
| ws.row_dimensions[r].height = 44 | |||||
| ws.row_dimensions[6].height = 22 | |||||
| ws.print_area = "A1:I21" | |||||
| def sheet_gate(wb: Workbook) -> None: | |||||
| ws = wb.create_sheet("InventoryId_match判定") | |||||
| ws.sheet_view.showGridLines = False | |||||
| ws.page_setup.orientation = "landscape" | |||||
| ws.page_setup.fitToPage = True | |||||
| ws.page_setup.fitToWidth = 1 | |||||
| ws.page_setup.fitToHeight = 1 | |||||
| ws.page_setup.paperSize = ws.PAPERSIZE_A4 | |||||
| ws.merge_cells("A1:E1") | |||||
| paint(ws["A1"], "InventoryId match 判定(數字過了才往下,不要用「時間靠近」)", FILL_TITLE, WHITE, WRAP_CENTER) | |||||
| for col in range(2, 6): | |||||
| ws.cell(1, col).fill = FILL_TITLE | |||||
| ws.cell(1, col).border = THIN | |||||
| ws.merge_cells("A2:E2") | |||||
| paint( | |||||
| ws["A2"], | |||||
| "A 在套任何 0830 SQL 之前(dump 原樣)。B 在套完 0830 SQL+再跑 1.0 之後(僅 MATCH)。" | |||||
| " C 在同夜修完 0830–0831+2.7(adjDate=今天)之後,才能完整匯出。", | |||||
| FILL_NOTE, | |||||
| ITALIC, | |||||
| ) | |||||
| ws.row_dimensions[2].height = 36 | |||||
| for col in range(2, 6): | |||||
| ws.cell(2, col).fill = FILL_NOTE | |||||
| ws.cell(2, col).border = THIN | |||||
| a_rows = [ | |||||
| ["A1", "inventory 頂 I0", "套檔前 MAX(inventory.id)", "= 0830 修帳前記住的頂(例 1000)", "≠ → 流程 B(NOT MATCH)。禁止套舊 1.0 INSERT"], | |||||
| ["A2", "新 UOM pair", "lot 有 (itemId,uomId) 但 inventory 還沒列", "0 或 >0 都可以", ">0 不是失敗:MATCH 時套完舊檔再 1.0;NOT MATCH 時本 dump 1.0 一併補"], | |||||
| ["A3", "ledger 頂 N", "套檔前 MAX(stock_ledger.id)", "記下,給 2.7 INSERT 用", "—"], | |||||
| ["A4", "0830 SQL inventory 段", "要不要套", "僅 A1 通過才套", "A1 失敗:inventory / inventoryId 都不要套"], | |||||
| ] | |||||
| write_table(ws, 4, 1, ["#", "檢查", "怎麼算", "MATCH 通過", "NOT MATCH/失敗"], a_rows, FILL_HEAD) | |||||
| ws.merge_cells("A10:E10") | |||||
| paint(ws["A10"], "B 僅流程 A(MATCH)套完 0830 SQL、再跑 1.0 之後", FILL_MATCH, WHITE11, WRAP_CENTER) | |||||
| for col in range(2, 6): | |||||
| ws.cell(10, col).fill = FILL_MATCH | |||||
| ws.cell(10, col).border = THIN | |||||
| b_rows = [ | |||||
| ["B1", "needInventoryInsert", "1.0 後還缺的 (item,uom)", "= 0", ">0 → 1.0 沒補完,不要匯出"], | |||||
| ["B2", "inventory 頂", "1.0 後 MAX(id)", "≥ I0;有新 UOM 會 > 舊頂(例 1051)", "仍 = dump 的 I0 且 A2>0 → 1.0 沒跑或沒 INSERT"], | |||||
| ["B3", "0830 區間 inventoryId", "miss / wrong vs MIN(inventory.id) per (item,uom)", "= 0", "≠0 → 再跑 0830 的 2.3(短區間)"], | |||||
| ] | |||||
| write_table(ws, 11, 1, ["#", "檢查", "怎麼算", "通過", "失敗"], b_rows, FILL_HEAD_M) | |||||
| ws.merge_cells("A16:E16") | |||||
| paint(ws["A16"], "C 兩條流程都要:同夜修完 0831+2.7(adjDate=今天)之後(完整匯出前)", FILL_TITLE, WHITE11, WRAP_CENTER) | |||||
| for col in range(2, 6): | |||||
| ws.cell(16, col).fill = FILL_TITLE | |||||
| ws.cell(16, col).border = THIN | |||||
| c_rows = [ | |||||
| ["C1", "0831 inventoryId", "miss / wrong", "= 0", "再跑 0831 的 2.3"], | |||||
| ["C2", "2.7 新列", "id > N 且 type=ADJ、createdBy=stock-ledger-fix", "匯出必須 INSERT 這些 id", "現況 writeExportSql 只 UPDATE JOIN id → 正式庫會丟 ADJ"], | |||||
| ["C3", "正式庫套檔前", "freeze 仍在", "MATCH:I0 仍為記住的頂、ledger 頂仍 N。NOT MATCH:I0 仍為本 dump 的頂", "任一變了 → 不要套完整檔"], | |||||
| ] | |||||
| write_table(ws, 17, 1, ["#", "檢查", "怎麼算", "通過", "失敗"], c_rows, FILL_HEAD) | |||||
| ws.merge_cells("A22:E22") | |||||
| paint(ws["A22"], "分支速查(給 23:05)", FILL_GATE, BOLD, WRAP_CENTER) | |||||
| for col in range(2, 6): | |||||
| ws.cell(22, col).fill = FILL_GATE | |||||
| ws.cell(22, col).border = THIN | |||||
| branch = [ | |||||
| ["MATCH", "無新 UOM", "流程 A", "套完整 SQL → 1.0 刷 qty → 修 0830–0831 → 2.7 adjDate=今天 → 匯出 → 套正式庫", "正式庫不必再跑 1.0/2.3"], | |||||
| ["MATCH", "有新 UOM", "流程 A(同一條)", "套完整 SQL → 1.0 INSERT 新 id → 修 0830–0831 → 2.7 → 完整檔必含 inventory", "不要因 max 仍舊頂就跳過第二次 1.0"], | |||||
| ["NOT MATCH", "—", "流程 B", "不完整 SQL → 本 dump 1.0 → 長 2.3 → 修 0830–0831 → 2.7 adjDate=今天", "2.3 逾時則本夜不套正式庫"], | |||||
| ] | |||||
| write_table(ws, 23, 1, ["A1", "A2 新 UOM", "走哪條", "本夜做什麼", "結果"], branch, FILL_HEAD) | |||||
| set_col_widths(ws, {"A": 14, "B": 22, "C": 36, "D": 42, "E": 48}) | |||||
| for r in list(range(5, 9)) + list(range(12, 15)) + list(range(18, 21)) + list(range(24, 27)): | |||||
| ws.row_dimensions[r].height = 42 | |||||
| ws.row_dimensions[1].height = 24 | |||||
| ws.print_area = "A1:E26" | |||||
| def sheet_notes(wb: Workbook) -> None: | |||||
| ws = wb.create_sheet("注意與對照") | |||||
| ws.sheet_view.showGridLines = False | |||||
| ws.page_setup.orientation = "landscape" | |||||
| ws.page_setup.fitToPage = True | |||||
| ws.page_setup.fitToWidth = 1 | |||||
| ws.page_setup.fitToHeight = 1 | |||||
| ws.page_setup.paperSize = ws.PAPERSIZE_A4 | |||||
| ws.merge_cells("A1:C1") | |||||
| paint(ws["A1"], "和舊圖的對照、套檔順序、程式缺口", FILL_TITLE, WHITE, WRAP_CENTER) | |||||
| ws["B1"].fill = FILL_TITLE | |||||
| ws["C1"].fill = FILL_TITLE | |||||
| ws["B1"].border = THIN | |||||
| ws["C1"].border = THIN | |||||
| vs_rows = [ | |||||
| ["舊圖等 0:00 才能修 0831/2.7", "已改:可修今天;2.7 傳 adjDate=今天", "同左"], | |||||
| ["舊圖 23:11–23:40 1.0+長 2.3", "MATCH:刪長 2.3;套檔後 1.0 很快", "NOT MATCH:長 2.3 約 15 分(23:16)"], | |||||
| ["舊圖 0:40 才套正式庫", "MATCH:約 23:30", "NOT MATCH:約 23:53"], | |||||
| ] | |||||
| write_table(ws, 3, 1, ["舊圖", "流程 A MATCH", "流程 B NOT MATCH"], vs_rows, FILL_HEAD) | |||||
| ws.merge_cells("A9:C9") | |||||
| paint(ws["A9"], "套檔順序(完整檔,兩條流程相同)", FILL_MATCH, WHITE11, WRAP_CENTER) | |||||
| ws["B9"].fill = FILL_MATCH | |||||
| ws["C9"].fill = FILL_MATCH | |||||
| ws["B9"].border = THIN | |||||
| ws["C9"].border = THIN | |||||
| order_rows = [ | |||||
| ["1", "inventory", "INSERT 寫死新 id + UPDATE 既有列 qty/status(MATCH 的 1001+ 來自 0830 檔;NOT MATCH 來自本 dump 1.0)"], | |||||
| ["2", "stock_ledger", "id ≤ N → UPDATE(含 inventoryId、lotQty、balance…);id > N(2.7 ADJ)→ INSERT 寫死 id"], | |||||
| ["3", "stock_lot_day", "刪區間再 INSERT/ON DUPLICATE。在 ledger 之後"], | |||||
| ["—", "正式庫不要再跑", "Preview C 通過則不必 1.0、不必 2.3。套完再開 expiry/日結"], | |||||
| ] | |||||
| write_table(ws, 10, 1, ["#", "對象", "做法"], order_rows, FILL_HEAD_M) | |||||
| ws.merge_cells("A16:C16") | |||||
| paint(ws["A16"], "現有程式缺口(上線前)", FILL_NOMATCH, WHITE11, WRAP_CENTER) | |||||
| ws["B16"].fill = FILL_NOMATCH | |||||
| ws["C16"].fill = FILL_NOMATCH | |||||
| ws["B16"].border = THIN | |||||
| ws["C16"].border = THIN | |||||
| gap_rows = [ | |||||
| ["fixDay / fixRange / upsertDayForFix", "已允許修/日結「今天」(拒未來)", "freeze 夜可修 0831"], | |||||
| ["2.7 ADJ", "可傳 adjDate(預設昨天;freeze 夜傳今天)", "UI 選「今天」或 API ?adjDate= / body.adjDate"], | |||||
| ["writeExportSql", "可匯出到今天;仍可能缺 inventory/inventoryId/2.7 INSERT", "完整檔選項若未做完需手動補"], | |||||
| ["排程 closeDay", "仍拒今天", "正式庫開站後日結仍只關昨天"], | |||||
| ] | |||||
| write_table(ws, 17, 1, ["位置", "現況", "要補"], gap_rows, FILL_HEAD_N) | |||||
| ws.merge_cells("A24:C24") | |||||
| paint( | |||||
| ws["A24"], | |||||
| "記住:inventory 頂沒變 ≠ 1.0 做完。新 UOM lot line 時常仍是 max=1000,但一定要再跑 1.0。" | |||||
| " MATCH 時順序必須是:先套 0830 SQL,再 1.0(否則新 pair 會把 1001+ 編錯)。", | |||||
| FILL_NOTE, | |||||
| ITALIC, | |||||
| ) | |||||
| ws.row_dimensions[24].height = 42 | |||||
| ws["B24"].fill = FILL_NOTE | |||||
| ws["C24"].fill = FILL_NOTE | |||||
| ws["B24"].border = THIN | |||||
| ws["C24"].border = THIN | |||||
| set_col_widths(ws, {"A": 36, "B": 48, "C": 56}) | |||||
| for r in list(range(4, 8)) + list(range(11, 15)) + list(range(18, 23)): | |||||
| ws.row_dimensions[r].height = 36 | |||||
| ws.row_dimensions[1].height = 24 | |||||
| ws.print_area = "A1:C24" | |||||
| def main() -> None: | |||||
| OUT.parent.mkdir(parents=True, exist_ok=True) | |||||
| wb = Workbook() | |||||
| sheet_main(wb) | |||||
| sheet_gate(wb) | |||||
| sheet_notes(wb) | |||||
| wb.save(OUT) | |||||
| print(f"Wrote {OUT}") | |||||
| if __name__ == "__main__": | |||||
| main() | |||||