Files
X-Financial/server/src/app/models/financial_record.py
caoxiaozhu 50b1c3f9a9 feat: 增强规则资产管理与审计页面运行时调试
后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险
规则生成模板执行和员工数据模型字段,知识库 RAG 增强本
地回退和文档提取能力,清理旧风险规则文件统一由生成引擎
管理,前端审计页面增加运行时调试面板和规则资产编辑交互,
补充单元测试覆盖。
2026-05-24 21:44:17 +08:00

148 lines
7.1 KiB
Python

from __future__ import annotations
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Numeric, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from app.db.base_class import Base
class ExpenseClaim(Base):
__tablename__ = "expense_claims"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
claim_no: Mapped[str] = mapped_column(String(50), unique=True, index=True)
employee_id: Mapped[str | None] = mapped_column(
ForeignKey("employees.id"), nullable=True, index=True
)
employee_name: Mapped[str] = mapped_column(String(100), 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)
project_code: Mapped[str | None] = mapped_column(String(50), nullable=True)
expense_type: Mapped[str] = mapped_column(String(50), index=True)
reason: Mapped[str] = mapped_column(Text())
location: Mapped[str] = mapped_column(String(100))
amount: Mapped[Decimal] = mapped_column(Numeric(12, 2))
currency: Mapped[str] = mapped_column(String(10), default="CNY")
invoice_count: Mapped[int] = mapped_column(Integer, default=0)
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
submitted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, index=True
)
status: Mapped[str] = mapped_column(String(30), index=True)
approval_stage: Mapped[str | None] = mapped_column(String(50), nullable=True)
risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
hermes_scanned_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
hermes_risk_flag: Mapped[bool] = mapped_column(Boolean, default=False, index=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()
)
employee = relationship("Employee", foreign_keys=[employee_id])
items = relationship(
"ExpenseClaimItem",
back_populates="claim",
cascade="all, delete-orphan",
order_by="asc(ExpenseClaimItem.item_date)",
)
@property
def employee_position(self) -> str | None:
return str(self.employee.position).strip() if self.employee is not None and self.employee.position else None
@property
def employee_grade(self) -> str | None:
return str(self.employee.grade).strip() if self.employee is not None and self.employee.grade else None
@property
def manager_name(self) -> str | None:
if self.employee is None:
return None
if self.employee.manager is not None and self.employee.manager.name:
return str(self.employee.manager.name).strip() or None
return None
@property
def role_labels(self) -> list[str]:
if self.employee is None or not self.employee.roles:
return []
return [str(role.name).strip() for role in sorted(self.employee.roles, key=lambda item: item.name) if role.name]
class ExpenseClaimItem(Base):
__tablename__ = "expense_claim_items"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
claim_id: Mapped[str] = mapped_column(ForeignKey("expense_claims.id"), index=True)
item_date: Mapped[date] = mapped_column(Date(), index=True)
item_type: Mapped[str] = mapped_column(String(50))
item_reason: Mapped[str] = mapped_column(Text())
item_location: Mapped[str] = mapped_column(String(100))
item_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2))
invoice_id: 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()
)
claim = relationship("ExpenseClaim", back_populates="items")
@property
def is_system_generated(self) -> bool:
return str(self.item_type or "").strip().lower() in {"travel_allowance"}
class AccountsReceivableRecord(Base):
__tablename__ = "accounts_receivable"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
receivable_no: Mapped[str] = mapped_column(String(50), unique=True, index=True)
customer_id: Mapped[str] = mapped_column(String(64), index=True)
customer_name: Mapped[str] = mapped_column(String(120), index=True)
contract_no: Mapped[str | None] = mapped_column(String(100), nullable=True)
invoice_no: Mapped[str | None] = mapped_column(String(100), nullable=True)
amount_receivable: Mapped[Decimal] = mapped_column(Numeric(12, 2))
amount_received: Mapped[Decimal] = mapped_column(Numeric(12, 2))
amount_outstanding: Mapped[Decimal] = mapped_column(Numeric(12, 2))
currency: Mapped[str] = mapped_column(String(10), default="CNY")
posting_date: Mapped[date] = mapped_column(Date(), index=True)
due_date: Mapped[date] = mapped_column(Date(), index=True)
aging_days: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(30), index=True)
risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
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()
)
class AccountsPayableRecord(Base):
__tablename__ = "accounts_payable"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
payable_no: Mapped[str] = mapped_column(String(50), unique=True, index=True)
vendor_id: Mapped[str] = mapped_column(String(64), index=True)
vendor_name: Mapped[str] = mapped_column(String(120), index=True)
invoice_no: Mapped[str | None] = mapped_column(String(100), nullable=True)
amount_payable: Mapped[Decimal] = mapped_column(Numeric(12, 2))
amount_paid: Mapped[Decimal] = mapped_column(Numeric(12, 2))
amount_outstanding: Mapped[Decimal] = mapped_column(Numeric(12, 2))
currency: Mapped[str] = mapped_column(String(10), default="CNY")
posting_date: Mapped[date] = mapped_column(Date(), index=True)
due_date: Mapped[date] = mapped_column(Date(), index=True)
aging_days: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(30), index=True)
risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
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()
)