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).
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""司库(资金)体系 · 阶段一 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] 审计必需")
|
||||
Reference in New Issue
Block a user