feat(cupang): 출고리스트 서식 보강 (요일·머리글색·파렛트 강조)
- 시트명 20260903(목) 형식, 작성일/출고일/센터입고일에 요일 표기 - 머리글 행 배경 #DBE9F7 + 굵게 - 출고방식이 파렛트인 센터 블록 행 배경 #FAE2D5 - A열 너비 10 xlsx·구글시트 양쪽에 동일 적용(google_sheets.write_table 에 header_bg / row_highlights 인자 추가). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,16 @@ SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
|||||||
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
||||||
|
|
||||||
|
|
||||||
|
def _hex_color(value: str) -> dict[str, float]:
|
||||||
|
""""RRGGBB" → Sheets API 색상."""
|
||||||
|
v = (value or "").lstrip("#")
|
||||||
|
return {
|
||||||
|
"red": int(v[0:2], 16) / 255.0,
|
||||||
|
"green": int(v[2:4], 16) / 255.0,
|
||||||
|
"blue": int(v[4:6], 16) / 255.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GoogleSheetsWriter:
|
class GoogleSheetsWriter:
|
||||||
"""시트 1장을 통째로 다시 쓰는 용도의 얇은 래퍼."""
|
"""시트 1장을 통째로 다시 쓰는 용도의 얇은 래퍼."""
|
||||||
|
|
||||||
@@ -157,10 +167,13 @@ class GoogleSheetsWriter:
|
|||||||
last_col: int,
|
last_col: int,
|
||||||
title_row: int,
|
title_row: int,
|
||||||
widths: dict[int, int],
|
widths: dict[int, int],
|
||||||
|
header_bg: str = "",
|
||||||
|
row_highlights: list[tuple[int, int, str]] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""시트를 만들고(있으면 비우고) 표를 통째로 기록한다.
|
"""시트를 만들고(있으면 비우고) 표를 통째로 기록한다.
|
||||||
|
|
||||||
좌표는 모두 1-based(엑셀과 동일). 반환값에 시트 URL 을 담는다.
|
좌표는 모두 1-based(엑셀과 동일). 반환값에 시트 URL 을 담는다.
|
||||||
|
header_bg / row_highlights 의 색은 "RRGGBB" 16진 문자열.
|
||||||
"""
|
"""
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
raise RuntimeError(self.reason or "Google Sheets 미설정")
|
raise RuntimeError(self.reason or "Google Sheets 미설정")
|
||||||
@@ -213,12 +226,19 @@ class GoogleSheetsWriter:
|
|||||||
"textFormat": {"bold": True, "fontSize": 16}},
|
"textFormat": {"bold": True, "fontSize": 16}},
|
||||||
"userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)",
|
"userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)",
|
||||||
))
|
))
|
||||||
requests.append(_fmt(
|
header_fmt: dict[str, Any] = {
|
||||||
header_row, first_col, header_row, last_col,
|
"horizontalAlignment": "CENTER", "verticalAlignment": "MIDDLE",
|
||||||
{"horizontalAlignment": "CENTER", "verticalAlignment": "MIDDLE",
|
"textFormat": {"bold": True},
|
||||||
"textFormat": {"bold": True}},
|
}
|
||||||
"userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)",
|
header_fields = "userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)"
|
||||||
))
|
if header_bg:
|
||||||
|
header_fmt["backgroundColor"] = _hex_color(header_bg)
|
||||||
|
header_fields = (
|
||||||
|
"userEnteredFormat(horizontalAlignment,verticalAlignment,"
|
||||||
|
"textFormat,backgroundColor)"
|
||||||
|
)
|
||||||
|
requests.append(_fmt(header_row, first_col, header_row, last_col,
|
||||||
|
header_fmt, header_fields))
|
||||||
if last_row >= first_data_row:
|
if last_row >= first_data_row:
|
||||||
requests.append(_fmt(
|
requests.append(_fmt(
|
||||||
first_data_row, first_col, last_row, last_col,
|
first_data_row, first_col, last_row, last_col,
|
||||||
@@ -245,6 +265,14 @@ class GoogleSheetsWriter:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 특정 행 구간 배경색(예: 파렛트 출고 블록)
|
||||||
|
for (r1, r2, color) in (row_highlights or []):
|
||||||
|
requests.append(_fmt(
|
||||||
|
r1, first_col, r2, last_col,
|
||||||
|
{"backgroundColor": _hex_color(color)},
|
||||||
|
"userEnteredFormat(backgroundColor)",
|
||||||
|
))
|
||||||
|
|
||||||
for col, width in widths.items():
|
for col, width in widths.items():
|
||||||
requests.append({
|
requests.append({
|
||||||
"updateDimensionProperties": {
|
"updateDimensionProperties": {
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ COL_FIRST = 2
|
|||||||
COL_LAST = 12
|
COL_LAST = 12
|
||||||
# 센터 단위로 병합되는 열
|
# 센터 단위로 병합되는 열
|
||||||
MERGE_COLS = (3, 4, 5, 6, 7, 11, 12)
|
MERGE_COLS = (3, 4, 5, 6, 7, 11, 12)
|
||||||
WIDTHS = {2: 6, 3: 12, 4: 12, 5: 12, 6: 11, 7: 10, 8: 12, 9: 26, 10: 8, 11: 12, 12: 11}
|
WIDTHS = {1: 10, 2: 6, 3: 12, 4: 12, 5: 12, 6: 11, 7: 10, 8: 12, 9: 26, 10: 8, 11: 12, 12: 11}
|
||||||
|
HEADER_BG = "DBE9F7" # 머리글 행 배경
|
||||||
|
PALLET_BG = "FAE2D5" # 출고방식이 파렛트인 센터 블록 배경
|
||||||
|
PALLET_METHOD = "파렛트"
|
||||||
|
|
||||||
TITLE_ROW = 2
|
TITLE_ROW = 2
|
||||||
COMPANY_ROW = 4
|
COMPANY_ROW = 4
|
||||||
@@ -36,9 +39,20 @@ HEADER_ROW = 5
|
|||||||
FIRST_DATA_ROW = 6
|
FIRST_DATA_ROW = 6
|
||||||
|
|
||||||
|
|
||||||
|
def with_dow(value: str) -> str:
|
||||||
|
"""YYYY-MM-DD → "YYYY-MM-DD(요일)". 날짜가 아니면 원문 그대로."""
|
||||||
|
text = (value or "").strip()
|
||||||
|
try:
|
||||||
|
d = _date.fromisoformat(text)
|
||||||
|
except ValueError:
|
||||||
|
return text
|
||||||
|
return f"{text}({DOW[d.weekday()]})"
|
||||||
|
|
||||||
|
|
||||||
def sheet_title(ship_date: str) -> str:
|
def sheet_title(ship_date: str) -> str:
|
||||||
"""시트명 = 출고일 YYYYMMDD."""
|
"""시트명 = 출고일 YYYYMMDD(요일)."""
|
||||||
return _date.fromisoformat(ship_date).strftime("%Y%m%d")
|
d = _date.fromisoformat(ship_date)
|
||||||
|
return f"{d.strftime('%Y%m%d')}({DOW[d.weekday()]})"
|
||||||
|
|
||||||
|
|
||||||
def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, Any]:
|
def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
@@ -53,6 +67,7 @@ def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, An
|
|||||||
d = _date.fromisoformat(ship_date)
|
d = _date.fromisoformat(ship_date)
|
||||||
tag = d.strftime("%Y%m%d")
|
tag = d.strftime("%Y%m%d")
|
||||||
title = f"{tag}({DOW[d.weekday()]}) 쿠팡로켓 밀크런 출고리스트"
|
title = f"{tag}({DOW[d.weekday()]}) 쿠팡로켓 밀크런 출고리스트"
|
||||||
|
pallet_ranges: list[tuple[int, int]] = []
|
||||||
|
|
||||||
cells: dict[tuple[int, int], Any] = {(TITLE_ROW, COL_FIRST): title,
|
cells: dict[tuple[int, int], Any] = {(TITLE_ROW, COL_FIRST): title,
|
||||||
(COMPANY_ROW, COL_FIRST): COMPANY}
|
(COMPANY_ROW, COL_FIRST): COMPANY}
|
||||||
@@ -77,12 +92,17 @@ def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, An
|
|||||||
row += 1
|
row += 1
|
||||||
end = row - 1
|
end = row - 1
|
||||||
|
|
||||||
|
method = sh.get("ship_method") or ""
|
||||||
|
if method == PALLET_METHOD:
|
||||||
|
pallet_ranges.append((start, end))
|
||||||
|
|
||||||
block = {
|
block = {
|
||||||
3: str(sh.get("document_date") or ""),
|
# 날짜는 요일까지 표기 — 2026-09-03(목)
|
||||||
4: str(sh.get("ship_date") or ""),
|
3: with_dow(str(sh.get("document_date") or "")),
|
||||||
5: str(sh.get("center_arrival_date") or ""),
|
4: with_dow(str(sh.get("ship_date") or "")),
|
||||||
|
5: with_dow(str(sh.get("center_arrival_date") or "")),
|
||||||
6: sh.get("center_name_snapshot") or "",
|
6: sh.get("center_name_snapshot") or "",
|
||||||
7: sh.get("ship_method") or "",
|
7: method,
|
||||||
11: sh.get("outbound_summary") or "",
|
11: sh.get("outbound_summary") or "",
|
||||||
12: WORKER,
|
12: WORKER,
|
||||||
}
|
}
|
||||||
@@ -91,13 +111,19 @@ def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, An
|
|||||||
if end > start:
|
if end > start:
|
||||||
merges.append((start, col, end, col))
|
merges.append((start, col, end, col))
|
||||||
|
|
||||||
return {"title": title, "cells": cells, "merges": merges, "last_row": row - 1}
|
return {
|
||||||
|
"title": title,
|
||||||
|
"cells": cells,
|
||||||
|
"merges": merges,
|
||||||
|
"last_row": row - 1,
|
||||||
|
"pallet_ranges": pallet_ranges,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any:
|
def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any:
|
||||||
"""xlsx 워크북(openpyxl). 시트명 = YYYYMMDD."""
|
"""xlsx 워크북(openpyxl). 시트명 = YYYYMMDD(요일)."""
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.styles import Alignment, Border, Font, Side
|
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
table = build_table(ship_date, shipments)
|
table = build_table(ship_date, shipments)
|
||||||
@@ -121,11 +147,13 @@ def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any:
|
|||||||
title_cell.font = Font(size=16, bold=True)
|
title_cell.font = Font(size=16, bold=True)
|
||||||
title_cell.alignment = center_align
|
title_cell.alignment = center_align
|
||||||
|
|
||||||
|
header_fill = PatternFill("solid", fgColor=HEADER_BG)
|
||||||
for col in range(COL_FIRST, COL_LAST + 1):
|
for col in range(COL_FIRST, COL_LAST + 1):
|
||||||
head = ws.cell(row=HEADER_ROW, column=col)
|
head = ws.cell(row=HEADER_ROW, column=col)
|
||||||
head.font = Font(bold=True)
|
head.font = Font(bold=True)
|
||||||
head.alignment = center_align
|
head.alignment = center_align
|
||||||
head.border = box
|
head.border = box
|
||||||
|
head.fill = header_fill
|
||||||
|
|
||||||
for row in range(FIRST_DATA_ROW, table["last_row"] + 1):
|
for row in range(FIRST_DATA_ROW, table["last_row"] + 1):
|
||||||
for col in range(COL_FIRST, COL_LAST + 1):
|
for col in range(COL_FIRST, COL_LAST + 1):
|
||||||
@@ -133,6 +161,13 @@ def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any:
|
|||||||
cell.border = box
|
cell.border = box
|
||||||
cell.alignment = left_align if col == 9 else center_align
|
cell.alignment = left_align if col == 9 else center_align
|
||||||
|
|
||||||
|
# 파렛트 출고 블록은 배경색으로 구분
|
||||||
|
pallet_fill = PatternFill("solid", fgColor=PALLET_BG)
|
||||||
|
for (r1, r2) in table["pallet_ranges"]:
|
||||||
|
for row in range(r1, r2 + 1):
|
||||||
|
for col in range(COL_FIRST, COL_LAST + 1):
|
||||||
|
ws.cell(row=row, column=col).fill = pallet_fill
|
||||||
|
|
||||||
for col, width in WIDTHS.items():
|
for col, width in WIDTHS.items():
|
||||||
ws.column_dimensions[get_column_letter(col)].width = width
|
ws.column_dimensions[get_column_letter(col)].width = width
|
||||||
ws.row_dimensions[TITLE_ROW].height = 28
|
ws.row_dimensions[TITLE_ROW].height = 28
|
||||||
|
|||||||
@@ -265,8 +265,8 @@ def _push_to_google_sheet(store: Any, ship_date: str) -> dict[str, Any]:
|
|||||||
from app.integrations.google_sheets import get_writer # noqa: WPS433
|
from app.integrations.google_sheets import get_writer # noqa: WPS433
|
||||||
|
|
||||||
from .export import ( # noqa: WPS433
|
from .export import ( # noqa: WPS433
|
||||||
COL_FIRST, COL_LAST, FIRST_DATA_ROW, HEADER_ROW, TITLE_ROW, WIDTHS,
|
COL_FIRST, COL_LAST, FIRST_DATA_ROW, HEADER_BG, HEADER_ROW, PALLET_BG,
|
||||||
build_table, sheet_title,
|
TITLE_ROW, WIDTHS, build_table, sheet_title,
|
||||||
)
|
)
|
||||||
|
|
||||||
spreadsheet_id = (os.getenv("CUPANG_SHEET_ID") or "").strip()
|
spreadsheet_id = (os.getenv("CUPANG_SHEET_ID") or "").strip()
|
||||||
@@ -295,6 +295,8 @@ def _push_to_google_sheet(store: Any, ship_date: str) -> dict[str, Any]:
|
|||||||
last_col=COL_LAST,
|
last_col=COL_LAST,
|
||||||
title_row=TITLE_ROW,
|
title_row=TITLE_ROW,
|
||||||
widths=WIDTHS,
|
widths=WIDTHS,
|
||||||
|
header_bg=HEADER_BG,
|
||||||
|
row_highlights=[(r1, r2, PALLET_BG) for (r1, r2) in table["pallet_ranges"]],
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001 - 시트 기록 실패는 경고로만 알린다
|
except Exception as exc: # noqa: BLE001 - 시트 기록 실패는 경고로만 알린다
|
||||||
return {"ok": False, "skipped": False, "reason": f"{type(exc).__name__}: {exc}"}
|
return {"ok": False, "skipped": False, "reason": f"{type(exc).__name__}: {exc}"}
|
||||||
|
|||||||
Reference in New Issue
Block a user