style(expense): 승인 목록 엑셀 서식 — 제목행 파랑/금액 쉼표/열너비 자동
- 제목행: 파랑(4472C4) 배경 + 흰 굵은 글씨 + 가운데 정렬 - 금액/합계금액: #,##0 쉼표 표시형식 - A~K 열너비 내용 기준 자동(한글/전각 2칸 계산) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -402,12 +402,52 @@ async def approved_page(request: Request) -> HTMLResponse:
|
||||
)
|
||||
|
||||
|
||||
def _xl_display_len(value: Any) -> int:
|
||||
"""엑셀 열너비 산정용 표시 폭. 한글/전각은 2칸으로 계산."""
|
||||
s = "" if value is None else str(value)
|
||||
width = 0
|
||||
for ch in s:
|
||||
# CJK(한글/한자/가나) 및 전각 문자 → 2칸
|
||||
width += 2 if ord(ch) > 0x2E7F else 1
|
||||
return width
|
||||
|
||||
|
||||
def _autofit_columns(ws: Any, ncols: int, *, min_w: int = 6, max_w: int = 60) -> None:
|
||||
"""A~열 내용 기준 자동 너비. 헤더 포함 최댓값 + 여유."""
|
||||
for col in range(1, ncols + 1):
|
||||
longest = 0
|
||||
for cell in ws[ws.cell(row=1, column=col).column_letter]:
|
||||
longest = max(longest, _xl_display_len(cell.value))
|
||||
ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = (
|
||||
max(min_w, min(max_w, longest + 2))
|
||||
)
|
||||
|
||||
|
||||
def _style_header(ws: Any, ncols: int) -> None:
|
||||
"""제목 행: 파랑 배경, 흰 굵은 글씨, 가운데 정렬."""
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
|
||||
fill = PatternFill(start_color="FF4472C4", end_color="FF4472C4", fill_type="solid")
|
||||
font = Font(color="FFFFFFFF", bold=True)
|
||||
align = Alignment(horizontal="center", vertical="center")
|
||||
for col in range(1, ncols + 1):
|
||||
cell = ws.cell(row=1, column=col)
|
||||
cell.fill = fill
|
||||
cell.font = font
|
||||
cell.alignment = align
|
||||
|
||||
|
||||
def _build_approved_xlsx(
|
||||
items: list[dict[str, Any]], names: dict[str, str]
|
||||
) -> io.BytesIO:
|
||||
"""승인완료 목록 + 직원별합계 시트 xlsx 바이트 생성."""
|
||||
"""승인완료 목록 + 직원별합계 시트 xlsx 바이트 생성.
|
||||
|
||||
서식: 제목행 파랑/흰굵은글씨/가운데, 금액 쉼표(#,##0), 열너비 내용 자동.
|
||||
"""
|
||||
from openpyxl import Workbook # 지연 import
|
||||
|
||||
money_fmt = "#,##0"
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "승인완료"
|
||||
@@ -416,6 +456,7 @@ def _build_approved_xlsx(
|
||||
"금액", "메모", "상태", "승인자", "결재일시",
|
||||
]
|
||||
ws.append(header)
|
||||
amount_col = header.index("금액") + 1 # 7 (G열)
|
||||
for r in items:
|
||||
owner = r.get("owner", "")
|
||||
ws.append([
|
||||
@@ -431,17 +472,22 @@ def _build_approved_xlsx(
|
||||
r.get("approver_email", "") or "",
|
||||
r.get("decided_at", "") or "",
|
||||
])
|
||||
widths = [12, 14, 28, 10, 12, 24, 12, 30, 10, 24, 22]
|
||||
for col, w in enumerate(widths, start=1):
|
||||
ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = w
|
||||
# 금액 열 쉼표 표시형식
|
||||
for row in range(2, ws.max_row + 1):
|
||||
ws.cell(row=row, column=amount_col).number_format = money_fmt
|
||||
_style_header(ws, len(header))
|
||||
_autofit_columns(ws, len(header))
|
||||
|
||||
# 직원별 합계 시트
|
||||
ws2 = wb.create_sheet("직원별합계")
|
||||
ws2.append(["이름", "이메일", "건수", "합계금액"])
|
||||
header2 = ["이름", "이메일", "건수", "합계금액"]
|
||||
ws2.append(header2)
|
||||
for row in _approved_aggregate(items, names):
|
||||
ws2.append([row["name"], row["owner"], row["count"], row["total"]])
|
||||
for col, w in enumerate([14, 28, 8, 16], start=1):
|
||||
ws2.column_dimensions[ws2.cell(row=1, column=col).column_letter].width = w
|
||||
for row in range(2, ws2.max_row + 1):
|
||||
ws2.cell(row=row, column=4).number_format = money_fmt # 합계금액
|
||||
_style_header(ws2, len(header2))
|
||||
_autofit_columns(ws2, len(header2))
|
||||
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
|
||||
Reference in New Issue
Block a user