feat(budget): effective-version caliber + process-order tabs + 4 missing rings

Task: CMA-20261003 (budget module governance). Local commit only; push is
gated on release approval (see report).

Backend
- app/utils/budget_status.py (new): single source for the effective-version
  caliber. EFFECTIVE_STATUSES = approved + active; effective_plan_clause()
  enforces entity isolation; pick_effective() priority active > approved >
  updated_at desc. Fixes: approved budgets disappeared from the execution
  surface because the three queries only read status='active'.
- budget.py: /comparison, /comparison/kpi/{id}, /deviation-check now read the
  effective surface; versions list gains is_effective / can_release fields;
  submit/approve are fail-closed (draft|active -> submitted -> approved).
- deviation_engine.get_budget_for_kpi: effective surface + deterministic pick.
- 4 missing rings, zero DDL, appended to the same router:
  POST /versions/release (approved -> active, idempotent by operation_logs),
  POST /adjustments (derive a new draft version, base rows untouched),
  GET /control/status (effective budget vs actual -> ok/warn/over),
  GET /assessment (achieve rate + A/B/C/D grade).
  Audit rows written through app.utils.oplog.log_op with entity_id.

Frontend
- BudgetManagement.vue: top-level tabs reordered to the process order
  (strategy, method, driver, input, decompose, versions, release, execution,
  deviation, adjust, assess, rolling). Pane name values unchanged, so old
  bookmarks keep working; routes unchanged.
- Cash classify rules / unclassified queue moved to /cash-plan
  (components/CashClassifyPanel.vue); budget actual-value collection moved to
  /data-manage (components/BudgetCollectPanel.vue). Mount points only: no
  change to cash_plans data, classify logic, collection logic or kpi_values.
- api/index.ts: 4 ring endpoints exposed.

Docs / tests
- docs/budget-module-governance-design.md: phase-0 design doc (status machine,
  tab order, domain boundaries, 4-ring design, cleanup write pack scope).
- tests/test_budget_status_machine.py (new): effective surface, version
  guards, release idempotency, adjustment, control and assessment.
- tests/test_budget.py: existing cases adapted to the fail-closed contract.

