feat(KR): 行动挂KR完成 — action_plans.kr_id字段+API+前端目标/KR选择+KR中文名自动填+地图krs端点
- action_plans表加kr_id字段(关联krs.id),create/update/list支持kr_id读写
- 创建行动选KR时自动继承objective_id;列表附kr_title
- 前端行动方案库加'所属目标/KR'两级联动选择,选中KR自动带出KPI
- OKR详情页KR卡片下显示达成行动列表(O→KR→行动三层)
- MapCanvasDialogs onKpiChange选中KPI自动填kr.name=kpi.kpi_name(中文)
- 编辑回填英文code KR名转中文显示兜底
- maps.py 新增GET /{map_id}/krs端点(MapReview战略回顾会KR进度)
- 数据修复: krs表9条英文code title→中文名(按kpi_definitions映射)
- 数据迁移: 高置信行动挂KR(费用率→kr10,应收→kr14); 地图32空KR清理
- okr list的kr_summary补metric_kpi_id(前端联动用)
- 新增6个kr_id测试(test_action_plans),全量547通过
This commit is contained in:
@@ -9,7 +9,7 @@ from calendar import monthrange
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_role, require_auth
|
||||
from app.models import ActionPlan, KPIAlert, KPIDefinition, User, Objective
|
||||
from app.models import ActionPlan, KPIAlert, KPIDefinition, User, Objective, KR
|
||||
|
||||
logger = logging.getLogger("cma.action_plans")
|
||||
|
||||
@@ -64,6 +64,7 @@ def plan_to_dict(p: ActionPlan) -> dict:
|
||||
"alert_id": p.alert_id,
|
||||
"kpi_id": p.kpi_id,
|
||||
"objective_id": p.objective_id,
|
||||
"kr_id": p.kr_id,
|
||||
"title": p.title,
|
||||
"description": p.description,
|
||||
"assignee": p.assignee,
|
||||
@@ -87,6 +88,7 @@ def list_plans(
|
||||
status: Optional[str] = None,
|
||||
kpi_id: Optional[int] = None,
|
||||
alert_id: Optional[int] = None,
|
||||
keyword: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
@@ -100,6 +102,8 @@ def list_plans(
|
||||
query = query.filter(ActionPlan.kpi_id == kpi_id)
|
||||
if alert_id:
|
||||
query = query.filter(ActionPlan.alert_id == alert_id)
|
||||
if keyword:
|
||||
query = query.filter(ActionPlan.title.like(f"%{keyword}%"))
|
||||
|
||||
# business角色只看自己的
|
||||
if current_user.role == "business":
|
||||
@@ -115,6 +119,16 @@ def list_plans(
|
||||
# 附带KPI名称
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == p.kpi_id).first()
|
||||
item["kpi_name"] = kpi.kpi_name if kpi else "未知KPI"
|
||||
item["kpi_code"] = kpi.kpi_code if kpi else None
|
||||
item["kpi_dimension"] = kpi.dimension if kpi else None
|
||||
# 附带KR信息(行动挂KR 2026-08-27)
|
||||
if p.kr_id is not None:
|
||||
kr = db.query(KR).filter(KR.id == p.kr_id).first()
|
||||
item["kr_title"] = kr.title if kr else None
|
||||
item["kr_metric_kpi_id"] = kr.metric_kpi_id if kr else None
|
||||
else:
|
||||
item["kr_title"] = None
|
||||
item["kr_metric_kpi_id"] = None
|
||||
result.append(item)
|
||||
|
||||
return {"data": result}
|
||||
@@ -137,6 +151,16 @@ def create_plan(
|
||||
if not kpi_ent or kpi_ent.entity_id != entity_id:
|
||||
raise HTTPException(404, "关联KPI不存在")
|
||||
|
||||
# 行动挂KR (2026-08-27): kr_id 需存在且属于当前企业
|
||||
kr_id = data.get("kr_id")
|
||||
if kr_id:
|
||||
kr_ent = db.query(KR).filter(KR.id == kr_id, KR.entity_id == entity_id).first()
|
||||
if not kr_ent:
|
||||
raise HTTPException(404, "关联KR不存在")
|
||||
# 未显式传objective_id时从KR继承目标
|
||||
if not data.get("objective_id"):
|
||||
data["objective_id"] = kr_ent.objective_id
|
||||
|
||||
due_date = datetime.fromisoformat(data["due_date"]) if data.get("due_date") else None
|
||||
|
||||
# 校验截止日期与关联Objective的季度匹配
|
||||
@@ -150,6 +174,7 @@ def create_plan(
|
||||
alert_id=data.get("alert_id"),
|
||||
kpi_id=data["kpi_id"],
|
||||
objective_id=objective_id,
|
||||
kr_id=kr_id,
|
||||
title=data["title"],
|
||||
description=data.get("description"),
|
||||
assignee=data.get("assignee"),
|
||||
@@ -214,6 +239,21 @@ def update_plan(
|
||||
plan.assignee = data["assignee"]
|
||||
if "priority" in data:
|
||||
plan.priority = data["priority"]
|
||||
if "kr_id" in data:
|
||||
# 行动挂KR: 支持置空(null)或改挂
|
||||
if data["kr_id"] is None:
|
||||
plan.kr_id = None
|
||||
else:
|
||||
kr_ent = db.query(KR).filter(KR.id == data["kr_id"]).first()
|
||||
if not kr_ent:
|
||||
raise HTTPException(404, "关联KR不存在")
|
||||
plan.kr_id = kr_ent.id
|
||||
if data.get("objective_id") is not None:
|
||||
plan.objective_id = data["objective_id"]
|
||||
elif plan.objective_id is None:
|
||||
plan.objective_id = kr_ent.objective_id
|
||||
if "objective_id" in data:
|
||||
plan.objective_id = data["objective_id"]
|
||||
if "due_date" in data:
|
||||
plan.due_date = datetime.fromisoformat(data["due_date"]) if data["due_date"] else None
|
||||
if "status" in data:
|
||||
|
||||
Reference in New Issue
Block a user