feat: 新增预算费控模型与报销审批流引擎

后端新增预算费控服务和报销单审批流模块,引入申请人费用画像
算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常
量和明细同步,更新差旅报销规则电子表格,前端新增预算分析
组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧
边栏和顶栏样式,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-27 17:31:27 +08:00
parent cbb98f4469
commit d4d5d40569
75 changed files with 5393 additions and 686 deletions

View File

@@ -13,6 +13,10 @@ from app.api.deps import get_db
from app.db.base import Base
from app.main import create_app
from app.models.budget import BudgetAllocation, BudgetTransaction
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.models.organization import OrganizationUnit
from app.models.role import Role
def build_session_factory() -> sessionmaker[Session]:
@@ -109,6 +113,27 @@ def seed_budget_allocations(db: Session) -> None:
db.commit()
def seed_market_budget_monitor(db: Session) -> tuple[Role, OrganizationUnit]:
role = Role(role_code="budget_monitor", name="预算监控员")
department = OrganizationUnit(
id="dept-market",
unit_code="MARKET-DEPT",
name="市场部",
unit_type="department",
)
employee = Employee(
employee_no="E-BUDGET-MARKET-P8",
name="赵预算",
email="budget-monitor@example.com",
grade="P8",
organization_unit=department,
roles=[role],
)
db.add_all([role, department, employee])
db.flush()
return role, department
def test_admin_can_view_all_budget_allocations_without_is_admin_header() -> None:
client, session_factory = build_client()
with session_factory() as db:
@@ -281,3 +306,69 @@ def test_budget_monitor_cannot_edit_and_admin_can_edit() -> None:
)
assert admin_response.status_code == 201
assert admin_response.json()["department_name"] == "销售部"
def test_budget_analysis_endpoint_is_limited_to_budget_roles() -> None:
client, session_factory = build_client()
with session_factory() as db:
seed_budget_allocations(db)
budget_role, market_department = seed_market_budget_monitor(db)
p6_budget_monitor = Employee(
employee_no="E-BUDGET-MARKET-P6",
name="低级预算",
email="p6-budget-monitor@example.com",
grade="P6",
organization_unit=market_department,
roles=[budget_role],
)
db.add(p6_budget_monitor)
db.flush()
claim = ExpenseClaim(
claim_no="APP-BUDGET-ANALYSIS-001",
employee_id=p6_budget_monitor.id,
employee_name="低级预算",
department_id="dept-market",
department_name="市场部",
project_code=None,
expense_type="travel_application",
reason="客户现场交付预算申请",
location="上海",
amount=Decimal("12000.00"),
currency="CNY",
invoice_count=0,
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
ordinary_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/budget-analysis",
headers={
"x-auth-username": "zhangsan@example.com",
"x-auth-role-codes": "employee",
},
)
monitor_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/budget-analysis",
headers={
"x-auth-username": "budget-monitor@example.com",
"x-auth-role-codes": "budget_monitor",
},
)
p6_monitor_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/budget-analysis",
headers={
"x-auth-username": "p6-budget-monitor@example.com",
"x-auth-role-codes": "budget_monitor",
},
)
assert ordinary_response.status_code == 403
assert p6_monitor_response.status_code == 403
assert monitor_response.status_code == 200
assert Decimal(monitor_response.json()["metrics"]["claim_amount_ratio"]) == Decimal("24.00")