feat(warning-p0a): 资金缺口阈值外置 + 复算对拍 + 讲人话(数字守恒) + 数据就绪核验
ci/woodpecker/push/main Pipeline failed

P0-A(任务 fullstack-20261017-warning-capability-build-p0):
- cash_thresholds.py:阈值迁 per-entity(复用 alert_rules.rule_type=cash_gap_threshold,
  默认 20/10 万不动;无配置/非法/异常一律回落默认,fail-safe 不改预警行为)
- cash_forecast_engine.py:阈值改从配置读取,响应新增 threshold_source 明写来源
- verify_cash_gap_replay.py:同输入双跑逐字段 diff=0 + 手算 3/3 + 端点逐字段对拍(--live)
- cash_gap_narrative.py:复用 ai_brief.call_deepseek,只写原因/建议;
  断言生成文本数字集合 ⊆ 规则层数字集合,违例即不带文案上线
- audit_warning_data_readiness.py:G0 只读核验(cma_reader SELECT only),
  行数/期间/枚举 + 数据质量缺口(期初全 0、负期末、单批次、无员工维度表)

未 push(push 即部署,须项目侧书面放行);未改生产库/未动 cron。
This commit is contained in:
wecom-fullstack
2026-10-17 23:33:51 +08:00
parent a52faa90a6
commit 885cfddbff
6 changed files with 1335 additions and 13 deletions
+42 -13
View File
@@ -13,9 +13,17 @@ if TYPE_CHECKING:
logger = logging.getLogger("cma.cash_forecast")
# 默认现金阈值(万元)
DEFAULT_CASH_WARNING = 20.0 # 黄灯 — 低于20万
DEFAULT_CASH_CRITICAL = 10.0 # 红灯 — 低于10万
# 默认现金阈值(万元)—— 单一事实源迁至 app/utils/cash_thresholds.py(G1,2026-10-17)
# 此处保留同名常量以兼容既有 import(app/api/cash.py 等);数值未变:黄 20 / 红 10。
from app.utils.cash_thresholds import ( # noqa: E402
DEFAULT_CRITICAL as _CFG_DEFAULT_CRITICAL,
DEFAULT_WARNING as _CFG_DEFAULT_WARNING,
get_cash_gap_thresholds,
describe_source as describe_cash_threshold_source,
)
DEFAULT_CASH_WARNING = _CFG_DEFAULT_WARNING # 黄灯 — 低于20万
DEFAULT_CASH_CRITICAL = _CFG_DEFAULT_CRITICAL # 红灯 — 低于10万
# 历史KPI编码映射(含候选编码,兼容F_*标准编码)
KPI_CODES = {
@@ -306,17 +314,27 @@ def forecast_cash_flow(
db: Session,
days: int = 30,
current_cash: Optional[float] = None,
warning_line: Optional[float] = None,
critical_line: Optional[float] = None,
) -> dict:
"""
预测未来30天现金流
算法:
1. 获取历史经营现金流、应收、应付趋势
2. 推算每日现金流入/流出
3. 生成每日预测值+置信区间
阈值(G1,2026-10-17):warning_line / critical_line 传 None 时取本账套配置
(alert_rules: cash_gap_threshold),未配置则默认 20/10 万;显式传入优先(便于复算对拍)。
"""
from app.models import KPIDefinition, KPIValue
if warning_line is None or critical_line is None:
cfg_warning, cfg_critical = get_cash_gap_thresholds(db, entity_id)
warning_line = cfg_warning if warning_line is None else warning_line
critical_line = cfg_critical if critical_line is None else critical_line
# 获取当前现金余额
if current_cash is None:
base_cash = get_current_cash_balance(db, entity_id)
@@ -375,9 +393,9 @@ def forecast_cash_flow(
upper = cash + std * 0.5
# 预警状态
if cash < DEFAULT_CASH_CRITICAL:
if cash < critical_line:
status = "red"
elif cash < DEFAULT_CASH_WARNING:
elif cash < warning_line:
status = "yellow"
else:
status = "green"
@@ -396,20 +414,20 @@ def forecast_cash_flow(
min_date = next(f["date"] for f in forecast if f["predicted_cash"] == min_cash)
suggestions = []
if min_cash < DEFAULT_CASH_CRITICAL:
if min_cash < critical_line:
suggestions.append({
"type": "critical",
"message": f"预计{min_date}现金余额降至{min_cash:.1f}万,低于警戒线{DEFAULT_CASH_CRITICAL}万",
"message": f"预计{min_date}现金余额降至{min_cash:.1f}万,低于警戒线{critical_line}万",
"actions": [
"立即催收大额应收账款",
"暂停非必要支出",
"准备短期融资安排",
]
})
elif min_cash < DEFAULT_CASH_WARNING:
elif min_cash < warning_line:
suggestions.append({
"type": "warning",
"message": f"预计{min_date}现金余额降至{min_cash:.1f}万,低于关注线{DEFAULT_CASH_WARNING}万",
"message": f"预计{min_date}现金余额降至{min_cash:.1f}万,低于关注线{warning_line}万",
"actions": [
"加快应收账款回款",
"控制采购付款节奏",
@@ -606,8 +624,8 @@ def forecast_cash_flow_with_plans(
db: Session,
days: int = 30,
current_cash: Optional[float] = None,
warning_line: float = DEFAULT_CASH_WARNING,
critical_line: float = DEFAULT_CASH_CRITICAL,
warning_line: Optional[float] = None,
critical_line: Optional[float] = None,
include_plans: bool = True,
) -> dict:
"""
@@ -616,11 +634,21 @@ def forecast_cash_flow_with_plans(
2. 叠加 cash_plans 的应收(收) / 应付(付)
3. 识别资金缺口日期(余额 < 警戒线)
4. 生成缺口前3天预警点
阈值来源(G1,2026-10-17):None → 本账套配置(alert_rules: cash_gap_threshold)
→ 未配置时默认 20/10 万;显式传入值优先(复算对拍用)。响应内 `threshold_source`
明写本次取值来源,便于页面标注与回执取证。
"""
from collections import defaultdict
from app.models import CashPlan
base = forecast_cash_flow(entity_id, db, days, current_cash)
if warning_line is None or critical_line is None:
cfg_warning, cfg_critical = get_cash_gap_thresholds(db, entity_id)
warning_line = cfg_warning if warning_line is None else warning_line
critical_line = cfg_critical if critical_line is None else critical_line
base = forecast_cash_flow(entity_id, db, days, current_cash,
warning_line=warning_line, critical_line=critical_line)
base_cash = base["base_cash"]
# 预算校准:本月经营现金流预算优先作为基线(万元/月)
@@ -805,6 +833,7 @@ def forecast_cash_flow_with_plans(
"days": days,
"warning_line": warning_line,
"critical_line": critical_line,
"threshold_source": describe_cash_threshold_source(db, entity_id),
"budget_monthly_ocf": budget_ocf,
"forecast": forecast,
"gap_dates": gap_dates,