wip(strategy-map): v4 口径落码(分线营收/新式毛利率/管理指标/C1 hold/57 归档)
This commit is contained in:
+42
-6
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user