Files
dbx-main/app/modules/expense/templates/expense/reports.html
T
king 550867ba0c feat(expense+admin): 결재 워크플로 + 첨부 + 집계 + 엑셀 + 사용자 직접 등록
[admin]
- POST /api/users 사용자 직접 등록 (이메일만으로). UserStore.create_user 추가
- admin.html: 등록 폼 + 삭제 버튼 + 한글 라벨 + 신규 권한 표시
- 신규 권한 키: expense_approver, vacation_approver (APPROVER_KEYS)
- is_approver(user_rec, kind) 헬퍼

[expense 워크플로]
- 상태 전이: 작성중 → 제출 → 승인/반려 → 정산완료
- API: /api/items/{id}/{submit,revert,approve,reject,settle}
- 권한: submit/revert=owner, approve/reject/settle=expense_approver 또는 admin
- expense_items 컬럼: approver_email, decided_at, reject_reason
- 작성중/반려 상태에서만 수정/삭제/첨부 가능

[expense 첨부]
- 영수증/기타 (kind: receipt|other). 최대 20MB, 확장자 화이트리스트
- 저장: DATA_DIR/uploads/expense/{item_id}/{att_id}_{filename}
- expense_attachments 테이블 (CASCADE on item delete)
- API: 업로드/목록/다운로드/삭제

[expense 집계]
- /expense/reports: 연도별 월×카테고리 피벗 표

[expense 엑셀]
- GET /expense/api/export.xlsx?from&to&scope=mine|all
- scope=all 은 승인자/관리자만

[DDL]
- expense_db_init.sql: 신규 컬럼/테이블 포함 (멱등)
- expense_db_002_workflow_attachments.sql: 운영 DB 마이그레이션용

[deps]
- openpyxl>=3.1, python-multipart>=0.0.20

[docs]
- DATABASES.md: 워크플로 다이어그램, attachments 테이블, 마이그레이션
- PROJECT_OVERVIEW.md: 권한 키 표
- DEPLOYMENT.md: DATA_DIR/uploads 안내
2026-05-29 02:52:44 +09:00

79 lines
2.9 KiB
HTML

{% extends "erp_base.html" %}
{% block content %}
<section class="erp-reports">
<div class="erp-page-actions">
<a class="erp-btn erp-btn-ghost" href="/expense/">← 내 개인경비</a>
<form method="get" style="display: inline-flex; gap: 6px; align-items: center;">
<label class="erp-muted">연도</label>
<input type="number" name="year" value="{{ year }}" min="2000" max="2100"
style="width: 100px; padding: 6px 10px; border: 1px solid var(--color-subtle-ash);
border-radius: var(--r-input); font-family: inherit;" />
<button type="submit" class="erp-btn erp-btn-outline">조회</button>
</form>
<a class="erp-btn erp-btn-outline"
href="/expense/api/export.xlsx?from={{ year }}-01-01&to={{ year }}-12-31">엑셀</a>
</div>
<div class="erp-card-block">
<div class="erp-card-block-head">
<h2>{{ year }}년 월별 / 카테고리별 합계</h2>
<span class="erp-muted">총 {{ "{:,}".format(grand_total) }} 원</span>
</div>
<div class="erp-table-wrap">
<table class="erp-table">
<thead>
<tr>
<th style="width: 110px;"></th>
{% for c in categories %}
<th style="text-align: right;">{{ c }}</th>
{% endfor %}
<th style="text-align: right; background: var(--color-ghost-gray);">합계</th>
</tr>
</thead>
<tbody>
{% for m in months %}
{% set row_total = pivot[m].values() | sum %}
<tr>
<td><strong>{{ m }}</strong></td>
{% for c in categories %}
<td style="text-align: right; font-variant-numeric: tabular-nums;">
{% if pivot[m].get(c) %}{{ "{:,}".format(pivot[m][c]) }}{% else %}-{% endif %}
</td>
{% endfor %}
<td style="text-align: right; font-variant-numeric: tabular-nums; background: var(--color-ghost-gray);">
<strong>{{ "{:,}".format(row_total) }}</strong>
</td>
</tr>
{% else %}
<tr><td colspan="{{ categories | length + 2 }}" class="erp-empty">데이터 없음</td></tr>
{% endfor %}
</tbody>
{% if months %}
<tfoot>
<tr style="background: var(--color-ghost-gray);">
<th>합계</th>
{% for c in categories %}
<th style="text-align: right; font-variant-numeric: tabular-nums;">
{{ "{:,}".format(cat_totals[c]) }}
</th>
{% endfor %}
<th style="text-align: right; font-variant-numeric: tabular-nums;">
{{ "{:,}".format(grand_total) }}
</th>
</tr>
</tfoot>
{% endif %}
</table>
</div>
</div>
</section>
<style>
.erp-page-actions { display: flex; gap: var(--sp-8); margin-bottom: var(--sp-16); align-items: center; flex-wrap: wrap; }
</style>
{% endblock %}