feat: 增加差旅报销标准测算和财务终审流程
新增差旅报销测算接口及 Spreadsheet 规则解析,审批流程拆分 直属领导审批与财务终审两阶段并细分权限,修复 PDF 文本层 缺失时自动回退 OCR,提交后清理关联会话,前端适配审批流 交互并补充单元测试。
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user