Files
cma-management/backend/app/models/treasury.py
Hermes CI Fix e5eb092d20 feat(cma): treasury stage-1 MVP - migration, models, endpoints, rules, 2 pages
G1 review fixes P0-1/P0-2/P1-3/P1-4/P1-5/P2-6/P2-7/P2-8 applied:
- up() filters comment lines then asserts missing_tables == [] (CREATE hits 6/6)
- created counter = new-object diff via information_schema (idempotent second run = 0)
- down() drops columns per-column, dumps via write-path creds + fail-closed
- seeds expanded to 8 rules + 6 limits; data_status DEFAULT estimated
- no physical FK (app-layer checks), amount FLOAT wan-yuan, round(x,4)

Scope: 6 treasury_* tables + cash_plans 4 added columns; cash.py untouched.
Not pushed: deploy is approval-gated (de851257).
2026-09-15 20:02:56 +08:00

179 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""司库(资金)体系 · 阶段一 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] 审计必需")