feat: 完善报销单审批流程及退回原因追踪

新增直属领导审批通过接口和审批待办列表查询,报销单退回
支持原因码分类和审批环节标记,优化票据附件去重和路径
回退查找,前端新增退回原因对话框、审批收件箱和工作台
图标组件,补充工具函数和单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-20 21:00:47 +08:00
parent f8b25a7ccc
commit 002bf4f756
62 changed files with 5331 additions and 2101 deletions

View File

@@ -546,11 +546,11 @@ def test_user_agent_guides_implicit_expense_draft_request() -> None:
assert slot_map["amount"].value == "1000.00元"
def test_user_agent_guides_narrative_with_day_before_yesterday() -> None:
session_factory = build_session_factory()
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
def test_user_agent_guides_narrative_with_day_before_yesterday() -> None:
session_factory = build_session_factory()
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query="我前天请客户吃饭花了200元",
user_id="pytest",
context_json={
@@ -571,15 +571,106 @@ def test_user_agent_guides_narrative_with_day_before_yesterday() -> None:
assert response.review_payload is not None
slot_map = {item.key: item for item in response.review_payload.slot_cards}
assert slot_map["time_range"].raw_value == "前天"
assert slot_map["time_range"].value == "2026-05-11"
assert "时间为 2026-05-11" in response.review_payload.intent_summary
def test_user_agent_attachment_only_upload_uses_generic_scene_reason_without_fabrication() -> None:
session_factory = build_session_factory()
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
assert slot_map["time_range"].raw_value == "前天"
assert slot_map["time_range"].value == "2026-05-11"
assert "时间为 2026-05-11" in response.review_payload.intent_summary
def test_user_agent_guides_riding_fare_as_transport_expense() -> None:
session_factory = build_session_factory()
with session_factory() as db:
message = "业务发生时间:2026-03-04送客户去林萃小区办事请报销乘车费用"
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
)
)
response = UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
tool_payload={"draft_only": True},
)
)
assert response.review_payload is not None
slot_map = {item.key: item for item in response.review_payload.slot_cards}
assert slot_map["expense_type"].value == "交通费"
assert slot_map["expense_type"].normalized_value == "transport"
assert slot_map["time_range"].value == "2026-03-04"
assert slot_map["reason"].value == "送客户去林萃小区办事,请报销乘车费用"
assert "业务发生时间" not in slot_map["reason"].raw_value
assert "“交通费”" in response.review_payload.intent_summary
def test_user_agent_does_not_treat_draft_saved_message_as_precheck_risk_for_transport() -> None:
session_factory = build_session_factory()
with session_factory() as db:
message = "业务发生时间:2026-03-04送客户去林萃小区办事打车花了32元请报销乘车费用"
context_json = {
"name": "赵六",
"attachment_names": ["didi-trip.png"],
"attachment_count": 1,
"ocr_documents": [
{
"filename": "didi-trip.png",
"summary": "滴滴出行 支付金额 32 元",
"text": "滴滴出行 支付金额 32 元",
"document_type": "taxi_receipt",
"scene_code": "transport",
"document_fields": [
{"key": "amount", "label": "支付金额", "value": "32.00"},
],
"warnings": [],
}
],
}
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
response = UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={
"draft_only": True,
"claim_id": "claim-1",
"claim_no": "EXP-202603-001",
"status": "draft",
"message": (
"已创建报销草稿 EXP-202603-001当前状态为 draft。"
"你可以继续补充费用明细、客户单位和票据附件。"
),
},
)
)
assert response.review_payload is not None
assert response.review_payload.can_proceed is True
assert "客户名称" not in response.review_payload.missing_slots
assert "参与人员" not in response.review_payload.missing_slots
assert "票据附件" not in response.review_payload.missing_slots
risk_text = "\n".join(
f"{item.title}\n{item.content}" for item in response.review_payload.risk_briefs
)
assert "AI预审未通过" not in risk_text
assert "已创建报销草稿" not in risk_text
def test_user_agent_attachment_only_upload_uses_generic_scene_reason_without_fabrication() -> None:
session_factory = build_session_factory()
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query="我上传了 1 份票据,请结合附件名称给出报销建议并尽量生成草稿。",
user_id="pytest",