feat(kpi): CMA保本点派生KPI F_BEP_REVENUE/COVER/GAP + 每日派生执行器 + 红黄绿静态阈值判定

- kpi_definitions entity1 新增3条派生KPI(456/457/458, formula/threshold/unit齐全)
- 新增 scripts/kpi_bep_deriver.py: 从 voucher_details 按财务口径聚合
  (5001销售收入/5602管理费 YTD月均, 渠补率政策84.61%) → 计算保本销售额/
  覆盖率/缺口 → upsert kpi_values source_type='derived' 幂等 + 同步yellow/red预警
- cron 06:35 注册(紧随 collector 06:30); 自动选最近完整月(8月未结账自动回退7月)
- kpis.py get_kpi: F_BEP_* 派生KPI按静态阈值文本判红黄绿(66.9→黄), 不走target比率
- 阈值经财务Bot 2026-09-08裁决: COVER >=100绿 50~100黄 <50红; GAP <=0绿 <=50黄 >50红
This commit is contained in:
Hermes CI Fix
2026-09-08 13:43:20 +08:00
parent ebe0962c09
commit 1a5356a2d1
4 changed files with 418 additions and 16 deletions
+46 -16
View File
@@ -5,6 +5,7 @@ from sqlalchemy import func
from typing import Optional, List
from datetime import datetime
import json
import re
from app.database import get_db
from app.deps import get_entity_id
@@ -111,6 +112,28 @@ REVERSE_INDICATORS = ['C_REBATE_RATE', 'P_BUG_RATE', 'P_REWORK_PCT', 'F_DEBT_RAT
'F_FACTORY_REBATE_RATE', 'F_COST_CONTROL_RATE', 'F_INV_DAYS']
def _static_threshold_level(value, kpi) -> str:
"""按 KPI 静态阈值文本判定级别(绿→黄→红,命中即止),无阈值返回 info。
供保本派生KPI(F_BEP_*)在详情页使用,与 alert_generator 语义一致。"""
for level, field in (("green", "threshold_green"), ("yellow", "threshold_yellow"),
("red", "threshold_red")):
expr = getattr(kpi, field, None)
if not expr:
continue
m = re.match(r"(>=|<=|>|<|=|!=)\s*([\d.]+)", str(expr).strip())
if not m:
continue
op, val = m.group(1), float(m.group(2))
hit = {
">=": value >= val, "<=": value <= val,
">": value > val, "<": value < val,
"=": value == val, "!=": value != val,
}[op]
if hit:
return level
return "info"
def _calc_five_tier_score(current_value, target_value, is_reverse=False):
"""五档评分:1-5分(支持正反向指标)"""
if current_value is None or target_value is None or target_value == 0:
@@ -572,28 +595,35 @@ def get_kpi(kpi_id: int, db: Session = Depends(get_db), entity_id: int = Depends
).order_by(KPIValue.period.asc()).all()
is_reverse = kpi.kpi_code in REVERSE_INDICATORS
target = kpi.target_value
# 保本派生KPI(F_BEP_*)使用静态阈值文本判定红黄绿(对齐 KPIList 预警tag语义),
# 不走 target 比率——保本线是参考值不是完成率目标(cma-bep-kpi 2026-09-08)
use_static_threshold = kpi.kpi_code.startswith("F_BEP_") and (
kpi.threshold_green or kpi.threshold_yellow or kpi.threshold_red)
result["values"] = []
for v in vals:
# 判定红黄绿(对齐工作台语义:正向≥0.9绿/≥0.7黄/否则红;反向≤目标绿/≤1.1倍黄/否则红)
level = "info"
score = None
if target and v.actual_value is not None:
if is_reverse:
if v.actual_value <= target:
level = "green"
elif v.actual_value <= target * 1.1:
level = "yellow"
if v.actual_value is not None:
if use_static_threshold:
level = _static_threshold_level(v.actual_value, kpi)
elif target:
if is_reverse:
if v.actual_value <= target:
level = "green"
elif v.actual_value <= target * 1.1:
level = "yellow"
else:
level = "red"
else:
level = "red"
else:
ratio = v.actual_value / target
if ratio >= 0.9:
level = "green"
elif ratio >= 0.7:
level = "yellow"
else:
level = "red"
score, _ = _calc_five_tier_score(v.actual_value, target, is_reverse=is_reverse)
ratio = v.actual_value / target
if ratio >= 0.9:
level = "green"
elif ratio >= 0.7:
level = "yellow"
else:
level = "red"
score, _ = _calc_five_tier_score(v.actual_value, target, is_reverse=is_reverse)
# 偏差率(相对目标)
deviation = None
if target and target != 0 and v.actual_value is not None: