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(
|
def _build_approved_xlsx(
|
||||||
items: list[dict[str, Any]], names: dict[str, str]
|
items: list[dict[str, Any]], names: dict[str, str]
|
||||||
) -> io.BytesIO:
|
) -> io.BytesIO:
|
||||||
"""승인완료 목록 + 직원별합계 시트 xlsx 바이트 생성."""
|
"""승인완료 목록 + 직원별합계 시트 xlsx 바이트 생성.
|
||||||
|
|
||||||
|
서식: 제목행 파랑/흰굵은글씨/가운데, 금액 쉼표(#,##0), 열너비 내용 자동.
|
||||||
|
"""
|
||||||
from openpyxl import Workbook # 지연 import
|
from openpyxl import Workbook # 지연 import
|
||||||
|
|
||||||
|
money_fmt = "#,##0"
|
||||||
|
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "승인완료"
|
ws.title = "승인완료"
|
||||||
@@ -416,6 +456,7 @@ def _build_approved_xlsx(
|
|||||||
"금액", "메모", "상태", "승인자", "결재일시",
|
"금액", "메모", "상태", "승인자", "결재일시",
|
||||||
]
|
]
|
||||||
ws.append(header)
|
ws.append(header)
|
||||||
|
amount_col = header.index("금액") + 1 # 7 (G열)
|
||||||
for r in items:
|
for r in items:
|
||||||
owner = r.get("owner", "")
|
owner = r.get("owner", "")
|
||||||
ws.append([
|
ws.append([
|
||||||
@@ -431,17 +472,22 @@ def _build_approved_xlsx(
|
|||||||
r.get("approver_email", "") or "",
|
r.get("approver_email", "") or "",
|
||||||
r.get("decided_at", "") 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):
|
for row in range(2, ws.max_row + 1):
|
||||||
ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = w
|
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 = wb.create_sheet("직원별합계")
|
||||||
ws2.append(["이름", "이메일", "건수", "합계금액"])
|
header2 = ["이름", "이메일", "건수", "합계금액"]
|
||||||
|
ws2.append(header2)
|
||||||
for row in _approved_aggregate(items, names):
|
for row in _approved_aggregate(items, names):
|
||||||
ws2.append([row["name"], row["owner"], row["count"], row["total"]])
|
ws2.append([row["name"], row["owner"], row["count"], row["total"]])
|
||||||
for col, w in enumerate([14, 28, 8, 16], start=1):
|
for row in range(2, ws2.max_row + 1):
|
||||||
ws2.column_dimensions[ws2.cell(row=1, column=col).column_letter].width = w
|
ws2.cell(row=row, column=4).number_format = money_fmt # 합계금액
|
||||||
|
_style_header(ws2, len(header2))
|
||||||
|
_autofit_columns(ws2, len(header2))
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
wb.save(buf)
|
wb.save(buf)
|
||||||
|
|||||||
Reference in New Issue
Block a user