feat: 完善报销单审批流程及退回原因追踪
新增直属领导审批通过接口和审批待办列表查询,报销单退回 支持原因码分类和审批环节标记,优化票据附件去重和路径 回退查找,前端新增退回原因对话框、审批收件箱和工作台 图标组件,补充工具函数和单元测试覆盖。
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import UTC, date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
@@ -110,6 +111,19 @@ def test_resolve_expense_type_maps_office_supplies_review_value_to_office() -> N
|
||||
assert expense_type == "office"
|
||||
|
||||
|
||||
def test_resolve_expense_type_maps_riding_fare_review_value_to_transport() -> None:
|
||||
expense_type = ExpenseClaimService._resolve_expense_type(
|
||||
[],
|
||||
context_json={
|
||||
"review_form_values": {
|
||||
"expense_type": "乘车费用"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert expense_type == "transport"
|
||||
|
||||
|
||||
def test_upsert_draft_from_ontology_defers_multi_document_association_choice() -> None:
|
||||
user_id = "zhangsan@example.com"
|
||||
|
||||
@@ -238,6 +252,48 @@ def test_upsert_draft_from_ontology_keeps_reason_missing_for_attachment_only_upl
|
||||
assert claim.reason == "待补充"
|
||||
|
||||
|
||||
def test_upsert_draft_from_ontology_strips_recognized_business_time_from_reason() -> None:
|
||||
user_id = "transport-time@example.com"
|
||||
message = "业务发生时间:2026-03-04,送客户去林萃小区办事,请报销乘车费用"
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E5004",
|
||||
name="赵六",
|
||||
email=user_id,
|
||||
)
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
claim = db.get(ExpenseClaim, result["claim_id"])
|
||||
assert claim is not None
|
||||
assert claim.occurred_at.date() == date(2026, 3, 4)
|
||||
assert claim.reason == "送客户去林萃小区办事,请报销乘车费用"
|
||||
assert len(claim.items) == 1
|
||||
assert claim.items[0].item_date == date(2026, 3, 4)
|
||||
assert claim.items[0].item_reason == "送客户去林萃小区办事,请报销乘车费用"
|
||||
assert "客户单位" not in result["message"]
|
||||
assert "票据附件" not in result["message"]
|
||||
assert "费用明细" not in result["message"]
|
||||
|
||||
|
||||
def test_upsert_draft_from_ontology_supports_link_or_create_for_multi_documents() -> None:
|
||||
user_id = "lisi@example.com"
|
||||
|
||||
@@ -348,6 +404,100 @@ def test_upsert_draft_from_ontology_supports_link_or_create_for_multi_documents(
|
||||
assert float(new_claim.amount) == 50.5
|
||||
|
||||
|
||||
def test_upsert_draft_from_ontology_updates_returned_claim_and_preserves_return_events() -> None:
|
||||
user_id = "returned-owner@example.com"
|
||||
return_flag = {
|
||||
"source": "manual_return",
|
||||
"return_event_id": "return-event-1",
|
||||
"message": "第一次退回:附件缺失。",
|
||||
"reason": "附件缺失。",
|
||||
"return_count": 1,
|
||||
"return_stage": "直属领导审批",
|
||||
"return_stage_key": "direct_manager",
|
||||
"risk_points": ["附件缺失或不清晰"],
|
||||
}
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E5004",
|
||||
name="赵六",
|
||||
email=user_id,
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
existing_claim = ExpenseClaim(
|
||||
claim_no="EXP-202605-012",
|
||||
employee_id=employee.id,
|
||||
employee_name="赵六",
|
||||
department_name="市场部",
|
||||
project_code=None,
|
||||
expense_type="transport",
|
||||
reason="原有交通报销",
|
||||
location="上海",
|
||||
amount=Decimal("20.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 13, tzinfo=UTC),
|
||||
status="returned",
|
||||
approval_stage="待提交",
|
||||
risk_flags_json=[return_flag],
|
||||
)
|
||||
existing_claim.items = [
|
||||
ExpenseClaimItem(
|
||||
claim_id=existing_claim.id,
|
||||
item_date=date(2026, 5, 13),
|
||||
item_type="transport",
|
||||
item_reason="原有交通报销",
|
||||
item_location="上海",
|
||||
item_amount=Decimal("20.00"),
|
||||
invoice_id="old-trip.png",
|
||||
)
|
||||
]
|
||||
db.add(existing_claim)
|
||||
db.commit()
|
||||
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我补充了交通票据,更新这张退回单据",
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
ontology.risk_flags = ["系统识别:票据金额待人工核对。"]
|
||||
|
||||
result = ExpenseClaimService(db).upsert_draft_from_ontology(
|
||||
run_id=ontology.run_id,
|
||||
user_id=user_id,
|
||||
message="我补充了交通票据,更新这张退回单据",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"name": "赵六",
|
||||
"draft_claim_id": existing_claim.id,
|
||||
"attachment_names": ["new-trip.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "new-trip.png",
|
||||
"summary": "滴滴出行 支付金额 32 元",
|
||||
"text": "滴滴出行 支付金额 32 元",
|
||||
"document_type": "taxi_receipt",
|
||||
"scene_code": "transport",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
db.refresh(existing_claim)
|
||||
assert result["claim_id"] == existing_claim.id
|
||||
assert existing_claim.status == "draft"
|
||||
assert "系统识别:票据金额待人工核对。" in existing_claim.risk_flags_json
|
||||
manual_returns = [
|
||||
flag
|
||||
for flag in list(existing_claim.risk_flags_json or [])
|
||||
if isinstance(flag, dict) and flag.get("source") == "manual_return"
|
||||
]
|
||||
assert manual_returns == [return_flag]
|
||||
|
||||
|
||||
def test_generate_claim_no_uses_max_suffix_instead_of_count() -> None:
|
||||
with build_session() as db:
|
||||
db.add_all(
|
||||
@@ -642,6 +792,44 @@ def test_delete_claim_item_removes_row_and_attachment_files(monkeypatch, tmp_pat
|
||||
assert not attachment_root.exists()
|
||||
|
||||
|
||||
def test_attachment_preview_resolves_legacy_filename_in_claim_item_directory(monkeypatch, tmp_path) -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-1",
|
||||
name="张三",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="transport", location="上海")
|
||||
claim.items[0].invoice_id = "legacy-ticket.pdf"
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
|
||||
attachment_dir = tmp_path / claim.id / claim.items[0].id
|
||||
attachment_dir.mkdir(parents=True)
|
||||
file_path = attachment_dir / "legacy-ticket.pdf"
|
||||
file_path.write_bytes(b"legacy-pdf-bytes")
|
||||
(attachment_dir / "legacy-ticket.pdf.meta.json").write_text(
|
||||
'{"file_name":"legacy-ticket.pdf","media_type":"application/pdf","previewable":true}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
payload = ExpenseClaimService(db).get_claim_item_attachment_preview_content(
|
||||
claim_id=claim.id,
|
||||
item_id=claim.items[0].id,
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
assert payload is not None
|
||||
resolved_path, media_type, filename = payload
|
||||
assert resolved_path == file_path
|
||||
assert media_type == "application/pdf"
|
||||
assert filename == "legacy-ticket.pdf"
|
||||
|
||||
|
||||
def test_submit_claim_runs_ai_review_and_routes_to_direct_manager() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-submit@example.com",
|
||||
@@ -677,6 +865,43 @@ def test_submit_claim_runs_ai_review_and_routes_to_direct_manager() -> None:
|
||||
assert submitted.submitted_at is not None
|
||||
|
||||
|
||||
def test_submit_claim_allows_returned_claim_to_be_resubmitted() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-submit@example.com",
|
||||
name="张三",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
employee_no="E7100",
|
||||
name="李经理",
|
||||
email="manager-returned@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E7101",
|
||||
name="张三",
|
||||
email="emp-submit@example.com",
|
||||
manager=manager,
|
||||
)
|
||||
claim = build_claim(expense_type="transport", location="上海")
|
||||
claim.employee = employee
|
||||
claim.employee_id = employee.id
|
||||
claim.status = "returned"
|
||||
claim.approval_stage = "待补充"
|
||||
claim.items[0].invoice_id = "taxi-ticket.png"
|
||||
db.add_all([manager, employee, claim])
|
||||
db.commit()
|
||||
|
||||
submitted = ExpenseClaimService(db).submit_claim(claim.id, current_user)
|
||||
|
||||
assert submitted is not None
|
||||
assert submitted.status == "submitted"
|
||||
assert submitted.approval_stage == "直属领导审批"
|
||||
assert submitted.submitted_at is not None
|
||||
|
||||
|
||||
def test_submit_claim_backfills_department_from_current_employee() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-dept@example.com",
|
||||
@@ -1327,7 +1552,377 @@ def test_privileged_user_can_return_and_delete_submitted_claim() -> None:
|
||||
assert db.get(ExpenseClaim, claim_id) is None
|
||||
|
||||
|
||||
def test_list_claims_allows_direct_manager_to_view_pending_claims_for_approval() -> None:
|
||||
def test_direct_manager_can_return_subordinate_claim_to_pending_submission() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="manager-return@example.com",
|
||||
name="李经理",
|
||||
role_codes=["manager"],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
employee_no="E8100",
|
||||
name="李经理",
|
||||
email="manager-return@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E8101",
|
||||
name="张三",
|
||||
email="zhangsan-return@example.com",
|
||||
manager=manager,
|
||||
)
|
||||
db.add_all([manager, employee])
|
||||
db.flush()
|
||||
claim = ExpenseClaim(
|
||||
claim_no="EXP-RET-201",
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-A",
|
||||
expense_type="transport",
|
||||
reason="交通报销",
|
||||
location="上海",
|
||||
amount=Decimal("66.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
claim_id = claim.id
|
||||
|
||||
returned = ExpenseClaimService(db).return_claim(claim_id, current_user, reason="请补充行程说明")
|
||||
|
||||
assert returned is not None
|
||||
assert returned.status == "returned"
|
||||
assert returned.approval_stage == "待提交"
|
||||
assert returned.submitted_at is None
|
||||
assert any(
|
||||
isinstance(flag, dict)
|
||||
and flag.get("source") == "manual_return"
|
||||
and flag.get("message") == "请补充行程说明"
|
||||
for flag in returned.risk_flags_json
|
||||
)
|
||||
|
||||
|
||||
def test_direct_manager_can_approve_subordinate_claim_to_finance_review() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="manager-approve@example.com",
|
||||
name="李经理",
|
||||
role_codes=["manager"],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
employee_no="E8110",
|
||||
name="李经理",
|
||||
email="manager-approve@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E8111",
|
||||
name="张三",
|
||||
email="zhangsan-approve@example.com",
|
||||
manager=manager,
|
||||
)
|
||||
db.add_all([manager, employee])
|
||||
db.flush()
|
||||
claim = ExpenseClaim(
|
||||
claim_no="EXP-APP-201",
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-A",
|
||||
expense_type="transport",
|
||||
reason="交通报销",
|
||||
location="上海",
|
||||
amount=Decimal("66.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
claim_id = claim.id
|
||||
|
||||
approved = ExpenseClaimService(db).approve_claim(
|
||||
claim_id,
|
||||
current_user,
|
||||
opinion="情况属实,同意报销。",
|
||||
)
|
||||
|
||||
assert approved is not None
|
||||
assert approved.status == "submitted"
|
||||
assert approved.approval_stage == "财务审批"
|
||||
assert approved.submitted_at is not None
|
||||
assert any(
|
||||
isinstance(flag, dict)
|
||||
and flag.get("source") == "manual_approval"
|
||||
and flag.get("event_type") == "expense_claim_approval"
|
||||
and flag.get("opinion") == "情况属实,同意报销。"
|
||||
and flag.get("previous_approval_stage") == "直属领导审批"
|
||||
and flag.get("next_approval_stage") == "财务审批"
|
||||
for flag in approved.risk_flags_json
|
||||
)
|
||||
|
||||
|
||||
def test_return_claim_rejects_already_returned_claim_without_adding_event() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="finance-returned@example.com",
|
||||
name="财务",
|
||||
role_codes=["finance"],
|
||||
is_admin=False,
|
||||
)
|
||||
return_flag = {
|
||||
"source": "manual_return",
|
||||
"return_event_id": "return-event-existing",
|
||||
"message": "请补充附件。",
|
||||
"return_count": 1,
|
||||
"return_stage": "直属领导审批",
|
||||
"return_stage_key": "direct_manager",
|
||||
}
|
||||
|
||||
with build_session() as db:
|
||||
claim = ExpenseClaim(
|
||||
claim_no="EXP-RET-202",
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-A",
|
||||
expense_type="transport",
|
||||
reason="交通报销",
|
||||
location="上海",
|
||||
amount=Decimal("66.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="returned",
|
||||
approval_stage="待提交",
|
||||
risk_flags_json=[return_flag],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
claim_id = claim.id
|
||||
|
||||
with pytest.raises(ValueError, match="无需重复退回"):
|
||||
ExpenseClaimService(db).return_claim(claim_id, current_user, reason="重复退回")
|
||||
|
||||
db.refresh(claim)
|
||||
manual_returns = [
|
||||
flag
|
||||
for flag in list(claim.risk_flags_json or [])
|
||||
if isinstance(flag, dict) and flag.get("source") == "manual_return"
|
||||
]
|
||||
assert manual_returns == [return_flag]
|
||||
|
||||
|
||||
def test_return_claim_records_each_return_event_with_stage_reason_and_counts() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="finance-return@example.com",
|
||||
name="财务复核",
|
||||
role_codes=["finance"],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
claim = ExpenseClaim(
|
||||
claim_no="EXP-RET-301",
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-A",
|
||||
expense_type="transport",
|
||||
reason="交通报销",
|
||||
location="上海",
|
||||
amount=Decimal("66.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
claim_id = claim.id
|
||||
|
||||
service = ExpenseClaimService(db)
|
||||
first_returned = service.return_claim(
|
||||
claim_id,
|
||||
current_user,
|
||||
reason="发票金额与明细金额不一致,请重新核对。",
|
||||
reason_codes=["invoice_mismatch", "business_explanation"],
|
||||
)
|
||||
assert first_returned is not None
|
||||
|
||||
first_returned.status = "submitted"
|
||||
first_returned.approval_stage = "财务审批"
|
||||
first_returned.submitted_at = datetime(2026, 5, 12, 11, 0, tzinfo=UTC)
|
||||
db.commit()
|
||||
|
||||
second_returned = service.return_claim(
|
||||
claim_id,
|
||||
current_user,
|
||||
reason="超标说明仍不完整,请补充制度例外依据。",
|
||||
reason_codes=["over_policy"],
|
||||
)
|
||||
|
||||
assert second_returned is not None
|
||||
return_events = [
|
||||
flag
|
||||
for flag in list(second_returned.risk_flags_json or [])
|
||||
if isinstance(flag, dict) and flag.get("source") == "manual_return"
|
||||
]
|
||||
assert len(return_events) == 2
|
||||
assert return_events[0]["return_count"] == 1
|
||||
assert return_events[0]["stage_return_count"] == 1
|
||||
assert return_events[0]["return_stage"] == "直属领导审批"
|
||||
assert return_events[0]["reason_codes"] == ["invoice_mismatch", "business_explanation"]
|
||||
assert return_events[0]["risk_points"] == ["票据类型/金额与明细不一致", "业务事由/地点/人员信息不完整"]
|
||||
assert return_events[0]["reason"] == "发票金额与明细金额不一致,请重新核对。"
|
||||
assert return_events[0]["operator_role_codes"] == ["finance"]
|
||||
assert return_events[1]["return_count"] == 2
|
||||
assert return_events[1]["stage_return_count"] == 1
|
||||
assert return_events[1]["return_stage"] == "财务审批"
|
||||
assert return_events[1]["risk_points"] == ["超出制度标准或缺少超标说明"]
|
||||
|
||||
|
||||
def test_submit_returned_claim_preserves_manual_return_events() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-submit-returned@example.com",
|
||||
name="张三",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
return_flag = {
|
||||
"source": "manual_return",
|
||||
"return_event_id": "return-event-submit",
|
||||
"message": "第一次退回:业务说明不完整。",
|
||||
"reason": "业务说明不完整。",
|
||||
"return_count": 1,
|
||||
"return_stage": "直属领导审批",
|
||||
"return_stage_key": "direct_manager",
|
||||
"risk_points": ["业务事由/地点/人员信息不完整"],
|
||||
}
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
employee_no="E8200",
|
||||
name="李经理",
|
||||
email="manager-submit-returned@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E8201",
|
||||
name="张三",
|
||||
email="emp-submit-returned@example.com",
|
||||
manager=manager,
|
||||
)
|
||||
claim = build_claim(expense_type="office", location="上海")
|
||||
claim.employee = employee
|
||||
claim.employee_id = employee.id
|
||||
claim.employee_name = "张三"
|
||||
claim.department_name = "市场部"
|
||||
claim.status = "returned"
|
||||
claim.approval_stage = "待提交"
|
||||
claim.risk_flags_json = [return_flag]
|
||||
db.add_all([manager, employee, claim])
|
||||
db.commit()
|
||||
|
||||
submitted = ExpenseClaimService(db).submit_claim(claim.id, current_user)
|
||||
|
||||
assert submitted is not None
|
||||
assert submitted.status == "submitted"
|
||||
assert submitted.approval_stage == "直属领导审批"
|
||||
assert any(
|
||||
isinstance(flag, dict)
|
||||
and flag.get("source") == "manual_return"
|
||||
and flag.get("return_event_id") == "return-event-submit"
|
||||
for flag in list(submitted.risk_flags_json or [])
|
||||
)
|
||||
|
||||
|
||||
def test_manager_personal_claims_exclude_subordinate_pending_approval_claims() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="manager-personal@example.com",
|
||||
name="李经理",
|
||||
role_codes=["manager"],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
employee_no="E8300",
|
||||
name="李经理",
|
||||
email="manager-personal@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E8301",
|
||||
name="张三",
|
||||
email="zhangsan-personal@example.com",
|
||||
manager=manager,
|
||||
)
|
||||
db.add_all([manager, employee])
|
||||
db.flush()
|
||||
db.add_all(
|
||||
[
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-MGR-OWN",
|
||||
employee_id=manager.id,
|
||||
employee_name="李经理",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-MGR",
|
||||
expense_type="office",
|
||||
reason="本人报销",
|
||||
location="上海",
|
||||
amount=Decimal("88.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="draft",
|
||||
approval_stage="待提交",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-MGR-SUB",
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-MGR",
|
||||
expense_type="transport",
|
||||
reason="下属待审批报销",
|
||||
location="上海",
|
||||
amount=Decimal("66.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 12, 11, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
service = ExpenseClaimService(db)
|
||||
personal_claims = service.list_claims(current_user)
|
||||
approval_claims = service.list_approval_claims(current_user)
|
||||
|
||||
assert [claim.claim_no for claim in personal_claims] == ["EXP-MGR-OWN"]
|
||||
assert [claim.claim_no for claim in approval_claims] == ["EXP-MGR-SUB"]
|
||||
|
||||
|
||||
def test_list_approval_claims_allows_direct_manager_to_view_pending_claims_for_approval() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="manager@example.com",
|
||||
name="李经理",
|
||||
@@ -1402,7 +1997,7 @@ def test_list_claims_allows_direct_manager_to_view_pending_claims_for_approval()
|
||||
)
|
||||
db.commit()
|
||||
|
||||
claims = ExpenseClaimService(db).list_claims(current_user)
|
||||
claims = ExpenseClaimService(db).list_approval_claims(current_user)
|
||||
|
||||
assert len(claims) == 1
|
||||
assert claims[0].claim_no == "EXP-MGR-201"
|
||||
|
||||
Reference in New Issue
Block a user