feat: 优化差旅报销预审流程与个人工作台 UI 体系
- 完善 user_agent_application 申请差旅报销预审槽位与消息组装 - 增强预算助理报告与风险建议卡片交互 - 重构登录页视觉样式与移动端响应式适配 - 优化个人工作台、文档中心、政策中心、员工管理等页面布局 - 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型 - 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
This commit is contained in:
@@ -165,6 +165,32 @@ def test_validate_claim_for_submission_still_requires_location_for_travel_claim(
|
||||
assert any("缺少地点" in item for item in issues)
|
||||
|
||||
|
||||
def test_validate_claim_for_submission_does_not_require_optional_ride_receipt() -> None:
|
||||
service = ExpenseClaimService.__new__(ExpenseClaimService)
|
||||
claim = build_claim(expense_type="transport", location="待补充")
|
||||
claim.invoice_count = 0
|
||||
claim.items[0].item_type = "ride_ticket"
|
||||
claim.items[0].invoice_id = ""
|
||||
|
||||
issues = service._validate_claim_for_submission(claim)
|
||||
|
||||
assert "票据附件数量不足" not in issues
|
||||
assert not any("缺少票据标识" in item for item in issues)
|
||||
|
||||
|
||||
def test_validate_claim_for_submission_still_requires_hotel_receipt() -> None:
|
||||
service = ExpenseClaimService.__new__(ExpenseClaimService)
|
||||
claim = build_claim(expense_type="hotel", location="北京")
|
||||
claim.invoice_count = 0
|
||||
claim.items[0].item_type = "hotel_ticket"
|
||||
claim.items[0].invoice_id = ""
|
||||
|
||||
issues = service._validate_claim_for_submission(claim)
|
||||
|
||||
assert "票据附件数量不足" in issues
|
||||
assert any("缺少票据标识" in item for item in issues)
|
||||
|
||||
|
||||
def test_save_or_submit_preview_does_not_create_claim_without_explicit_action() -> None:
|
||||
user_id = "preview-only@example.com"
|
||||
message = "业务发生时间:2026-03-04,打车去客户现场,交通费32元,请帮我看看怎么报"
|
||||
@@ -342,6 +368,80 @@ def test_upsert_draft_from_ontology_persists_linked_application_context() -> Non
|
||||
assert link_flag["application_detail"]["application_reason"] == "支撑国网仿生产环境部署"
|
||||
|
||||
|
||||
def test_upsert_linked_application_draft_without_receipts_has_no_placeholder_item() -> None:
|
||||
user_id = "linked-application-no-receipt@example.com"
|
||||
message = (
|
||||
"报销类型:差旅费\n"
|
||||
"关联申请单:AP-202606-001 / 支撑国网仿生产服务器部署 / 2026-02-20 至 2026-02-23 / 上海 / ¥3,000\n"
|
||||
"报销票据:草稿生成后在详情中上传"
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E5104",
|
||||
name="关联员工",
|
||||
email=user_id,
|
||||
grade="P5",
|
||||
)
|
||||
db.add(employee)
|
||||
db.commit()
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query=message,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
result = ExpenseClaimService(db).upsert_draft_from_ontology(
|
||||
run_id=ontology.run_id,
|
||||
user_id=user_id,
|
||||
message=message,
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"name": "关联员工",
|
||||
"user_input_text": message,
|
||||
"review_action": "save_draft",
|
||||
"review_form_values": {
|
||||
"expense_type": "差旅费",
|
||||
"amount": "¥3,000",
|
||||
"reason": "支撑国网仿生产服务器部署",
|
||||
"location": "上海",
|
||||
"business_location": "上海",
|
||||
"time_range": "2026-02-20 至 2026-02-23",
|
||||
"business_time": "2026-02-20 至 2026-02-23",
|
||||
"application_claim_id": "application-linked-no-receipt",
|
||||
"application_claim_no": "AP-202606-001",
|
||||
"application_reason": "支撑国网仿生产服务器部署",
|
||||
"application_location": "上海",
|
||||
"application_amount": "3000",
|
||||
"application_amount_label": "¥3,000",
|
||||
"application_business_time": "2026-02-20 至 2026-02-23",
|
||||
},
|
||||
"expense_scene_selection": {
|
||||
"expense_type": "travel",
|
||||
"application_claim_id": "application-linked-no-receipt",
|
||||
"application_claim_no": "AP-202606-001",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
claim = db.get(ExpenseClaim, result["claim_id"])
|
||||
assert claim is not None
|
||||
assert claim.expense_type == "travel"
|
||||
assert claim.reason == "支撑国网仿生产服务器部署"
|
||||
assert claim.location == "上海"
|
||||
assert claim.amount == Decimal("0.00")
|
||||
assert claim.invoice_count == 0
|
||||
assert claim.items == []
|
||||
link_flag = next(
|
||||
flag
|
||||
for flag in claim.risk_flags_json
|
||||
if isinstance(flag, dict) and flag.get("source") == "application_link"
|
||||
)
|
||||
assert link_flag["application_claim_no"] == "AP-202606-001"
|
||||
assert link_flag["application_detail"]["application_amount"] == "3000"
|
||||
|
||||
|
||||
def test_unsaved_conversation_expires_after_retention_but_saved_conversation_stays() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentConversationService(db)
|
||||
@@ -2165,7 +2265,7 @@ def test_pre_review_claim_records_ai_result_without_submitting() -> None:
|
||||
|
||||
assert reviewed is not None
|
||||
assert reviewed.status == "draft"
|
||||
assert reviewed.approval_stage == "AI预审"
|
||||
assert reviewed.approval_stage == "待提交"
|
||||
assert reviewed.submitted_at is None
|
||||
pre_review_flag = next(
|
||||
flag
|
||||
@@ -3098,6 +3198,93 @@ def test_executive_can_delete_submitted_claim() -> None:
|
||||
assert db.get(ExpenseClaim, claim_id) is None
|
||||
|
||||
|
||||
def test_direct_manager_cannot_delete_application_claim() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="manager-delete-application@example.com",
|
||||
name="李经理",
|
||||
role_codes=["manager"],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
employee_no="E-APP-DEL-MANAGER",
|
||||
name="李经理",
|
||||
email="manager-delete-application@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E-APP-DEL-EMP",
|
||||
name="张三",
|
||||
email="zhangsan-application-delete@example.com",
|
||||
manager=manager,
|
||||
)
|
||||
db.add_all([manager, employee])
|
||||
db.flush()
|
||||
claim = ExpenseClaim(
|
||||
claim_no="APP-DEL-MANAGER-101",
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code=None,
|
||||
expense_type="travel_application",
|
||||
reason="差旅申请",
|
||||
location="上海",
|
||||
amount=Decimal("1200.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 11, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
claim_id = claim.id
|
||||
|
||||
with pytest.raises(ValueError, match="申请单只有系统管理员可以删除"):
|
||||
ExpenseClaimService(db).delete_claim(claim_id, current_user)
|
||||
|
||||
assert db.get(ExpenseClaim, claim_id) is not None
|
||||
|
||||
|
||||
def test_admin_can_delete_application_claim() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="superadmin",
|
||||
name="系统管理员",
|
||||
role_codes=["manager"],
|
||||
is_admin=True,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
claim = ExpenseClaim(
|
||||
claim_no="APP-DEL-ADMIN-101",
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code=None,
|
||||
expense_type="travel_application",
|
||||
reason="差旅申请",
|
||||
location="上海",
|
||||
amount=Decimal("1200.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 11, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
claim_id = claim.id
|
||||
|
||||
deleted = ExpenseClaimService(db).delete_claim(claim_id, current_user)
|
||||
|
||||
assert deleted is not None
|
||||
assert deleted.claim_no == "APP-DEL-ADMIN-101"
|
||||
assert db.get(ExpenseClaim, claim_id) is None
|
||||
|
||||
|
||||
def test_executive_cannot_delete_archived_claim() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="executive-archive-delete@example.com",
|
||||
|
||||
Reference in New Issue
Block a user