Refine voucher compare UI/layout and fix suggestion behavior
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user