Refine voucher compare UI/layout and fix suggestion behavior

This commit is contained in:
b17301
2026-04-27 16:22:23 +09:00
parent 080f7cf112
commit 354d149245
8 changed files with 5977 additions and 18 deletions
+356
View File
@@ -3,6 +3,7 @@ import logging
import json
import re
import time
import tempfile
import zipfile
from datetime import date, datetime
from functools import lru_cache
@@ -18,6 +19,23 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from openpyxl import load_workbook
from sqlalchemy import create_engine, event, text
from wehago_compare import (
get_erp_filtered_rows,
get_individual_pair_recommendations,
get_last_action_summary,
get_status_field_suggestions,
get_status_detail_rows,
get_wehago_compare_dashboard,
get_wehago_filtered_rows,
import_uploaded_erp_voucher_file,
init_wehago_compare_db,
recommend_pair_matches,
refresh_wehago_compare_data,
save_recommended_pair_matches,
save_manual_pair_matches,
save_recheck_review_rows,
undo_last_action,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -1078,6 +1096,7 @@ def init_db() -> None:
migrate_project_basic_info(conn)
ensure_default_app_config(conn)
conn.execute(text("ANALYZE"))
init_wehago_compare_db(engine)
@lru_cache(maxsize=1)
@@ -6135,6 +6154,20 @@ def render_annual_summary_page(request: Request, message: str = "") -> HTMLRespo
return templates.TemplateResponse(request, "annual_summary.html", context)
def render_wehago_compare_page(
request: Request,
start_year: int | None = None,
end_year: int | None = None,
message: str = "",
) -> HTMLResponse:
init_db()
context = {
**base_context(request, message),
"wehago_compare": get_wehago_compare_dashboard(engine, start_year=start_year, end_year=end_year),
}
return templates.TemplateResponse(request, "wehago_compare.html", context)
@app.get("/health")
async def health() -> dict[str, str]:
return build_health_payload()
@@ -6620,6 +6653,329 @@ async def annual_summary(request: Request):
return HTMLResponse("<h1>서버 오류</h1><p>로그를 확인해주세요.</p>", status_code=500)
@app.get("/wehago-compare")
async def wehago_compare(request: Request, start_year: str | None = None, end_year: str | None = None):
try:
return render_wehago_compare_page(
request,
start_year=parse_optional_year(start_year),
end_year=parse_optional_year(end_year),
)
except Exception as exc:
logger.exception("전표비교 페이지 에러: %s", exc)
return HTMLResponse("<h1>서버 오류</h1><p>로그를 확인해주세요.</p>", status_code=500)
@app.post("/wehago-compare/refresh")
async def refresh_wehago_compare(
request: Request,
start_year: str | None = None,
end_year: str | None = None,
):
try:
summary = refresh_wehago_compare_data(engine)
message = "WEHAGO DB 갱신 완료: " + ", ".join(f"{key}={value}" for key, value in summary.items())
return render_wehago_compare_page(
request,
start_year=parse_optional_year(start_year),
end_year=parse_optional_year(end_year),
message=message,
)
except Exception as exc:
logger.exception("전표비교 DB 갱신 에러: %s", exc)
return render_wehago_compare_page(
request,
start_year=parse_optional_year(start_year),
end_year=parse_optional_year(end_year),
message=f"WEHAGO DB 갱신 중 오류가 발생했습니다: {exc}",
)
@app.post("/wehago-compare/upload-erp")
async def upload_wehago_erp_file(
request: Request,
start_year: str | None = None,
end_year: str | None = None,
erp_file: UploadFile = File(...),
):
temp_path: Path | None = None
try:
if not erp_file.filename:
raise ValueError("업로드할 ERP 파일을 선택해주세요.")
suffix = Path(erp_file.filename).suffix or ".xlsx"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as handle:
temp_path = Path(handle.name)
while True:
chunk = await erp_file.read(1024 * 1024)
if not chunk:
break
handle.write(chunk)
summary = import_uploaded_erp_voucher_file(engine, temp_path, erp_file.filename)
message = (
f"ERP 파일 반영 완료: {summary['file_name']} "
f"(추가 {summary['inserted_rows']}건, 중복 제외 {summary['duplicate_rows']}건)"
)
return render_wehago_compare_page(
request,
start_year=parse_optional_year(start_year),
end_year=parse_optional_year(end_year),
message=message,
)
except Exception as exc:
logger.exception("전표비교 ERP 업로드 에러: %s", exc)
return render_wehago_compare_page(
request,
start_year=parse_optional_year(start_year),
end_year=parse_optional_year(end_year),
message=f"ERP 파일 업로드 중 오류가 발생했습니다: {exc}",
)
finally:
await erp_file.close()
if temp_path and temp_path.exists():
temp_path.unlink(missing_ok=True)
@app.get("/wehago-compare/api/status-rows")
async def wehago_compare_status_rows(
start_year: int | None = None,
end_year: int | None = None,
status: str = "",
voucher_no: str = "",
draft_no: str = "",
wehago_account: str = "",
erp_account: str = "",
wehago_amount: str = "",
erp_amount: str = "",
wehago_vendor: str = "",
erp_vendor: str = "",
desc_keyword: str = "",
review_reason: str = "",
offset: int = 0,
limit: int = 200,
):
try:
payload = get_status_detail_rows(
engine,
start_year=start_year,
end_year=end_year,
status=status,
voucher_no=voucher_no,
draft_no=draft_no,
wehago_account=wehago_account,
erp_account=erp_account,
wehago_amount=wehago_amount,
erp_amount=erp_amount,
wehago_vendor=wehago_vendor,
erp_vendor=erp_vendor,
desc_keyword=desc_keyword,
review_reason=review_reason,
offset=offset,
limit=limit,
)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 상태 상세 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.get("/wehago-compare/api/status-suggestions")
async def wehago_compare_status_suggestions(
start_year: int | None = None,
end_year: int | None = None,
status: str = "",
field: str = "voucher_no",
keyword: str = "",
offset: int = 0,
limit: int = 10,
):
try:
payload = get_status_field_suggestions(
engine,
start_year=start_year,
end_year=end_year,
status=status,
field=field,
keyword=keyword,
offset=offset,
limit=limit,
)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 자동완성 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.post("/wehago-compare/api/recheck-review-save")
async def wehago_compare_recheck_review_save(request: Request):
try:
payload = await request.json()
rows = payload.get("rows") if isinstance(payload, dict) else None
if not isinstance(rows, list):
raise ValueError("저장할 검토 항목 형식이 올바르지 않습니다.")
saved = save_recheck_review_rows(engine, rows)
return JSONResponse(content={"saved_count": saved})
except Exception as exc:
logger.exception("전표비교 검토 저장 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.post("/wehago-compare/api/pair-match-save")
async def wehago_compare_pair_match_save(request: Request):
try:
payload = await request.json()
ledger_rows = payload.get("ledger_rows") if isinstance(payload, dict) else None
voucher_rows = payload.get("voucher_rows") if isinstance(payload, dict) else None
if not isinstance(ledger_rows, list) or not isinstance(voucher_rows, list):
raise ValueError("저장할 쌍비교 항목 형식이 올바르지 않습니다.")
saved = save_manual_pair_matches(engine, ledger_rows, voucher_rows)
return JSONResponse(content={"saved_count": saved})
except Exception as exc:
logger.exception("전표비교 쌍매칭 저장 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.get("/wehago-compare/api/pair-recommendations")
async def wehago_compare_pair_recommendations(
start_year: int | None = None,
end_year: int | None = None,
ledger_voucher_no: str = "",
ledger_review_reason: str = "",
voucher_voucher_no: str = "",
voucher_review_reason: str = "",
limit: int = 300,
):
try:
payload = recommend_pair_matches(
engine,
start_year=start_year,
end_year=end_year,
ledger_voucher_no=ledger_voucher_no,
ledger_review_reason=ledger_review_reason,
voucher_voucher_no=voucher_voucher_no,
voucher_review_reason=voucher_review_reason,
limit=limit,
)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 추천 매칭 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.post("/wehago-compare/api/pair-recommendations/apply")
async def wehago_compare_pair_recommendations_apply(request: Request):
try:
payload = await request.json()
pair_keys = payload.get("pair_keys") if isinstance(payload, dict) else None
auto_only = bool(payload.get("auto_only")) if isinstance(payload, dict) else False
saved = save_recommended_pair_matches(
engine,
start_year=parse_optional_year(payload.get("start_year")) if isinstance(payload, dict) else None,
end_year=parse_optional_year(payload.get("end_year")) if isinstance(payload, dict) else None,
pair_keys=pair_keys if isinstance(pair_keys, list) else None,
auto_only=auto_only,
ledger_voucher_no=payload.get("ledger_voucher_no", "") if isinstance(payload, dict) else "",
ledger_review_reason=payload.get("ledger_review_reason", "") if isinstance(payload, dict) else "",
voucher_voucher_no=payload.get("voucher_voucher_no", "") if isinstance(payload, dict) else "",
voucher_review_reason=payload.get("voucher_review_reason", "") if isinstance(payload, dict) else "",
)
return JSONResponse(content=jsonable_encoder(saved))
except Exception as exc:
logger.exception("전표비교 추천 매칭 저장 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.get("/wehago-compare/api/last-action")
async def wehago_compare_last_action():
try:
payload = get_last_action_summary(engine=engine)
return JSONResponse(content=jsonable_encoder(payload or {}))
except Exception as exc:
logger.exception("전표비교 최근 작업 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.post("/wehago-compare/api/undo-last-action")
async def wehago_compare_undo_last_action():
try:
payload = undo_last_action(engine)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 되돌리기 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.get("/wehago-compare/api/pair-individual-recommendations")
async def wehago_compare_pair_individual_recommendations(
start_year: int | None = None,
end_year: int | None = None,
source_status: str = "",
source_row_key: str = "",
offset: int = 0,
limit: int = 10,
):
try:
payload = get_individual_pair_recommendations(
engine,
start_year=start_year,
end_year=end_year,
source_status=source_status,
source_row_key=source_row_key,
offset=offset,
limit=limit,
)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 개별 추천 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.get("/wehago-compare/api/wehago-rows")
async def wehago_compare_wehago_rows(
start_year: int | None = None,
end_year: int | None = None,
voucher_no: str = "",
account_code: str = "",
vendor_name: str = "",
):
try:
payload = get_wehago_filtered_rows(
engine,
start_year=start_year,
end_year=end_year,
voucher_no=voucher_no,
account_code=account_code,
vendor_name=vendor_name,
)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 WEHAGO 상세 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.get("/wehago-compare/api/erp-rows")
async def wehago_compare_erp_rows(
start_year: int | None = None,
end_year: int | None = None,
voucher_no: str = "",
account_code: str = "",
vendor_name: str = "",
):
try:
payload = get_erp_filtered_rows(
engine,
start_year=start_year,
end_year=end_year,
voucher_no=voucher_no,
account_code=account_code,
vendor_name=vendor_name,
)
return JSONResponse(content=jsonable_encoder(payload))
except Exception as exc:
logger.exception("전표비교 ERP 상세 조회 에러: %s", exc)
return JSONResponse(content={"error": str(exc)}, status_code=500)
@app.post("/upload")
async def upload_excel(request: Request, excel_file: UploadFile = File(...)):
try: