完成差旅发票录入流程
This commit is contained in:
+33
-82
@@ -38,13 +38,32 @@ from src.doc.fill_consumable_doc import ( # noqa: E402, I001
|
||||
fill_consumable_from_template,
|
||||
)
|
||||
from src.doc.invoice import ( # noqa: E402, I001
|
||||
classify_invoice_batch,
|
||||
load_csv,
|
||||
load_invoice_csv,
|
||||
save_csv as save_payment_csv,
|
||||
save_invoice_csv,
|
||||
save_application_json,
|
||||
)
|
||||
|
||||
|
||||
def _classify_invoice_batch(
|
||||
invoices: list[dict[str, str]],
|
||||
) -> dict[str, list[dict[str, str]]]:
|
||||
"""按发票类型分组"""
|
||||
travel: list[dict[str, str]] = []
|
||||
general: list[dict[str, str]] = []
|
||||
application: list[dict[str, str]] = []
|
||||
for inv in invoices:
|
||||
inv_type = inv.get("invoice_type", "general")
|
||||
if inv_type == "application":
|
||||
application.append(inv)
|
||||
elif inv_type in ("train", "hotel"):
|
||||
travel.append(inv)
|
||||
else:
|
||||
general.append(inv)
|
||||
return {"travel": travel, "general": general, "application": application}
|
||||
|
||||
|
||||
fill_log = get_logger("fill_consumable_doc")
|
||||
CONSUMABLE_TEMPLATE = PROJECT_ROOT / CONSUMABLE_DOC_FILENAME
|
||||
|
||||
@@ -161,7 +180,7 @@ def _has_general_invoices(rows: list[dict[str, str]]) -> bool:
|
||||
invoices.append(row)
|
||||
if not invoices:
|
||||
return False
|
||||
groups = classify_invoice_batch(invoices)
|
||||
groups = _classify_invoice_batch(invoices)
|
||||
return len(groups["general"]) > 0
|
||||
|
||||
|
||||
@@ -179,7 +198,7 @@ def _get_invoice_groups(rows: list[dict[str, str]]) -> dict[str, int]:
|
||||
# 发票级别格式:直接使用
|
||||
elif "发票类型" in row:
|
||||
invoices.append(row)
|
||||
groups = classify_invoice_batch(invoices)
|
||||
groups = _classify_invoice_batch(invoices)
|
||||
return {
|
||||
"travel_count": len(groups["travel"]),
|
||||
"general_count": len(groups["general"]),
|
||||
@@ -250,14 +269,18 @@ def run_pipeline_web(session_dir: Path, config: dict[str, Any]) -> dict[str, Any
|
||||
start = time.time()
|
||||
|
||||
# ---- Step 1: 发票提取 ----
|
||||
invoices, groups = extract_invoices(str(session_dir))
|
||||
invoices, applications, groups = extract_invoices(str(session_dir))
|
||||
if not invoices:
|
||||
return {"ok": False, "error": "未提取到任何发票数据"}
|
||||
|
||||
# 保存两个 CSV:支付记录级别(供 bot/出库单使用)和发票级别(供人工参考)
|
||||
# 保存 CSV:支付记录级别(供 bot/出库单使用)和发票级别(供人工参考)
|
||||
save_payment_csv(invoices, session_dir / "payment_records.csv")
|
||||
save_invoice_csv(invoices, session_dir / "invoice_summary.csv")
|
||||
|
||||
# 出差申请单单独保存
|
||||
if applications:
|
||||
save_application_json(applications, session_dir / "travel_applications.json")
|
||||
|
||||
# 统计发票总数
|
||||
invoice_count = sum(len(inv.get("_matched_invoices", [])) for inv in invoices)
|
||||
|
||||
@@ -276,50 +299,6 @@ def run_pipeline_web(session_dir: Path, config: dict[str, Any]) -> dict[str, Any
|
||||
return result
|
||||
|
||||
|
||||
def run_csv_pipeline_web(session_dir: Path, config: dict[str, Any], csv_filename: str) -> dict[str, Any]:
|
||||
"""直接使用上传的 CSV 文件,跳过 PDF 提取"""
|
||||
start = time.time()
|
||||
|
||||
csv_path = session_dir / csv_filename
|
||||
if not csv_path.exists():
|
||||
return {"ok": False, "error": "CSV 文件不存在"}
|
||||
|
||||
# 尝试读取支付记录格式
|
||||
rows = load_csv(csv_path)
|
||||
if rows is None:
|
||||
# 尝试读取发票级别格式
|
||||
invoice_rows = load_invoice_csv(csv_path)
|
||||
if invoice_rows is None:
|
||||
return {"ok": False, "error": "CSV 读取失败"}
|
||||
# 发票级别格式:直接统计
|
||||
type_stats = _get_invoice_groups(invoice_rows)
|
||||
elapsed = time.time() - start
|
||||
return {
|
||||
"ok": True,
|
||||
"elapsed": f"{elapsed:.1f}s",
|
||||
"invoice_count": len(invoice_rows),
|
||||
"csv_url": f"/api/download/{session_dir.name}/{csv_filename}",
|
||||
"travel_count": type_stats["travel_count"],
|
||||
"general_count": type_stats["general_count"],
|
||||
}
|
||||
|
||||
# 支付记录格式:统计发票类型
|
||||
type_stats = _get_invoice_groups(rows)
|
||||
|
||||
elapsed = time.time() - start
|
||||
result = {
|
||||
"ok": True,
|
||||
"elapsed": f"{elapsed:.1f}s",
|
||||
"invoice_count": type_stats["travel_count"] + type_stats["general_count"],
|
||||
"csv_url": f"/api/download/{session_dir.name}/{csv_filename}",
|
||||
"travel_count": type_stats["travel_count"],
|
||||
"general_count": type_stats["general_count"],
|
||||
}
|
||||
doc_fill = _try_fill_consumable_doc(session_dir, config)
|
||||
_append_doc_download(result, session_dir.name, doc_fill)
|
||||
return result
|
||||
|
||||
|
||||
def run_financial_submit(session_dir: Path, config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行财务系统填报(从前端确认后调用)
|
||||
|
||||
@@ -331,9 +310,10 @@ def run_financial_submit(session_dir: Path, config: dict[str, Any]) -> dict[str,
|
||||
if not csv_path.exists():
|
||||
return {"ok": False, "error": "未找到发票数据,请先处理"}
|
||||
|
||||
from src.bot import load_invoice_data, run_bot_web
|
||||
from src.bot import run_bot_web
|
||||
|
||||
bot_invoices = load_invoice_data(str(csv_path), config)
|
||||
# TODO: 改为从缓存读取或直接请求 LLM,与差旅报销保持一致
|
||||
# bot_invoices = load_invoice_data(str(csv_path), config)
|
||||
|
||||
# 判断发票类型
|
||||
rows = load_csv(csv_path)
|
||||
@@ -345,7 +325,7 @@ def run_financial_submit(session_dir: Path, config: dict[str, Any]) -> dict[str,
|
||||
else:
|
||||
fill_log.info("检测到普通发票,使用普通报销模式")
|
||||
|
||||
run_bot_web(config, bot_invoices, session_dir)
|
||||
run_bot_web(config, session_dir)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -384,22 +364,6 @@ def upload_file(session_id: str) -> Any:
|
||||
return jsonify({"ok": True, "filename": safe_name})
|
||||
|
||||
|
||||
@app.route("/api/upload-csv/<session_id>", methods=["POST"])
|
||||
def upload_csv(session_id: str) -> Any:
|
||||
"""上传 CSV 发票数据文件(跳过 PDF 提取)"""
|
||||
session_dir = _validate_session(session_id)
|
||||
if isinstance(session_dir, tuple):
|
||||
return session_dir
|
||||
|
||||
f = request.files.get("file")
|
||||
if not f or not f.filename:
|
||||
return jsonify({"error": "未选择文件"}), 400
|
||||
|
||||
safe_name = Path(f.filename).name
|
||||
f.save(str(session_dir / safe_name))
|
||||
return jsonify({"ok": True, "filename": safe_name})
|
||||
|
||||
|
||||
@app.route("/api/files/<session_id>", methods=["GET"])
|
||||
def list_files(session_id: str) -> Any:
|
||||
"""列出会话目录中的文件"""
|
||||
@@ -420,7 +384,6 @@ def start_process(session_id: str) -> Any:
|
||||
return session_dir
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
mode = body.get("mode", "auto") # "pdf", "csv", or "auto"
|
||||
|
||||
# 读取配置
|
||||
config = _build_web_config(body)
|
||||
@@ -435,19 +398,7 @@ def start_process(session_id: str) -> Any:
|
||||
def _run() -> None:
|
||||
result = {"ok": False, "error": "未知错误"}
|
||||
try:
|
||||
if mode == "csv":
|
||||
csv_files = list(session_dir.glob("*.csv"))
|
||||
if not csv_files:
|
||||
result = {"ok": False, "error": "未找到 CSV 文件"}
|
||||
else:
|
||||
result = run_csv_pipeline_web(session_dir, config, csv_files[0].name)
|
||||
else:
|
||||
csv_files = list(session_dir.glob("*.csv"))
|
||||
pdf_files = list(session_dir.glob("*.pdf"))
|
||||
if csv_files and not pdf_files:
|
||||
result = run_csv_pipeline_web(session_dir, config, csv_files[0].name)
|
||||
else:
|
||||
result = run_pipeline_web(session_dir, config)
|
||||
result = run_pipeline_web(session_dir, config)
|
||||
except BaseException as e:
|
||||
result = {"ok": False, "error": str(e)}
|
||||
if isinstance(e, KeyboardInterrupt | SystemExit):
|
||||
|
||||
Reference in New Issue
Block a user