feat: 新增预算后端服务与差旅风险规则库

后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额
查询,清理旧生成规则文件并替换为按严重等级分类的差旅风
险规则库,优化认证权限和报销单访问策略,新增财务规则目
录和演示数据构建脚本,前端预算中心增加对话框交互,完善
审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-26 17:29:35 +08:00
parent e1e515ecae
commit e7bef0883d
85 changed files with 6443 additions and 1497 deletions

View File

@@ -3,6 +3,7 @@ from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersi
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
from app.models.approval import ApprovalRecord
from app.models.audit_log import AuditLog
from app.models.budget import BudgetAllocation, BudgetReservation, BudgetTransaction
from app.models.employee_change_log import EmployeeChangeLog
from app.models.employee import Employee
from app.models.financial_record import (
@@ -32,6 +33,9 @@ __all__ = [
"AgentToolCall",
"ApprovalRecord",
"AuditLog",
"BudgetAllocation",
"BudgetReservation",
"BudgetTransaction",
"Employee",
"EmployeeChangeLog",
"ExpenseClaim",

View File

@@ -0,0 +1,115 @@
from __future__ import annotations
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Any
from sqlalchemy import DateTime, ForeignKey, Index, Numeric, String, Text, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from app.db.base_class import Base
class BudgetAllocation(Base):
__tablename__ = "budget_allocations"
__table_args__ = (
UniqueConstraint(
"fiscal_year",
"period_key",
"department_id",
"cost_center",
"project_code",
"subject_code",
name="uq_budget_allocation_dimension",
),
Index("ix_budget_allocations_dimension", "fiscal_year", "period_key", "subject_code"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
budget_no: Mapped[str] = mapped_column(String(50), unique=True, index=True)
fiscal_year: Mapped[int] = mapped_column(index=True)
period_type: Mapped[str] = mapped_column(String(20), default="quarter", index=True)
period_key: Mapped[str] = mapped_column(String(30), index=True)
department_id: Mapped[str | None] = mapped_column(
ForeignKey("organization_units.id"), nullable=True, index=True
)
department_name: Mapped[str] = mapped_column(String(100), index=True)
cost_center: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
project_code: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
subject_code: Mapped[str] = mapped_column(String(50), index=True)
subject_name: Mapped[str] = mapped_column(String(100))
original_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=Decimal("0.00"))
adjusted_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=Decimal("0.00"))
status: Mapped[str] = mapped_column(String(30), default="active", index=True)
warning_threshold: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=Decimal("80.00"))
control_action: Mapped[str] = mapped_column(String(30), default="block")
description: Mapped[str | None] = mapped_column(Text(), nullable=True)
created_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
updated_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
transactions = relationship("BudgetTransaction", back_populates="allocation")
reservations = relationship("BudgetReservation", back_populates="allocation")
class BudgetReservation(Base):
__tablename__ = "budget_reservations"
__table_args__ = (
Index("ix_budget_reservations_source", "source_type", "source_id"),
Index("ix_budget_reservations_status", "allocation_id", "source_status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
reservation_no: Mapped[str] = mapped_column(String(50), unique=True, index=True)
allocation_id: Mapped[str] = mapped_column(ForeignKey("budget_allocations.id"), index=True)
source_type: Mapped[str] = mapped_column(String(40), index=True)
source_id: Mapped[str] = mapped_column(String(64), index=True)
source_no: Mapped[str] = mapped_column(String(80), index=True)
source_status: Mapped[str] = mapped_column(String(30), default="active", index=True)
amount: Mapped[Decimal] = mapped_column(Numeric(14, 2))
consumed_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=Decimal("0.00"))
released_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=Decimal("0.00"))
context_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
released_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
allocation = relationship("BudgetAllocation", back_populates="reservations")
transactions = relationship("BudgetTransaction", back_populates="reservation")
class BudgetTransaction(Base):
__tablename__ = "budget_transactions"
__table_args__ = (
Index("ix_budget_transactions_allocation_created", "allocation_id", "created_at"),
Index("ix_budget_transactions_source", "source_type", "source_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
transaction_no: Mapped[str] = mapped_column(String(50), unique=True, index=True)
allocation_id: Mapped[str] = mapped_column(ForeignKey("budget_allocations.id"), index=True)
reservation_id: Mapped[str | None] = mapped_column(
ForeignKey("budget_reservations.id"), nullable=True, index=True
)
source_type: Mapped[str] = mapped_column(String(40), index=True)
source_id: Mapped[str] = mapped_column(String(64), index=True)
source_no: Mapped[str] = mapped_column(String(80), index=True)
transaction_type: Mapped[str] = mapped_column(String(30), index=True)
amount: Mapped[Decimal] = mapped_column(Numeric(14, 2))
before_available_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2))
after_available_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2))
operator: Mapped[str | None] = mapped_column(String(100), nullable=True)
reason: Mapped[str | None] = mapped_column(Text(), nullable=True)
context_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
allocation = relationship("BudgetAllocation", back_populates="transactions")
reservation = relationship("BudgetReservation", back_populates="transactions")