(cherry picked from commit 4267fc4d25ee45c5675a541af9777be96fe9f9c2)
This commit is contained in:
Hermes CI Fix
2026-10-18 16:32:55 +08:00
parent 75a48f55d2
commit fa1267c323
12 changed files with 1777 additions and 549 deletions
+334 -17
View File
@@ -14,6 +14,14 @@ from app.deps import get_entity_id
from app.auth_middleware import require_auth, require_role
from app.models import BudgetPlan, KPIDefinition, OperationLog, KPIValue
from app.utils.kpi_value_scope import actual_only_clause
from app.utils.oplog import log_op
from app.utils.budget_status import (
EFFECTIVE_STATUSES,
effective_plan_clause,
effective_value_map,
pick_effective,
approval_status_of,
)
router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"],
dependencies=[Depends(require_role("ceo", "finance", "it"))],
@@ -730,18 +738,15 @@ def get_budget_comparison(
)
if kpi_id:
query = query.filter(BudgetPlan.kpi_id == kpi_id)
# 生效版口径(2026-10-03 状态机归一):approved(已批准) ∪ active(已下达)
query = query.filter(
BudgetPlan.period.in_(periods),
BudgetPlan.status == "active",
BudgetPlan.entity_id == entity_id, # 账套隔离 2026-09-21
effective_plan_clause(entity_id), # 含账套隔离
)
budget_plans = query.all()
# 按KPI+期间索引
bp_map = {}
for bp in budget_plans:
key = (bp.kpi_id, bp.period)
bp_map[key] = bp.budget_value
# 按KPI+期间索引(同键多版本 → 取生效行 active > approved > updated_at desc)
bp_map = effective_value_map(budget_plans)
# 查询实际值
kpi_ids = set(bp.kpi_id for bp in budget_plans)
@@ -831,12 +836,13 @@ def get_kpi_comparison(
periods = [f"{y}-{m:02d}" for m in range(1, 13)]
# 预算值
# 生效版口径(2026-10-03 状态机归一;保持原「未按账套过滤」行为不变)
budgets = db.query(BudgetPlan).filter(
BudgetPlan.kpi_id == kpi_id,
BudgetPlan.period.in_(periods),
BudgetPlan.status == "active",
BudgetPlan.status.in_(EFFECTIVE_STATUSES),
).all()
budget_map = {bp.period: bp.budget_value for bp in budgets}
budget_map = {p: v for (_kid, p), v in effective_value_map(budgets).items()}
# 实际值
actuals = db.query(KPIValue).filter(
@@ -898,11 +904,16 @@ def check_budget_deviation(
period = data.get("period") or datetime.now().strftime("%Y-%m")
# 查询该期间有预算的KPI(多租户隔离 entity_id)
budget_plans = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id,
# 生效版口径(2026-10-03 状态机归一):approved(已批准) ∪ active(已下达)
_effective_rows = db.query(BudgetPlan).filter(
BudgetPlan.period == period,
BudgetPlan.status == "active",
effective_plan_clause(entity_id),
).all()
# 同 KPI 多版本并存 → 取生效行(active > approved > updated_at desc),防重复告警
_by_kpi = {}
for _bp in _effective_rows:
_by_kpi.setdefault(_bp.kpi_id, []).append(_bp)
budget_plans = [r for r in (pick_effective(v) for v in _by_kpi.values()) if r is not None]
if not budget_plans:
return {
@@ -1520,7 +1531,7 @@ def list_budget_versions(
# 状态机对齐(2026-09-07 UI修复):DB 默认态是 active(当前在用/草稿态),
# 前端审批按钮语义用 draft=可提交审批/submitted=待审批/approved=已批准只读/rejected=已驳回
# → approval_status 把 active 映射为 draft,同时保留 status 原字段兼容其他引用(下拉"当前"标记等)
approval_status = "draft" if raw_status == "active" else raw_status
approval_status = approval_status_of(raw_status) # 单一实现(预算状态机口径)
updated_str = v["updated_at"].strftime("%Y-%m-%d %H:%M") if v["updated_at"] else ""
result.append({
"version": v["version"],
@@ -1530,6 +1541,8 @@ def list_budget_versions(
"plan_count": v["kpi_count"], # 前端列 prop 别名(BudgetManagement.vue versions tab)
"total_budget": round(v["total_budget"], 2),
"period_count": len(v["periods"]),
"is_effective": raw_status in EFFECTIVE_STATUSES, # 是否执行基准
"can_release": raw_status == "approved", # 是否可下达
"year": year,
"updated_at": updated_str,
"last_updated": updated_str, # 前端列 prop 别名
@@ -1552,10 +1565,13 @@ def submit_budget_version(
).all()
if not rows:
raise HTTPException(404, f"版本 {version} 不存在")
for r in rows:
submittable = [r for r in rows if (r.status or "") in ("draft", "active")]
if not submittable:
raise HTTPException(400, f"版本 {version} 无可提交行(需 draft/active,当前: {sorted({r.status for r in rows})})")
for r in submittable:
r.status = "submitted"
db.commit()
return {"success": True, "version": version, "status": "submitted", "count": len(rows)}
return {"success": True, "version": version, "status": "submitted", "count": len(submittable)}
@router.post("/versions/approve")
@@ -1574,10 +1590,13 @@ def approve_budget_version(
if not rows:
raise HTTPException(404, f"版本 {version} 不存在")
new_status = "approved" if action == "approved" else "rejected"
for r in rows:
decidable = [r for r in rows if (r.status or "") == "submitted"]
if not decidable:
raise HTTPException(400, f"版本 {version} 无可审批行(需 submitted,当前: {sorted({r.status for r in rows})})")
for r in decidable:
r.status = new_status
db.commit()
return {"success": True, "version": version, "status": new_status, "count": len(rows)}
return {"success": True, "version": version, "status": new_status, "count": len(decidable)}
@router.post("/versions/diff")
@@ -1781,3 +1800,301 @@ def sync_cash_plans(
"unclassified_count": unclassified_count,
"skipped_annual": skipped_annual,
}
# ============ 预算闭环补齐(2026-10-03 施工单 CMA-20261003 阶段 3:最小可用骨架) ============
# 设计件 docs/budget-module-governance-design.md §4:控制 / 调整 / 下达 / 考核
# 零 DDL:全部基于既有 budget_plans / kpi_values / operation_logs。
DEFAULT_CONTROL_THRESHOLD_PCT = 20.0
def _budget_control_threshold(db) -> float:
"""控制阈值(%):system_configs.budget_control_threshold,缺省 20%。"""
from app.models import SystemConfig
import json as _json
cfg = db.query(SystemConfig).filter(
SystemConfig.config_key == "budget_control_threshold"
).first()
if cfg:
try:
return float(_json.loads(cfg.config_value))
except (ValueError, TypeError):
return DEFAULT_CONTROL_THRESHOLD_PCT
return DEFAULT_CONTROL_THRESHOLD_PCT
def _effective_rows_for_periods(db, entity_id: int, periods):
"""期间集合内的生效预算行(同键取生效行,防同 KPI 多版本重复计算)。"""
rows = db.query(BudgetPlan).filter(
BudgetPlan.period.in_(periods),
effective_plan_clause(entity_id),
).all()
grouped = {}
for r in rows:
grouped.setdefault((r.kpi_id, r.period), []).append(r)
out = []
for key, rows_of_key in grouped.items():
row = pick_effective(rows_of_key)
if row is not None:
out.append((key[0], key[1], row))
return out
def _direction_of(kpi, higher_better_codes) -> str:
return "higher" if (kpi.kpi_code in higher_better_codes or kpi.kpi_name in higher_better_codes) else "lower"
@router.get("/control/status")
def get_budget_control_status(
year: Optional[int] = Query(None),
period: Optional[str] = Query(None),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""预算控制状态(缺失环「控制」最小骨架,只读):生效预算 vs 实际 → ok/warn/over。"""
from app.utils.deviation_engine import get_higher_better_codes
y = year or datetime.now().year
periods = [period] if period else [f"{y}-{m:02d}" for m in range(1, 13)]
threshold = _budget_control_threshold(db)
eff = _effective_rows_for_periods(db, entity_id, periods)
kpi_ids = {kid for kid, _p, _r in eff}
kpis = {k.id: k for k in db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()} if kpi_ids else {}
actuals = {}
if kpi_ids:
for v in db.query(KPIValue).filter(
KPIValue.kpi_id.in_(kpi_ids),
KPIValue.period.in_(periods),
KPIValue.actual_value.isnot(None),
actual_only_clause(),
).all():
actuals[(v.kpi_id, v.period)] = v.actual_value
higher_better = set(get_higher_better_codes(db, entity_id))
items = []
summary = {"total": 0, "ok": 0, "warn": 0, "over": 0, "no_actual": 0}
for kid, per, plan in sorted(eff, key=lambda x: (x[0], x[1])):
kpi = kpis.get(kid)
if kpi is None:
continue
budget = plan.budget_value or 0
actual = actuals.get((kid, per))
direction = _direction_of(kpi, higher_better)
level, rate, deviation_rate = "no_actual", None, None
if actual is not None and budget:
rate = round(actual / budget * 100, 2)
deviation_rate = round((actual - budget) / abs(budget) * 100, 2)
if direction == "higher":
level = "over" if deviation_rate <= -threshold else ("warn" if deviation_rate <= -threshold * 0.8 else "ok")
else:
level = "over" if deviation_rate >= threshold else ("warn" if deviation_rate >= threshold * 0.8 else "ok")
summary["total"] += 1
summary[level] = summary.get(level, 0) + 1
items.append({
"kpi_id": kid,
"kpi_code": kpi.kpi_code,
"kpi_name": kpi.kpi_name,
"unit": kpi.unit or "",
"period": per,
"basis_version": plan.version,
"basis_status": plan.status,
"direction": direction,
"budget_value": budget,
"actual_value": actual,
"execution_rate": rate,
"deviation_rate": deviation_rate,
"control_level": level,
})
return {"year": y, "period": period, "threshold_pct": threshold, "summary": summary, "items": items}
@router.get("/assessment")
def get_budget_assessment(
year: Optional[int] = Query(None),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""预算考核(缺失环「考核」最小骨架,只读):生效预算 vs 实际 → 达成率 + 等级 A/B/C/D。"""
from app.utils.deviation_engine import get_higher_better_codes
y = year or datetime.now().year
periods = [f"{y}-{m:02d}" for m in range(1, 13)]
eff = _effective_rows_for_periods(db, entity_id, periods)
kpi_ids = {kid for kid, _p, _r in eff}
kpis = {k.id: k for k in db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()} if kpi_ids else {}
actuals = {}
if kpi_ids:
for v in db.query(KPIValue).filter(
KPIValue.kpi_id.in_(kpi_ids),
KPIValue.period.in_(periods),
KPIValue.actual_value.isnot(None),
actual_only_clause(),
).all():
actuals[(v.kpi_id, v.period)] = v.actual_value
higher_better = set(get_higher_better_codes(db, entity_id))
agg = {}
for kid, per, plan in eff:
row = agg.setdefault(kid, {"budget_total": 0.0, "actual_total": 0.0, "periods": set(), "basis_version": plan.version,
"basis_status": plan.status, "_ts": plan.updated_at})
row["budget_total"] += plan.budget_value or 0
row["periods"].add(per)
if plan.updated_at and (row["_ts"] is None or plan.updated_at > row["_ts"]):
row["_ts"] = plan.updated_at
row["basis_version"] = plan.version
row["basis_status"] = plan.status
if (kid, per) in actuals:
row["actual_total"] += actuals[(kid, per)] or 0
items = []
summary = {"kpi_count": 0, "a": 0, "b": 0, "c": 0, "d": 0, "na": 0,
"total_budget": 0.0, "total_actual": 0.0}
for kid, row in sorted(agg.items()):
kpi = kpis.get(kid)
if kpi is None:
continue
direction = _direction_of(kpi, higher_better)
bt, at = round(row["budget_total"], 2), round(row["actual_total"], 2)
achieve, grade = None, "NA"
if bt and row["periods"]:
achieve = round((at / bt * 100) if direction == "higher" else (bt / at * 100 if at else 0.0), 2)
grade = "A" if achieve >= 100 else ("B" if achieve >= 90 else ("C" if achieve >= 80 else "D"))
summary["kpi_count"] += 1
summary[grade.lower()] = summary.get(grade.lower(), 0) + 1
summary["total_budget"] += bt
summary["total_actual"] += at
items.append({
"kpi_id": kid,
"kpi_code": kpi.kpi_code,
"kpi_name": kpi.kpi_name,
"unit": kpi.unit or "",
"direction": direction,
"basis_version": row["basis_version"],
"basis_status": row["basis_status"],
"period_count": len(row["periods"]),
"budget_total": bt,
"actual_total": at,
"achieve_rate": achieve,
"grade": grade,
})
summary["total_budget"] = round(summary["total_budget"], 2)
summary["total_actual"] = round(summary["total_actual"], 2)
summary["achieve_rate"] = round(summary["total_actual"] / summary["total_budget"] * 100, 2) if summary["total_budget"] else None
return {"year": y, "summary": summary, "items": items}
@router.post("/versions/release")
def release_budget_version(
data: dict,
db: Session = Depends(get_db),
current_user=Depends(require_auth),
entity_id: int = Depends(get_entity_id),
):
"""预算下达(缺失环「下达」):已批准版本 → 已下达(active)。fail-closed + 幂等。"""
version = (data.get("version") or "").strip()
if not version:
raise HTTPException(400, "需要 version")
rows = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id,
BudgetPlan.version == version,
).all()
if not rows:
raise HTTPException(404, f"版本 {version} 不存在")
approved = [r for r in rows if (r.status or "") == "approved"]
if not approved:
# 幂等判据 = 「本版本确有下达历史」(operation_logs 记录),而不是 active 现状:
# 迁移前遗留的 active 行 = 在用/草稿态,不能当成「已下达」(设计件 §1.2/§1.3)。
released_before = any(
isinstance(l.detail, dict)
and l.detail.get("op") == "release"
and l.detail.get("version") == version
for l in db.query(OperationLog)
.filter(OperationLog.target_type == "budget_version")
.order_by(OperationLog.id.desc())
.limit(500)
.all()
)
if released_before and any((r.status or "") == "active" for r in rows):
return {"success": True, "version": version, "status": "active", "count": 0, "already": True}
raise HTTPException(400, f"版本 {version} 不可下达(需 approved,当前: {sorted({r.status for r in rows})})")
for r in approved:
r.status = "active"
log_op(db, "update", target_type="budget_version", target_id=None,
detail={"op": "release", "version": version, "count": len(approved),
"from_status": "approved", "to_status": "active"},
entity_id=entity_id)
db.commit()
return {"success": True, "version": version, "status": "active", "count": len(approved), "already": False}
@router.post("/adjustments")
def create_budget_adjustment(
data: dict,
db: Session = Depends(get_db),
current_user=Depends(require_auth),
entity_id: int = Depends(get_entity_id),
):
"""预算调整(缺失环「调整」最小骨架):基于生效基线版本派生新版本(draft),原版本零改动。"""
base_version = (data.get("base_version") or "").strip()
new_version = (data.get("new_version") or "").strip()
reason = (data.get("reason") or "").strip()
items = data.get("items") or []
if not base_version or not new_version:
raise HTTPException(400, "需要 base_version 与 new_version")
if new_version == base_version:
raise HTTPException(400, "new_version 不能与 base_version 相同")
if not reason:
raise HTTPException(400, "调整单必须填写 reason(可追溯)")
if db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id, BudgetPlan.version == new_version
).first():
raise HTTPException(400, f"版本 {new_version} 已存在(调整单只允许派生新版本,不覆盖)")
base_rows = db.query(BudgetPlan).filter(
BudgetPlan.entity_id == entity_id,
BudgetPlan.version == base_version,
BudgetPlan.status.in_(EFFECTIVE_STATUSES),
).all()
if not base_rows:
raise HTTPException(404, f"基线版本 {base_version} 不存在或非生效版")
overrides = {}
for it in items:
kid, per = it.get("kpi_id"), it.get("period")
if kid is None or not per or it.get("budget_value") is None:
raise HTTPException(400, "items 每项需 kpi_id / period / budget_value")
overrides[(int(kid), per)] = float(it["budget_value"])
operator = current_user.name if hasattr(current_user, "name") else ""
applied, unmatched = 0, len(overrides)
for r in base_rows:
key = (r.kpi_id, r.period)
if key in overrides:
val = overrides.pop(key)
applied += 1
unmatched -= 1
else:
val = r.budget_value
db.add(BudgetPlan(
entity_id=entity_id,
map_id=r.map_id,
kpi_id=r.kpi_id,
period=r.period,
budget_value=val,
budget_year=r.budget_year,
budget_month=r.budget_month,
version=new_version,
status="draft",
source_type=r.source_type or "manual",
calc_logic=r.calc_logic,
remark=f"调整单:{reason} 基线:{base_version}"[:500],
created_by=operator,
))
log_op(db, "create", target_type="budget_adjustment", target_id=None,
detail={"base_version": base_version, "new_version": new_version, "reason": reason,
"copied": len(base_rows), "applied_overrides": applied, "unmatched_overrides": unmatched},
entity_id=entity_id)
db.commit()
return {"success": True, "new_version": new_version, "base_version": base_version, "reason": reason,
"created": len(base_rows), "applied_overrides": applied, "unmatched_overrides": unmatched,
"status": "draft", "note": "调整版本已生成为 draft,需提交审批 → 批准 → 下达后方可作为执行基准"}
+100
View File
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""预算状态机口径 —— 单一事实源(2026-10-03,施工单 CMA-20261003 阶段 1)
设计件:`docs/budget-module-governance-design.md` §1
口径(逐字固化):
| status | 语义 | 入执行面 |
|---|---|---|
| `draft` | 草稿(编制中,可编辑) | 否 |
| `submitted` | 待审批 | 否 |
| `approved` | **已批准(= 执行基准 / 生效版)** | 是 |
| `active` | 已下达(下达动作后)/ 历史遗留「在用」 | 是 |
| `rejected` | 已驳回 | 否 |
| `archived` | 已归档 | 否 |
- **执行/对比/差异的唯一基准 = `approved` ∪ `active`**:审批通过即获得执行资格,
修的是「执行查询只读 `active` ⇒ 已批准预算从执行视图消失」这一处口径错配。
- 同键 `(kpi_id, period)` 多行并存时选行优先级:`active`(已下达)> `approved`,
同级取 `updated_at` 最新 —— 保证取行确定性(改前 `bp_map[key] = value` 后写覆盖先写,
结果依赖 DB 返回顺序)。
- 既有 216 行 `active` 本单**不改标**(口径彻底归一属待 finance/任总确认项,见设计件 §1.3);
`active` 在 UI 侧仍映射为 `draft`(历史兼容,`approval_status_of`)。
**规则**:任何「预算执行基准」查询必须走 `effective_plan_clause()` /
`pick_effective()`,**禁止再手写 `BudgetPlan.status == "active"`**。
非执行面用法(新建去重、自动分解、滚动延展、方法应用、现金流联动)
不在本口径范围内,随「彻底归一」单一起收敛(设计件 §1.5)。
"""
from sqlalchemy import and_
from app.models import BudgetPlan
#: 执行基准状态(生效版)
EFFECTIVE_STATUSES = ("approved", "active")
#: UI 展示为「草稿」的状态(`active` = 历史遗留「在用」态)
DRAFT_LIKE_STATUSES = ("draft", "active")
#: 已下达态(选行优先级最高)
_STATUS_ACTIVE = "active"
def effective_plan_clause(entity_id: int):
"""执行基准条件(含账套隔离)—— 单一取数口。
用法:`db.query(BudgetPlan).filter(BudgetPlan.period == p, effective_plan_clause(eid))`
"""
if entity_id is None:
raise ValueError("effective_plan_clause 必须传 entity_id(账套隔离,禁止漏传落默认账套)")
return and_(
BudgetPlan.entity_id == entity_id,
BudgetPlan.status.in_(EFFECTIVE_STATUSES),
)
def effective_status_priority(status: str) -> int:
"""选行优先级:`active`(已下达) 最高,其余生效态次之。"""
return 0 if status == _STATUS_ACTIVE else 1
def _sort_key(row):
ts = getattr(row, "updated_at", None)
try:
stamp = ts.timestamp() if ts is not None else 0.0
except (AttributeError, ValueError, OSError):
stamp = 0.0
return (effective_status_priority(getattr(row, "status", "") or ""), -stamp)
def pick_effective(rows):
"""同一 `(kpi_id, period)` 的多行 → 取生效行;空输入返回 None。"""
if not rows:
return None
return sorted(rows, key=_sort_key)[0]
def effective_value_map(rows):
"""行列表 → `{(kpi_id, period): budget_value}`(按生效优先级去重)。"""
grouped = {}
for r in rows:
grouped.setdefault((r.kpi_id, r.period), []).append(r)
out = {}
for key, rows_of_key in grouped.items():
row = pick_effective(rows_of_key)
if row is not None:
out[key] = row.budget_value
return out
def is_effective_status(status: str) -> bool:
return (status or "") in EFFECTIVE_STATUSES
def approval_status_of(raw: str) -> str:
"""DB status → 前端 `approval_status`(历史兼容:`active` 曾表示在用/草稿态)。"""
if not raw:
return "draft"
if raw in DRAFT_LIKE_STATUSES:
return "draft"
return raw
+4 -2
View File
@@ -13,6 +13,7 @@ from datetime import datetime
from typing import Optional
from app.database import get_session_local
from app.utils.budget_status import EFFECTIVE_STATUSES, pick_effective
from app.models import KPIDefinition, KPIValue, KPIAlert, BudgetPlan, SystemConfig
# 裁定 R1/R2/R3(2026-09-19 财务口径):demo 账套排除 / 期间窗口 / 规整月期间
from app.utils.alert_entity_scope import (
@@ -93,14 +94,15 @@ def calc_deviation(actual: float, budget: float) -> dict:
def get_budget_for_kpi(db, kpi_id: int, period: str, version: str = None) -> Optional[float]:
"""获取指定KPI在指定期间的预算值"""
# 生效版口径(2026-10-03 状态机归一):approved(已批准) ∪ active(已下达)
query = db.query(BudgetPlan).filter(
BudgetPlan.kpi_id == kpi_id,
BudgetPlan.period == period,
BudgetPlan.status == "active",
BudgetPlan.status.in_(EFFECTIVE_STATUSES),
)
if version:
query = query.filter(BudgetPlan.version == version)
plan = query.order_by(BudgetPlan.updated_at.desc()).first()
plan = pick_effective(query.all()) # active > approved > updated_at desc
return plan.budget_value if plan else None