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:
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from dotenv import load_dotenv
|
||||
from app.database import init_db, get_db
|
||||
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json, ext_input_ledgers
|
||||
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json, ext_input_ledgers, treasury
|
||||
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
||||
from scripts.erp_sync import run_sync as run_erp_sync
|
||||
from app.auth_middleware import require_auth, require_auth_401
|
||||
@@ -99,6 +99,7 @@ app.include_router(data_classification.router)
|
||||
app.include_router(tax_compliance.router)
|
||||
app.include_router(verify.router)
|
||||
app.include_router(ext_input_ledgers.router) # 外部输入台账(M2 准入3 派生,2026-09-15)
|
||||
app.include_router(treasury.router) # 司库(资金)阶段一:账户池/头寸/内控闸门(2026-09-15)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
|
||||
@@ -8,6 +8,10 @@ from app.models.knowledge import KnowledgeEvent, KnowledgeSummary
|
||||
from app.models.driver_budget import DriverFactorTemplate, DriverFactorBudget
|
||||
from app.models.product_sales import ProductSales
|
||||
from app.models.ext_input_ledger import TaxFilingLog, HrMonthlyLedger, LicenceLedger
|
||||
from app.models.treasury import (
|
||||
TreasuryAccount, TreasuryBalance, TreasuryControlRule,
|
||||
TreasuryPayment, TreasuryControlHit, TreasuryLimit,
|
||||
)
|
||||
|
||||
|
||||
class Entity(Base):
|
||||
|
||||
@@ -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