feat: 重构报销单AI预审流程并添加平台风险规则引擎
- 将AI验审改为AI预审,高风险不再拦截而是随单流转给审批人复核 - 新增平台风险规则评估引擎,支持事由过短、票据异常、重复发票等多种评估器 - 用户上下文增加部门信息(department_name),认证流程同步关联组织架构 - 规则scenario_json改为中文标签(差旅/费用科目),统一场景分类 - 新增orchestrator审核流程测试用例 - 前端更新审计视图、差旅报销等相关页面
This commit is contained in:
@@ -11,6 +11,7 @@ from app.api.deps import CurrentUserContext
|
||||
from app.db.base import Base
|
||||
from app.models.employee import Employee
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.models.organization import OrganizationUnit
|
||||
from app.schemas.ontology import OntologyParseRequest
|
||||
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
|
||||
from app.schemas.reimbursement import ExpenseClaimItemCreate, ExpenseClaimItemUpdate
|
||||
@@ -76,6 +77,16 @@ def test_validate_claim_for_submission_allows_office_claim_without_location() ->
|
||||
assert not any("缺少地点" in item for item in issues)
|
||||
|
||||
|
||||
def test_validate_claim_for_submission_allows_transport_claim_without_location() -> None:
|
||||
service = ExpenseClaimService.__new__(ExpenseClaimService)
|
||||
claim = build_claim(expense_type="transport", location="待补充")
|
||||
|
||||
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_location_for_travel_claim() -> None:
|
||||
service = ExpenseClaimService.__new__(ExpenseClaimService)
|
||||
claim = build_claim(expense_type="travel", location="待补充")
|
||||
@@ -666,7 +677,54 @@ def test_submit_claim_runs_ai_review_and_routes_to_direct_manager() -> None:
|
||||
assert submitted.submitted_at is not None
|
||||
|
||||
|
||||
def test_submit_claim_blocks_high_risk_attachment_at_ai_review(monkeypatch, tmp_path) -> None:
|
||||
def test_submit_claim_backfills_department_from_current_employee() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-dept@example.com",
|
||||
name="张三",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
department = OrganizationUnit(
|
||||
unit_code="D7200",
|
||||
name="销售部",
|
||||
)
|
||||
manager = Employee(
|
||||
employee_no="E7200",
|
||||
name="李经理",
|
||||
email="manager-dept@example.com",
|
||||
)
|
||||
employee = Employee(
|
||||
employee_no="E7201",
|
||||
name="张三",
|
||||
email="emp-dept@example.com",
|
||||
organization_unit=department,
|
||||
manager=manager,
|
||||
)
|
||||
claim = build_claim(expense_type="transport", location="待补充")
|
||||
claim.employee = None
|
||||
claim.employee_id = None
|
||||
claim.employee_name = "张三"
|
||||
claim.department_id = None
|
||||
claim.department_name = "待补充"
|
||||
claim.items[0].item_location = "待补充"
|
||||
db.add_all([department, 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.department_id == department.id
|
||||
assert submitted.department_name == "销售部"
|
||||
assert submitted.approval_stage == "直属领导审批"
|
||||
|
||||
|
||||
def test_submit_claim_routes_high_risk_attachment_to_approval_with_review_flag(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
) -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-risk@example.com",
|
||||
name="张三",
|
||||
@@ -732,16 +790,19 @@ def test_submit_claim_blocks_high_risk_attachment_at_ai_review(monkeypatch, tmp_
|
||||
submitted = service.submit_claim(claim.id, current_user)
|
||||
|
||||
assert submitted is not None
|
||||
assert submitted.status == "supplement"
|
||||
assert submitted.approval_stage == "待补充"
|
||||
assert submitted.submitted_at is None
|
||||
assert submitted.status == "submitted"
|
||||
assert submitted.approval_stage == "直属领导审批"
|
||||
assert submitted.submitted_at is not None
|
||||
assert any(
|
||||
isinstance(flag, dict) and str(flag.get("source") or "").strip() == "submission_review"
|
||||
for flag in list(submitted.risk_flags_json or [])
|
||||
)
|
||||
|
||||
|
||||
def test_submit_claim_blocks_travel_route_mismatch_without_explanation(monkeypatch, tmp_path) -> None:
|
||||
def test_submit_claim_routes_travel_route_mismatch_to_approval_with_review_flag(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
) -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-travel@example.com",
|
||||
name="张三",
|
||||
@@ -876,8 +937,8 @@ def test_submit_claim_blocks_travel_route_mismatch_without_explanation(monkeypat
|
||||
submitted = service.submit_claim(claim.id, current_user)
|
||||
|
||||
assert submitted is not None
|
||||
assert submitted.status == "supplement"
|
||||
assert submitted.approval_stage == "待补充"
|
||||
assert submitted.status == "submitted"
|
||||
assert submitted.approval_stage == "直属领导审批"
|
||||
assert any(
|
||||
isinstance(flag, dict)
|
||||
and str(flag.get("source") or "").strip() == "submission_review"
|
||||
@@ -889,7 +950,10 @@ def test_submit_claim_blocks_travel_route_mismatch_without_explanation(monkeypat
|
||||
)
|
||||
|
||||
|
||||
def test_submit_claim_blocks_hotel_amount_over_travel_policy_without_explanation(monkeypatch, tmp_path) -> None:
|
||||
def test_submit_claim_routes_hotel_amount_over_travel_policy_to_approval_with_review_flag(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
) -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-hotel@example.com",
|
||||
name="张三",
|
||||
@@ -1024,8 +1088,8 @@ def test_submit_claim_blocks_hotel_amount_over_travel_policy_without_explanation
|
||||
submitted = service.submit_claim(claim.id, current_user)
|
||||
|
||||
assert submitted is not None
|
||||
assert submitted.status == "supplement"
|
||||
assert submitted.approval_stage == "待补充"
|
||||
assert submitted.status == "submitted"
|
||||
assert submitted.approval_stage == "直属领导审批"
|
||||
assert any(
|
||||
isinstance(flag, dict)
|
||||
and str(flag.get("source") or "").strip() == "submission_review"
|
||||
|
||||
Reference in New Issue
Block a user