feat: 增强差旅报销审核流程与票据智能推理
优化本体解析和编排器的差旅场景处理能力,完善报销单草稿 保存和费用明细同步逻辑,前端报销创建页面增加行程推理和 票据审核交互,新增助手会话快照工具函数,补充单元测试。
This commit is contained in:
@@ -202,7 +202,7 @@ def test_conversation_hydration_does_not_reuse_review_type_for_fresh_expense_pro
|
||||
|
||||
fresh_context = service.hydrate_context_json(
|
||||
conversation=conversation,
|
||||
context_json={},
|
||||
context_json={"draft_claim_id": "claim-old"},
|
||||
message="业务发生时间:2026-02-20 至 2026-02-23,去上海支持上海电力部署项目,申请报销",
|
||||
)
|
||||
continued_context = service.hydrate_context_json(
|
||||
@@ -217,3 +217,183 @@ def test_conversation_hydration_does_not_reuse_review_type_for_fresh_expense_pro
|
||||
assert fresh_context["conversation_state"]["review_form_values"]["expense_type"] == "差旅费"
|
||||
assert continued_context["draft_claim_id"] == "claim-old"
|
||||
assert continued_context["review_form_values"]["expense_type"] == "差旅费"
|
||||
|
||||
|
||||
def test_orchestrator_history_query_filters_location_time_and_returns_real_amount(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
employee = Employee(
|
||||
id="emp-history-query",
|
||||
employee_no="E9020",
|
||||
name="张三",
|
||||
email="history-query@example.com",
|
||||
)
|
||||
beijing_claim = ExpenseClaim(
|
||||
id="claim-history-beijing",
|
||||
claim_no="EXP-202506-001",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason="去北京支持客户项目",
|
||||
location="北京",
|
||||
amount=Decimal("321.45"),
|
||||
currency="CNY",
|
||||
invoice_count=2,
|
||||
occurred_at=datetime(2025, 6, 18, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2025, 6, 19, 10, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
approval_stage="已入账",
|
||||
)
|
||||
shanghai_claim = ExpenseClaim(
|
||||
id="claim-history-shanghai",
|
||||
claim_no="EXP-202507-001",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason="去上海支持项目",
|
||||
location="上海",
|
||||
amount=Decimal("888.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2025, 7, 8, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2025, 7, 9, 10, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
approval_stage="已入账",
|
||||
)
|
||||
current_year_claim = ExpenseClaim(
|
||||
id="claim-history-beijing-current",
|
||||
claim_no="EXP-202601-001",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason="去北京支持年度项目",
|
||||
location="北京",
|
||||
amount=Decimal("666.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 1, 8, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 1, 9, 10, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
approval_stage="已入账",
|
||||
)
|
||||
db.add_all([employee, beijing_claim, shanghai_claim, current_year_claim])
|
||||
db.commit()
|
||||
|
||||
response = OrchestratorService(db).run(
|
||||
OrchestratorRequest(
|
||||
source="user_message",
|
||||
user_id="history-query@example.com",
|
||||
message="我去年去北京报销的单据",
|
||||
context_json={
|
||||
"client_now_iso": "2026-05-21T04:00:00.000Z",
|
||||
"client_timezone_offset_minutes": -480,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
query_payload = response.result["query_payload"]
|
||||
assert response.status == "succeeded"
|
||||
assert response.trace_summary.scenario == "expense"
|
||||
assert response.trace_summary.intent == "query"
|
||||
assert query_payload["record_count"] == 1
|
||||
assert query_payload["total_amount"] == 321.45
|
||||
assert [item["claim_no"] for item in query_payload["records"]] == ["EXP-202506-001"]
|
||||
assert "321.45" in response.result["answer"]
|
||||
|
||||
|
||||
def test_orchestrator_expense_preview_does_not_persist_claim_before_user_action(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
employee = Employee(
|
||||
employee_no="E9030",
|
||||
name="预览员工",
|
||||
email="preview-orchestrator@example.com",
|
||||
)
|
||||
db.add(employee)
|
||||
db.commit()
|
||||
|
||||
response = OrchestratorService(db).run(
|
||||
OrchestratorRequest(
|
||||
source="user_message",
|
||||
user_id="preview-orchestrator@example.com",
|
||||
message="业务发生时间:2026-03-04,打车去客户现场,交通费32元,请帮我看看怎么报",
|
||||
context_json={
|
||||
"name": "预览员工",
|
||||
"user_input_text": "业务发生时间:2026-03-04,打车去客户现场,交通费32元,请帮我看看怎么报",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
user_claims = [
|
||||
claim
|
||||
for claim in db.query(ExpenseClaim).all()
|
||||
if claim.employee_name == "预览员工"
|
||||
]
|
||||
assert response.status == "succeeded"
|
||||
assert response.result.get("review_payload") is not None
|
||||
assert response.result.get("draft_payload") is None
|
||||
assert "尚未保存为草稿" in response.result["answer"]
|
||||
assert user_claims == []
|
||||
|
||||
|
||||
def test_orchestrator_prompts_scene_choices_before_review_for_fresh_ambiguous_expense(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
service = AgentConversationService(db)
|
||||
conversation = service.get_or_create_conversation(
|
||||
conversation_id="conv-scene-choice",
|
||||
user_id="emp-scene-choice@example.com",
|
||||
source="user_message",
|
||||
context_json={
|
||||
"session_type": "expense",
|
||||
"draft_claim_id": "claim-old",
|
||||
"review_form_values": {
|
||||
"expense_type": "差旅费",
|
||||
"business_location": "北京",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
response = OrchestratorService(db).run(
|
||||
OrchestratorRequest(
|
||||
source="user_message",
|
||||
user_id="emp-scene-choice@example.com",
|
||||
conversation_id=conversation.conversation_id,
|
||||
message="业务发生时间:2026-02-20 至 2026-02-23,去上海支持上海电力部署项目,申请报销",
|
||||
context_json={
|
||||
"session_type": "expense",
|
||||
"draft_claim_id": "claim-old",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
result = response.result
|
||||
assert response.status == "succeeded"
|
||||
assert result.get("review_payload") is None
|
||||
assert result.get("draft_payload") is None
|
||||
assert "请先在下面选择报销场景" in result["answer"]
|
||||
assert [item["label"] for item in result["suggested_actions"][:3]] == ["差旅费", "交通费", "住宿费"]
|
||||
|
||||
Reference in New Issue
Block a user