wip(strategy-map): v4 口径落码(分线营收/新式毛利率/管理指标/C1 hold/57 归档)

This commit is contained in:
Hermes CI Fix
2026-10-19 14:32:08 +08:00
parent 4d20c27de4
commit a8141e8879
7 changed files with 538 additions and 88 deletions
+42 -6
View File
@@ -11,6 +11,8 @@ from app.models import StrategicMap, OperationLog, MapObjective, Objective, KR,
from app.utils.kr_name import sanitize_kr_name
from app.utils.kpi_value_scope import actual_only_clause
import copy
# CMA-KPI-20261003 ③ 落码:考核型目标读路径分流(kpi_targets 优先;未命中回退哨兵在 utils 内)
from app.utils.kpi_target_reader import fetch_target_map, pick_cycle_target, target_type_for_frequency
import json
import logging
@@ -85,11 +87,10 @@ def list_maps(db: Session = Depends(get_db), entity_id: int = Depends(get_entity
@router.get("/strategy/revenue-caliber")
def strategy_revenue_caliber(entity_id: int = Depends(get_entity_id)):
"""战略地图摘要条 · 收入口径状态(真数据铁律:未闭合不出单一汇总数)。
"""战略地图摘要条 · 收入口径状态(口径已闭合,金额取 /maps/summary)。
2026-10-18 改造单 v2 §4:A1 收入口径未闭合前,前端只显示「口径待定」+
分业务位(酒类代销 / IT自营,amount=None ⇒ 显示「—/无数据」);
后端不得擅自选总额/净额,也不得把两块合并成一个「营业收入」。
2026-10-19 A1 终稿:两家均总额法;增值税销售货物 13%。
本端点只出口径与业务线维度(不含金额);金额一律由 /maps/summary 现算。
"""
from app.data import revenue_caliber
return revenue_caliber.snapshot(entity_id)
@@ -983,6 +984,8 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
KPIDefinition.entity_id == entity_id,
).all() if all_kpi_codes else []
kpi_map = {k.kpi_code: k for k in kpi_defs}
# ③ 落码:考核型目标一次性取新路径(战略回顾当期)
_tmap = fetch_target_map(db, entity_id, [k.id for k in kpi_defs])
# 查询最新KPI实际值
kpi_ids = [k.id for k in kpi_defs]
@@ -1051,10 +1054,17 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
held = code in DATA_HOLD_KPIS
actual = None if held else lv.get("actual_value")
# 多粒度目标优先: 月目标(多数KPI月度) → 兼容legacy target_value (OpenCode审查#5)
target = kpi_def.target_monthly or kpi_def.target_value
# ③ 落码:新路径命中即用;未命中回退兼容列(哨兵在 utils,不 or 0)
# COMPAT-FALLBACK 到期 2026-11-02:新表未命中 → 回退兼容列(顺序回退,不 or 0)
target = pick_cycle_target(_tmap, kpi_def.id, "monthly", kpi_def.target_monthly)
if target is None:
target = pick_cycle_target(_tmap, kpi_def.id,
target_type_for_frequency(kpi_def.frequency),
kpi_def.target_value)
# 判断红黄绿灯(绿≥90% / 黄60-90% / 红<60%)
# 方向修正: 越低越好型(费用率/成本率/损耗/逾期/投诉/流失) 与 负值目标 (OpenCode审查#5)
level = "gray"
ratio_val = None
if actual is not None and target:
name = (kpi_def.kpi_name or "") + " " + (kpi_def.kpi_code or "")
lower_better = any(k in name for k in
@@ -1065,6 +1075,7 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
ratio = actual / target
if ratio < 0:
ratio = 0 # 负值目标(如亏损控制) 完成度不为负
ratio_val = ratio
if ratio >= 0.9:
level = "green"
elif ratio >= 0.6:
@@ -1074,6 +1085,11 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
else:
level = "gray"
# C1 数据前置:held KPI 不着色、不透出达成率(level=pending 由前端灰虚线呈现)
if held:
level = "pending"
ratio_val = None
if level == "red":
worst_level = "red"
elif level == "yellow" and worst_level != "red":
@@ -1087,14 +1103,29 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
"actual_value": actual,
"unit": kpi_def.unit,
"level": level,
# 达成率(按指标方向归一,None=无目标/无实测,调用方须显式显示"无数据")
"ratio": round(ratio_val, 4) if ratio_val is not None else None,
})
kpi_ratios = [k["ratio"] for k in obj_kpis if k.get("ratio") is not None]
obj_ratio = round(sum(kpi_ratios) / len(kpi_ratios), 4) if kpi_ratios else None
# C1 数据前置:目标含 held KPI ⇒ 整节点「数据待核」,不计入任何达成率汇总
hold_reason = next((DATA_HOLD_KPIS[c] for c in obj.get("kpis", []) if c in DATA_HOLD_KPIS), None)
obj_item = {
"name": obj.get("name", ""),
"icon": obj.get("icon", ""),
"kpis": obj_kpis,
"level": worst_level,
"level": "pending" if hold_reason else worst_level,
"has_data": len(obj_kpis) > 0,
# 目标达成率 = 有实测值 KPI 的方向归一达成率均值;None ⇒ 前端显示"无数据"
"ratio": None if hold_reason else obj_ratio,
"kpi_total": len(obj_kpis),
"kpi_with_data": 0 if hold_reason else len(kpi_ratios),
"has_actual": False if hold_reason else bool(kpi_ratios),
"data_hold": bool(hold_reason),
"hold_reason": hold_reason,
# 行动方案汇总(OpenCode审查#6): 按本目标KPI统计
"action_plan_summary": {
"total": len([p for p in action_plans_data if p["kpi_id"] in [k["kpi_id"] for k in obj_kpis]]),
@@ -1105,6 +1136,11 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
}
objectives.append(obj_item)
# C1 数据前置:数据待核目标单列,不进达标/关注/风险任何一档
if hold_reason:
total_hold += 1
continue
if worst_level == "green":
total_ok += 1
elif worst_level == "yellow":