"""司库(资金)体系 · 阶段一 ORM 模型 — 管理会计OS 对齐 DDL: backend/scripts/treasury_ddl.sql(md5 见阶段1回报) G1 裁定:**不建物理外键**(entity_id 亦不建)——软链 + 应用层校验,覆盖说明见 DDL §F-2。 金额:FLOAT(万元),写库前 round(x,4),比较用容差 1e-4(禁用浮点等值比较)。 多租户:6 张表全部 entity_id NOT NULL(铁律)。 """ from sqlalchemy import ( Column, Integer, String, Float, DateTime, Date, JSON, Text, Index, UniqueConstraint, func, ) from app.database import Base class TreasuryAccount(Base): """司库·资金账户主数据(统一管理:台账唯一入口)""" __tablename__ = "treasury_accounts" __table_args__ = ( UniqueConstraint("entity_id", "account_no", name="uq_tacct_entity_no"), Index("ix_tacct_entity", "entity_id", "status"), Index("ix_tacct_subject", "entity_id", "subject_code"), {"comment": "司库·资金账户主数据"}, ) id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False, comment="企业ID(多租户铁律,必带)") account_no = Column(String(64), nullable=False, comment="账号/虚拟账户号(掩码)") account_name = Column(String(100), nullable=False, comment="账户名") account_type = Column(String(20), nullable=False, comment="cash/bank/e_wallet/other") bank_name = Column(String(100), nullable=True) subject_code = Column(String(20), nullable=True, comment="对应科目 1001xx/1002xx") currency = Column(String(10), default="CNY") tier = Column(String(20), nullable=False, comment="hq/platform/grassroots(按账户性质判定)") tier_basis = Column(String(20), default="nature", comment="[D-1] tier 判定依据 nature/level/manual") owner_node_id = Column(Integer, nullable=True, comment="使用/保管部门标注 org_nodes.id(软链,不派生 tier)") open_balance = Column(Float, nullable=True, comment="期初余额(万元);未落定留 NULL") open_period = Column(String(20), nullable=True, comment="期初期间 YYYY-MM") is_pool_member = Column(Integer, default=0, comment="阶段三资金池用") status = Column(String(20), default="active", comment="active/inactive/closed") remark = Column(String(500), nullable=True) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) class TreasuryBalance(Base): """司库·账户头寸(日/期末)——余额 = 期初 + 流水;禁把流水净额当余额""" __tablename__ = "treasury_balances" __table_args__ = ( UniqueConstraint("account_id", "period", name="uq_tbal_acct_period"), Index("ix_tbal_entity_period", "entity_id", "period"), Index("ix_tbal_entity_status", "entity_id", "data_status"), {"comment": "司库·账户头寸(日/期末)"}, ) id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False) account_id = Column(Integer, nullable=False, comment="treasury_accounts.id(软链)") period = Column(String(20), nullable=False, comment="YYYY-MM-DD 日 / YYYY-MM 月末") balance = Column(Float, nullable=False, comment="账面余额(万元)") available = Column(Float, nullable=True, comment="可用余额(万元)(NULL=未采集)") frozen = Column(Float, default=0, comment="冻结/在途(万元)") source = Column(String(30), nullable=False, comment="ledger/import/manual/pool") # [P2-6] fail-safe:默认 estimated,verified 必须显式写入 data_status = Column(String(20), default="estimated", comment="verified/estimated;默认 estimated(fail-safe),estimated 禁 block") batch = Column(String(100), nullable=True, comment="导入批次指纹(幂等)") created_at = Column(DateTime, server_default=func.now()) class TreasuryControlRule(Base): """司库·资金内控规则库(P0 核心)""" __tablename__ = "treasury_control_rules" __table_args__ = ( UniqueConstraint("entity_id", "rule_code", name="uq_tcr_entity_code"), {"comment": "司库·资金内控规则库"}, ) id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False) rule_code = Column(String(20), nullable=False, comment="NP-001/AU-001/BL-001/LQ-001/...") rule_name = Column(String(100), nullable=False) rule_type = Column(String(30), nullable=False, comment="budget_required/authorization/dual_sign/liquidity/...") severity = Column(String(10), nullable=False, comment="block/warn") params = Column(JSON, nullable=True, comment="阈值参数(金额段/下限,不硬编码)") rule_version = Column(String(20), default="v1", comment="[调整2] 规则版本(写入 hits.detail)") enabled = Column(Integer, default=1) remark = Column(String(500), nullable=True) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) class TreasuryPayment(Base): """司库·支付单(资金内控闸门载体)""" __tablename__ = "treasury_payments" __table_args__ = ( UniqueConstraint("entity_id", "pay_no", name="uq_tpay_entity_no"), Index("ix_tpay_entity_status", "entity_id", "status"), Index("ix_tpay_daysplit", "entity_id", "pay_date", "to_name", "submitter"), Index("ix_tpay_acct", "entity_id", "from_account_id", "status"), {"comment": "司库·支付单(资金内控闸门)"}, ) id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False) pay_no = Column(String(50), nullable=False, comment="支付单号") from_account_id = Column(Integer, nullable=False, comment="付款账户(必须已登记)") to_name = Column(String(200), nullable=False, comment="收款方") to_account_no = Column(String(64), nullable=True) amount = Column(Float, nullable=False, comment="金额(万元)") currency = Column(String(10), default="CNY") purpose = Column(String(500), nullable=False, comment="用途(必填)") pay_type = Column(String(30), nullable=True, comment="货款/费用/税费/薪酬/融资/内部划转") pay_date = Column(Date, nullable=True, comment="业务付款日(拆单累计按此自然日归集)") budget_plan_id = Column(Integer, nullable=True, comment="关联 budget_plans.id(NP-001 首选判据)") kpi_id = Column(Integer, nullable=True) related_plan_id = Column(Integer, nullable=True, comment="关联 cash_plans.id(只读复用)") status = Column(String(20), default="draft", comment="draft/precheck/pending_approval/approved/rejected/paid/cancelled") check_result = Column(String(20), nullable=True, comment="pass/blocked/warn") check_detail = Column(JSON, nullable=True, comment="[{rule_code,verdict,detail,matched_scope}]") required_approvals = Column(Integer, nullable=True, comment="[D-2] 本单所需审批级数(取最严档)") day_cum_amount = Column(Float, nullable=True, comment="[D-3] 同提交人×同收款方×同自然日累计(万元,含本单)") approvals = Column(JSON, nullable=True, comment="[{account,role,person_name,at,comment}]") dual_sign_effective = Column(Integer, nullable=True, comment="BL-001:0=形式双签(放行无效,标红)") is_waived = Column(Integer, default=0, comment="[D-4] 是否经豁免通道放行") submitter = Column(String(100), nullable=False, comment="[D-8] 提交人(拆单累计判据)") submitted_at = Column(DateTime, nullable=True, comment="[D-5] 提交时间") approved_at = Column(DateTime, nullable=True, comment="[D-5] 放行时间") paid_at = Column(DateTime, nullable=True) created_by = Column(String(100), nullable=False, comment="[D-8] 建单人(首版与 submitter 同值)") created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) class TreasuryControlHit(Base): """司库·内控规则命中留痕(append-only 审计)""" __tablename__ = "treasury_control_hits" __table_args__ = ( Index("ix_tch_target", "target_type", "target_id"), Index("ix_tch_entity_rule", "entity_id", "rule_code", "created_at"), Index("ix_tch_waived", "entity_id", "verdict", "created_at"), {"comment": "司库·内控规则命中留痕(append-only 审计)"}, ) id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False) target_type = Column(String(20), nullable=False, comment="payment/plan/internal_transfer") target_id = Column(Integer, nullable=False) rule_code = Column(String(20), nullable=False) verdict = Column(String(20), nullable=False, comment="pass/blocked/waived/warn") detail = Column(JSON, nullable=True, comment="[调整2] {rule_code,rule_version,matched_scope,threshold,cumulative,verdict_basis}") waived_by = Column(String(100), nullable=True, comment="豁免审批人(级别不得低于本单所需)") waived_reason = Column(String(500), nullable=True) created_at = Column(DateTime, server_default=func.now()) class TreasuryLimit(Base): """司库·分级授权矩阵(角色 × 金额段 × 审批级数;金额段左闭右开)""" __tablename__ = "treasury_limits" __table_args__ = ( UniqueConstraint("entity_id", "scope_type", "scope_value", "amount_from", name="uq_tlim_scope"), Index("ix_tlim_entity", "entity_id", "status"), {"comment": "司库·分级授权矩阵"}, ) id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False) scope_type = Column(String(20), nullable=False, comment="role/account/tier") scope_value = Column(String(50), nullable=False, comment="finance/ceo/.../account_id/tier") amount_from = Column(Float, nullable=False, comment="段下限(万元,含)") amount_to = Column(Float, nullable=True, comment="段上限(万元,不含),NULL=无上限") required_approvals = Column(Integer, default=1, comment="所需审批级数(≥2=双签)") approver_roles = Column(JSON, nullable=False, comment='["ceo","finance"](先复核后批准)') require_budget = Column(Integer, default=1) daily_cum_limit = Column(Float, nullable=True, comment="自主档日累计上限(=3×A)") status = Column(String(20), default="active") created_at = Column(DateTime, server_default=func.now(), comment="[D-9] 审计必需")