feat: 增加差旅报销标准测算和财务终审流程

新增差旅报销测算接口及 Spreadsheet 规则解析,审批流程拆分
直属领导审批与财务终审两阶段并细分权限,修复 PDF 文本层
缺失时自动回退 OCR,提交后清理关联会话,前端适配审批流
交互并补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-21 09:28:33 +08:00
parent 002bf4f756
commit 8f65661809
43 changed files with 4366 additions and 410 deletions

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import pytest
from openpyxl import Workbook, load_workbook
from sqlalchemy import create_engine
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
@@ -24,11 +24,14 @@ from app.core.agent_enums import (
)
from app.core.config import SERVER_DIR
from app.db.base import Base
from app.models.agent_asset import AgentAsset
from app.models.employee import Employee
from app.schemas.agent_asset import (
AgentAssetCreate,
AgentAssetReviewCreate,
AgentAssetVersionCreate,
)
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
from app.services.agent_asset_spreadsheet import (
COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
@@ -41,6 +44,7 @@ from app.services.agent_runs import AgentRunService
from app.services.audit import AuditLogService
from app.services.expense_rule_runtime import ExpenseRuleRuntimeService
from app.services.settings import OnlyOfficeRuntimeConfig
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
@pytest.fixture(autouse=True)
@@ -618,6 +622,126 @@ def test_agent_asset_service_returns_travel_policy_rule_detail() -> None:
assert "住宿标准、飞机舱位和火车席别" in str(detail.current_version_content)
def test_expense_rule_runtime_reads_amount_standards_from_travel_spreadsheet() -> None:
with build_session() as db:
AgentAssetService(db).list_assets(asset_type=AgentAssetType.RULE.value)
travel_spreadsheet_rule = db.scalar(
select(AgentAsset).where(AgentAsset.code == COMPANY_TRAVEL_EXPENSE_RULE_CODE)
)
assert travel_spreadsheet_rule is not None
travel_spreadsheet_rule.status = AgentAssetStatus.REVIEW.value
db.commit()
catalog = ExpenseRuleRuntimeService(db).load_catalog()
assert catalog.travel_policy is not None
assert catalog.travel_policy.standard_rule_code == COMPANY_TRAVEL_EXPENSE_RULE_CODE
assert catalog.travel_policy.standard_rule_name == "公司差旅费报销规则"
assert catalog.travel_policy.hotel_city_limits["北京"]["mid"] == 450
assert catalog.travel_policy.hotel_city_limits["北京"]["junior"] == 450
assert catalog.travel_policy.hotel_city_limits["北京"]["manager"] == 500
assert catalog.travel_policy.allowance_limits["meal"]["直辖市/特区"] == 65
assert catalog.travel_policy.allowance_limits["meal"]["其他地区"] == 55
assert catalog.travel_policy.allowance_limits["total"]["其他地区"] == 90
assert catalog.travel_policy.transport_limits["senior"]["flight"] == 1
assert catalog.travel_policy.transport_limits["executive"]["train"] == 1
def test_travel_reimbursement_calculator_uses_finance_spreadsheet_amounts() -> None:
with build_session() as db:
db.add(
Employee(
employee_no="E9001",
name="测试员工",
email="traveler@example.com",
position="产品经理",
grade="P4",
)
)
db.commit()
result = TravelReimbursementCalculatorService(db).calculate(
TravelReimbursementCalculatorRequest(days=3, location="北京市朝阳区"),
CurrentUserContext(
username="traveler@example.com",
name="测试员工",
role_codes=[],
is_admin=False,
),
)
assert result.rule_name == "公司差旅费报销规则"
assert result.grade == "P4"
assert result.grade_band == "mid"
assert result.matched_city == "北京"
assert result.hotel_rate == 450
assert result.hotel_amount == 1350
assert result.allowance_region == "直辖市/特区"
assert result.total_allowance_rate == 100
assert result.allowance_amount == 300
assert result.total_amount == 1650
assert "住宿 450.00 × 3 天 + 补贴 100.00 × 3 天 = 1650.00" == result.formula_text
assert "参考可报销总金额为 1650.00 元" in result.summary_text
def test_travel_reimbursement_calculator_uses_other_region_for_known_unlisted_location() -> None:
with build_session() as db:
db.add(
Employee(
employee_no="E9002",
name="其他地区员工",
email="other-region@example.com",
position="产品经理",
grade="P4",
)
)
db.commit()
result = TravelReimbursementCalculatorService(db).calculate(
TravelReimbursementCalculatorRequest(days=2, location="吉林延边"),
CurrentUserContext(
username="other-region@example.com",
name="其他地区员工",
role_codes=[],
is_admin=False,
),
)
assert result.matched_city == "延边(其他地区)"
assert result.city_tier == "tier_3"
assert result.hotel_rate == 380
assert result.hotel_amount == 760
assert result.allowance_region == "其他地区"
assert result.total_allowance_rate == 90
assert result.allowance_amount == 180
assert result.total_amount == 940
def test_travel_reimbursement_calculator_rejects_unrecognized_location() -> None:
with build_session() as db:
db.add(
Employee(
employee_no="E9003",
name="无效地点员工",
email="invalid-location@example.com",
position="产品经理",
grade="P4",
)
)
db.commit()
with pytest.raises(ValueError, match="未识别为有效出差地区"):
TravelReimbursementCalculatorService(db).calculate(
TravelReimbursementCalculatorRequest(days=2, location="背景"),
CurrentUserContext(
username="invalid-location@example.com",
name="无效地点员工",
role_codes=[],
is_admin=False,
),
)
def test_agent_run_service_lists_seeded_trace_data() -> None:
with build_session() as db:
service = AgentRunService(db)