feat: 细化差旅票据费用明细分类并自动计算出差补贴
将差旅费用明细拆分为火车票、机票、住宿票、乘车等细分类 型,根据票据字段自动生成行程/事由描述,结合规则引擎自 动计算出差补贴金额,前端适配费用明细编辑和差旅票据审 核交互,补充单元测试覆盖。
This commit is contained in:
@@ -1315,6 +1315,230 @@ def test_user_agent_review_payload_prefers_hotel_invoice_for_hotel_name() -> Non
|
||||
assert slot_map["merchant_name"].value == "北京中心酒店"
|
||||
|
||||
|
||||
def test_user_agent_review_payload_does_not_fill_hotel_name_from_train_ticket() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
query = "我去北京出差,先上传一张火车票,酒店发票还没上传,帮我生成差旅费报销草稿"
|
||||
context = {
|
||||
"name": "张三",
|
||||
"grade": "P4",
|
||||
"attachment_names": ["北京南站火车票.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "北京南站火车票.png",
|
||||
"document_type": "train_ticket",
|
||||
"scene_code": "travel",
|
||||
"scene_label": "差旅票据",
|
||||
"summary": "电子发票(铁路电子客票) 2026-03-04 广州南至北京南 G123 二等座 金额 560 元 中国铁路",
|
||||
"text": "电子发票(铁路电子客票) 2026-03-04 广州南至北京南 G123 二等座 票价 ¥560.00 中国铁路祝您旅途愉快",
|
||||
"avg_score": 0.95,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "金额", "value": "560"},
|
||||
{"key": "route", "label": "行程", "value": "广州南-北京南"},
|
||||
{"key": "date", "label": "业务发生时间", "value": "2026-03-04"},
|
||||
{"key": "merchant_name", "label": "商户", "value": "中国铁路"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query=query,
|
||||
user_id="pytest-train-only-hotel-name@example.com",
|
||||
context_json=context,
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest-train-only-hotel-name@example.com",
|
||||
message=query,
|
||||
ontology=ontology,
|
||||
context_json=context,
|
||||
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["merchant_name"].value == ""
|
||||
assert "酒店/商户" not in response.review_payload.missing_slots
|
||||
assert "酒店的报销票据待上传(必须)" in response.review_payload.missing_slots
|
||||
assert response.review_payload.can_proceed is False
|
||||
assert [item.action_type for item in response.review_payload.confirmation_actions if item.emphasis == "primary"] == [
|
||||
"save_draft"
|
||||
]
|
||||
assert "继续下一步" not in [item.label for item in response.review_payload.confirmation_actions]
|
||||
assert "酒店住宿发票/住宿清单(必须,当前待上传)" in response.answer
|
||||
assert "市内交通/乘车票据(非必须" in response.answer
|
||||
assert "只能保存为草稿" in response.answer or "保存为草稿" in response.answer
|
||||
assert "您的职级为:P4" in response.answer
|
||||
assert "去北京" in response.answer
|
||||
assert "已提交火车 560.00 元" in response.answer
|
||||
field_labels = [
|
||||
field.label
|
||||
for card in response.review_payload.document_cards
|
||||
for field in card.fields
|
||||
]
|
||||
assert "商户/酒店" not in field_labels
|
||||
|
||||
|
||||
def test_user_agent_review_payload_allows_next_step_when_only_optional_ride_receipt_is_missing() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
query = "我去北京出差,上传了火车票和酒店票,帮我生成差旅费报销草稿"
|
||||
context = {
|
||||
"name": "张三",
|
||||
"grade": "P4",
|
||||
"review_form_values": {"occurred_date": "2026-03-04"},
|
||||
"attachment_names": ["北京南站火车票.png", "北京酒店发票.png"],
|
||||
"attachment_count": 2,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "北京南站火车票.png",
|
||||
"document_type": "train_ticket",
|
||||
"scene_code": "travel",
|
||||
"scene_label": "差旅票据",
|
||||
"summary": "电子发票(铁路电子客票) 2026-03-04 广州南至北京南 G123 二等座 金额 560 元",
|
||||
"text": "电子发票(铁路电子客票) 2026-03-04 广州南至北京南 G123 二等座 票价 ¥560.00",
|
||||
"avg_score": 0.95,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "金额", "value": "560"},
|
||||
{"key": "route", "label": "行程", "value": "广州南-北京南"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
{
|
||||
"filename": "北京酒店发票.png",
|
||||
"document_type": "hotel_invoice",
|
||||
"summary": "北京中心酒店 住宿 1 晚 金额 450 元",
|
||||
"text": "北京中心酒店 住宿 1 晚 金额 450 元",
|
||||
"avg_score": 0.96,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "金额", "value": "450"},
|
||||
{"key": "merchant", "label": "酒店名称", "value": "北京中心酒店"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query=query,
|
||||
user_id="pytest-travel-optional-ride@example.com",
|
||||
context_json=context,
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest-travel-optional-ride@example.com",
|
||||
message=query,
|
||||
ontology=ontology,
|
||||
context_json=context,
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
assert response.review_payload.can_proceed is True
|
||||
assert response.review_payload.missing_slots == []
|
||||
receipt_brief = next(item for item in response.review_payload.risk_briefs if item.title == "差旅票据待补充")
|
||||
assert receipt_brief.level == "info"
|
||||
assert "市内交通/乘车票据可继续上传(非必须)" in receipt_brief.content
|
||||
assert "酒店的报销票据待上传(必须)" not in receipt_brief.content
|
||||
action_types = [item.action_type for item in response.review_payload.confirmation_actions]
|
||||
assert "save_draft" in action_types
|
||||
assert "next_step" in action_types
|
||||
assert "市内交通/乘车票据(非必须" in response.answer
|
||||
assert "也可以继续下一步" in response.answer
|
||||
|
||||
|
||||
def test_user_agent_review_payload_allows_next_step_after_required_travel_receipts_are_complete() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
query = "我去北京出差,上传了火车票、酒店票和打车票,帮我生成差旅费报销草稿"
|
||||
context = {
|
||||
"name": "张三",
|
||||
"grade": "P4",
|
||||
"review_form_values": {"occurred_date": "2026-03-04"},
|
||||
"attachment_names": ["北京南站火车票.png", "北京酒店发票.png", "北京打车票.png"],
|
||||
"attachment_count": 3,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "北京南站火车票.png",
|
||||
"document_type": "train_ticket",
|
||||
"scene_code": "travel",
|
||||
"scene_label": "差旅票据",
|
||||
"summary": "电子发票(铁路电子客票) 2026-03-04 广州南至北京南 G123 二等座 金额 560 元",
|
||||
"text": "电子发票(铁路电子客票) 2026-03-04 广州南至北京南 G123 二等座 票价 ¥560.00",
|
||||
"avg_score": 0.95,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "金额", "value": "560"},
|
||||
{"key": "route", "label": "行程", "value": "广州南-北京南"},
|
||||
{"key": "date", "label": "日期", "value": "2026-03-04"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
{
|
||||
"filename": "北京酒店发票.png",
|
||||
"document_type": "hotel_invoice",
|
||||
"summary": "北京中心酒店 住宿 1 晚 金额 450 元",
|
||||
"text": "北京中心酒店 住宿 1 晚 金额 450 元",
|
||||
"avg_score": 0.96,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "金额", "value": "450"},
|
||||
{"key": "merchant", "label": "酒店名称", "value": "北京中心酒店"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
{
|
||||
"filename": "北京打车票.png",
|
||||
"document_type": "taxi_receipt",
|
||||
"summary": "北京网约车 打车票 支付金额 32 元",
|
||||
"text": "北京网约车 打车票 支付金额 32 元",
|
||||
"avg_score": 0.94,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "支付金额", "value": "32"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query=query,
|
||||
user_id="pytest-travel-complete@example.com",
|
||||
context_json=context,
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest-travel-complete@example.com",
|
||||
message=query,
|
||||
ontology=ontology,
|
||||
context_json=context,
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
assert response.review_payload.can_proceed is True
|
||||
assert response.review_payload.missing_slots == []
|
||||
action_types = [item.action_type for item in response.review_payload.confirmation_actions]
|
||||
assert "save_draft" in action_types
|
||||
assert "next_step" in action_types
|
||||
assert not any(item.title == "差旅票据待补充" for item in response.review_payload.risk_briefs)
|
||||
assert "无需继续上传票据" in response.answer
|
||||
assert "当前信息已较完整" in response.answer
|
||||
|
||||
|
||||
def test_user_agent_review_payload_prechecks_taxi_amount_against_rule_standard() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
|
||||
Reference in New Issue
Block a user