feat(expense): 승인완료 메뉴 — 전 직원 월별 + 엑셀/첨부 zip

- /expense/approved: 승인자/관리자 전용, 월 선택 + 전 직원 승인완료 항목
- 직원별 승인 금액 합계표 + 총합
- 엑셀 다운(리스트 + 직원별합계 시트)
- 첨부 일괄 zip: 파일명 yy년 mm월 dd일(요일)_이름.확장자 (업로드일 기준)
- store: APPROVED_STATUSES(승인,정산완료) / db: list_approved, approved_attachments

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 17:46:41 +09:00
parent 506db5ff07
commit a107b06826
5 changed files with 396 additions and 2 deletions
+41 -1
View File
@@ -20,7 +20,7 @@ from psycopg_pool import ConnectionPool
from app.timezone import KST
from .store import CATEGORIES, METHODS, STATUSES
from .store import APPROVED_STATUSES, CATEGORIES, METHODS, STATUSES
class ExpenseDBStore:
@@ -370,6 +370,46 @@ class ExpenseDBStore:
out["size_bytes"] = int(out.get("size_bytes", 0))
return out
# ── 승인완료 (월별, 전 직원) ──
def list_approved(self, *, year: int, month: int) -> list[dict[str, Any]]:
"""해당 월(spent_at)의 승인완료 항목 — 전 직원."""
with self._pool.connection() as conn:
rows = conn.execute(
"""
SELECT * FROM expense_items
WHERE status = ANY(%s)
AND EXTRACT(YEAR FROM spent_at) = %s
AND EXTRACT(MONTH FROM spent_at) = %s
ORDER BY owner ASC, spent_at ASC, created_at ASC
""",
(list(APPROVED_STATUSES), year, month),
).fetchall()
return [self._serialize(r) for r in rows]
def approved_attachments(
self, *, year: int, month: int
) -> list[dict[str, Any]]:
"""해당 월 승인완료 항목의 첨부 — zip 다운로드용.
uploaded_at(datetime), spent_at(date) 를 가공 없이 반환(파일명 생성용).
"""
with self._pool.connection() as conn:
rows = conn.execute(
"""
SELECT a.id, a.item_id, a.owner, a.kind, a.filename,
a.stored_path, a.content_type, a.uploaded_at,
i.spent_at
FROM expense_attachments a
JOIN expense_items i ON i.id = a.item_id
WHERE i.status = ANY(%s)
AND EXTRACT(YEAR FROM i.spent_at) = %s
AND EXTRACT(MONTH FROM i.spent_at) = %s
ORDER BY a.owner ASC, a.uploaded_at ASC
""",
(list(APPROVED_STATUSES), year, month),
).fetchall()
return [dict(r) for r in rows]
# ── 집계 (월별) ──
def monthly_summary(
self, *, email: str, year: int