Files
cma-management/docs/budget-module-governance-design.md
Hermes CI Fix fa1267c323 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)
2026-10-18 16:32:55 +08:00

17 KiB
Raw Permalink Blame History

CMA 预算管理模块治理设计 v0.1

施工单:CMA-20261003.md(研学调度 2026-10-03|P1)· 阶段 0 设计件 依据:/srv/hermes/wisdom/_wisdom/reference/CMA预算管理-子项与编制顺序审查-20261003.md(研学只读实测,快照 2026-10-03 18:05 CST) 基准:管理会计应用指引「预算管理」200-203 号 + 本模块自带流程图 docs/budget-flow-verify-20260827.md 落点:代码全部在 worktree 分支 wt/budget-governance(基于 origin/main 8631ce7),不 push、不部署;生产写库清单只编制不执行。


0. 结论摘要(四问)

问 结论
状态机口径归一 status 收敛为 draft → submitted → approved(已批准=生效基准) → active(已下达),旁支 rejected / archived;执行/对比/差异的唯一基准 = approved ∪ active(approved 优先)。修的是「执行查询只读 active」这一处口径错配
页签顺序归位 12 序:战略预算编制 → 编制方法 → 驱动因子预算 → 预算录入 → 年度分解 → 版本审批 → 下达 → 预算执行 → 差异分析 → 预算调整 → 预算考核 → 持续规划
分域边界 现金流分类规则/待分类队列 → 收付款计划页(资金域);实际值归集(取数映射/实际值标签/采集日志)→ 数据管理页(KPI 取数域)。只挪前端入口与挂载位置,数据与逻辑零改动
缺失 4 环 控制 / 调整 / 下达 / 考核 各出「最小可用骨架」= 零 DDL 的真实接口 + 页面(一条端到端路径),完整功能拆后续单

本设计件唯一待确认项(阻塞彻底归一,不阻塞本单代码):若把「生效」定义为唯一态(新建写 draft、历史 216 行 active 改标 draft),执行面将从 216 行收缩为 85 行覆盖的 8 个核心 KPI —— 这是财务预算口径变更,须 finance/任总确认后另单落地。本单采取零 status 数据迁移的兼容口径(见 §1.3)。


1. 问一:状态机口径归一

1.1 现状(实测)

  • budget_plans.status 现网取值:active 216 行 / approved 85 行(301 行总量,entity1 276 · entity2 25)。
  • 新建行写 active(backend/app/api/budget.py:128),前端把 active 映射为 draft(budget.py:1504)⇒ active 一个值同时承担「草稿/在用」与「执行基准」两种语义(审查件指出的根因)。
  • 执行/对比/差异三处查询只读 status == 'active':
    • backend/app/api/budget.py:716(/comparison 实际 vs 预算)
    • backend/app/api/budget.py:818(/comparison/kpi/{kpi_id})
    • backend/app/api/budget.py:885(/deviation-check 偏差预警)
    • backend/app/utils/deviation_engine.py:94-105(get_budget_for_kpi,calc_period_deviation 的取数口)
  • 审批链:/versions/submit 置 submitted、/versions/approve 置 approved(budget.py:1522-1561)。
  • ⇒ 版本一旦获批即离开执行面;系统实际拿「在用/草稿」版本当执行基准。审批既非执行前置、也不作执行依据。

1.2 目标状态机(唯一语义)

draft        草稿(编制中,可编辑)
  │ submit
  ▼
submitted    待审批(锁定编辑,等待审批人)
  │ approve            │ reject
  ▼                    ▼
approved     已批准    rejected  已驳回(可回退 draft 重编)
  │ release(下达)
  ▼
active       已下达(生效执行;下达动作 = 缺失环之一,见 §4.3)
  │
  ▼
archived     已归档(被新版本取代;只做 status 改标,不删行)
  • 执行基准(生效版)= status ∈ {approved, active}:审批通过即获得执行资格,下达只改变「是否已正式下达」的管理状态,不再把已批准预算踢出执行面。
  • 同一 (entity_id, kpi_id, period) 出现多行生效版时,取行优先级 active > approved,同级取 updated_at 最新,保证查询确定性(现状 bp_map[key] = value 后写覆盖先写,结果依赖返回顺序)。
  • draft / submitted / rejected / archived 一律不入执行面。

1.3 既有数据的迁移口径(零改标,先立口径)

数据 处置 理由
approved 85 行 不改 status。口径切换后自动进入执行面(改前 0 → 改后 85) 它们本就是「已批准」,改标反而破坏语义;approved 在新机里就是生效态
active 216 行 本单不改标 见下方「待确认项」
其余字段类脏数据 阶段 4 写包 B1–B4(月=0 / source_type / map_id / 版本命名) 与状态机解耦,见 §5

待确认项(须 finance/任总拍板后才可执行):把「生效」收敛为唯一态,需要把 216 行历史 active 改标 draft(一条 status 改标 DML,可 1:1 回滚)。其后果是执行面由 216 行收缩为 8 个核心 KPI 的 85 行 —— 属财务预算口径变更,且会改变现网所有预算执行/偏差告警数字。本单不执行该 DML,只把口径与回滚方案写进 §5 待批清单。

1.4 UI / 接口兼容

  • GET /versions 的 approval_status 映射(budget.py:1498-1517)扩展为:active → draft(遗留在用,保留现映射)、draft → draft、submitted/approved/rejected/archived → 原值;status 原字段保留(下拉「当前」标记等仍在用)。
  • 版本列表新增 is_effective: bool(该版本是否为执行基准)与 can_release(是否可下达),供前端按钮显隐;不改任何既有字段语义。

1.5 单一实现(防第三处手写副本)

新增 backend/app/utils/budget_status.py:

EFFECTIVE_STATUSES = ("approved", "active")   # 执行基准
DRAFT_LIKE = ("draft", "active")              # UI 展示为「草稿」
def effective_plan_filter(entity_id)          # 返回 SQLAlchemy 条件列表(含 entity 隔离)
def pick_effective(rows)                      # 同键多行选行:active > approved > updated_at desc
def approval_status_of(raw)                   # DB status → UI approval_status

规则:全仓任何「预算执行基准」查询只能经 effective_plan_filter(),禁止再出现 BudgetPlan.status == "active"(执行面);既有非执行面用法(新建去重 :109、自动分解 :219-391、滚动延展 :590、方法应用 :1406、现金流联动 :1680)本单不动,避免行为漂移(它们属数据写入/生成路径,随「彻底归一」单一起收敛)。


2. 问二:页签顺序归位

2.1 目标顺序(12 序)

# 页签(name) 环节 变更
1 战略预算编制(strategy) 战略驱动 由第 3 提至第 1
2 编制方法(method) 选方法 由第 6 提至第 2
3 驱动因子预算(driver) 驱动因子/明细 由第 7 提至第 3
4 预算录入(input) 明细编制 由第 1 降至第 4(明细在方法与驱动因子之后)
5 年度分解(decompose) 汇总平衡 保持(前移到编制之后)
6 版本审批(versions) 审批 由第 4 后移,标签由「版本管理」改为「版本审批」
7 下达(release) 下达(新环) 新增
8 预算执行(execution) 执行/控制 由第 5 后移;现金流分类块迁出(§3)
9 差异分析(deviation) 差异分析 从「持续规划 > 偏差告警」提升为一级页签
10 预算调整(adjust) 调整(新环) 新增
11 预算考核(assess) 考核(新环) 新增
12 持续规划(rolling) 滚动/预测 保持尾部

2.2 迁移方案与兼容

  • 只改 <el-tabs> 内 <el-tab-pane> 的出现顺序与标签文案,v-model="activeTab" 的 name 值全部不变 ⇒ 旧书签(?tab=xxx)/ 已保存的 activeTab / 路由 query 全部继续可用。
  • 归位后与模块自带流程图(战略编制→录入→分解→审批→执行→滚动)一致,并补齐流程图缺的下达/差异/调整/考核。
  • 前端零新依赖,弹窗继续走 MyDialog(el-dialog 在本项目线上不可见)。

3. 问三:分域边界(迁出,只挪入口)

迁出项 现状 迁至 权限变化 边界
现金流分类规则 + 待分类队列 「预算执行」页签内嵌(BudgetManagement.vue 207-253) 收付款计划页 /cash-plan(资金域)新增页签「分类规则」,组件 components/CashClassifyPanel.vue /cash-plan roles = ceo, finance, business, it ⇒ 比 /budget(ceo, finance, it)多 business 可见该面板 ⇒ 资金域入口对商务可见(分类规则维护仍受后端 require_role 约束,不变) 不动 cash_plans / CashPlanClassifyRule 数据与分类逻辑;接口仍挂 /api/cma/budget/cash-classify-rules(路由归属后续随资金域单再迁,避免本单改后端契约)
实际值归集(取数映射 / 实际值标签 / 采集日志) 「实际值归集」页签(BudgetManagement.vue 382-470) 数据管理页 /data-manage(KPI 取数域)新增页签「预算取数」,组件 components/BudgetCollectPanel.vue 两端 roles 相同(ceo, finance, it)⇒ 无权限变化 不动采集逻辑、不碰 kpi_values / kpi_value_sources 数据
现金流联动按钮(⇄ 同步现金流计划) 「预算执行」页签 随分类规则面板一并迁至收付款计划页 同上 仍调 POST /budget/sync-cash-plans(契约不变),cash_plans 数据零改动

归属口径:「预算」模块只保留预算自身的生命周期环节;跨域资产业务(现金流分类/计划)归资金域,数据取数归 KPI 数据域。本单只做入口归位,接口前缀与后端文件归属留待对应域的单子处理(避免一次改两处契约)。


4. 问四:缺失 4 环设计

设计原则:最小可用骨架 = 零 DDL + 真实接口 + 页面(每环至少一条端到端路径);表结构设计与完整功能拆后续单。

落地说明(实现期,2026-10-03):四个端点实际追加在 backend/app/api/budget.py 末尾(同 router 同 prefix=/api/cma/budget)——本仓「新 router 漏注册 main.py ⇒ 404」是既有坑,同文件追加免改注册;因此没有 backend/app/api/budget_rings.py 这个文件(设计稿的落点在此按实现订正)。

4.1 预算控制(control)

  • 语义:执行中的控制点 —— 生效预算 vs 实际值,按方向(越高越好 / 越低越好,复用 deviation_engine.higher_better_key)给出控制级 ok / warn / over。
  • 接口:GET /api/cma/budget/control/status?year=&period=(只读)。
  • 判据(最小版):over = 改善型 KPI 实际 < 预算 ×(1−阈值) 或 费用型实际 > 预算;warn = 越过阈值的 80%;阈值取 system_configs.budget_control_threshold(缺省 20%;实现期从设计稿的 deviation_threshold 订正为独立键,避免与 /deviation-check 的请求阈值互相影响)。
  • 页面:预算执行页签顶部「控制状态」卡片(over 计数 + 明细表)。
  • 不在本单:硬控制(阻断提交/付款)、控制点规则表、与司库支付联动。

4.2 预算调整(adjust)

  • 语义:正式调整/追加单 —— 以既有版本为基线派生新版本,保留原版本与调整原因,形成可追溯的调整链(现网只有隐式的「版本/滚动」)。
  • 接口:POST /api/cma/budget/adjustments,body {base_version, new_version, reason, items:[{kpi_id, period, budget_value}]};行为 = 复制基线行 → 新版本行(status='draft')→ 覆盖 items 指定值 → remark 记 调整单:<reason> 基线:<base_version> → 写 OperationLog。
  • 约束:new_version 必须不存在(防覆盖);基线必须存在且为生效版;原版本数值一行不改。
  • 页面:新增「预算调整」页签(表单 + 调整链列表)。
  • 不在本单:调整单表、审批流、调整额度上下限校验(须 DDL,另单)。

4.3 预算下达(release)

  • 语义:审批通过后的正式下达动作,把版本由 approved 置 active,是「审批 → 执行」链路的闭合点。
  • 接口:POST /api/cma/budget/versions/release,body {version};仅允许 approved 行 → active(行内混态/非 approved 一律 400,fail-closed);返回 count。写 OperationLog。
  • 幂等:重复下达返回 released: 0, already: true(不报错、不改数据)。
  • 页面:新增「下达」页签(待下达版本列表 + 下达按钮 + 确认弹窗 MyDialog)。
  • 不在本单:下达审批双签、下达通知推送、下达版本唯一性强制(见 §1.3 待确认项)。

4.4 预算考核(assess)

  • 语义:执行结果考核 —— 按生效预算 vs 实际值算达成率并分级,形成年度预算考核表。
  • 接口:GET /api/cma/budget/assessment?year=(只读):逐 KPI 输出 budget_total / actual_total / achieve_rate / grade(A/B/C/D) / basis_version,并给汇总。
  • 分级(最小版,按方向分叉):改善型 ≥100% → A / ≥90% → B / ≥80% → C / else D;费用型反向(≤100% → A)。
  • 页面:新增「预算考核」页签(汇总卡 + 明细表)。
  • 不在本单:考核指标权重、与绩效模块打通、考核周期冻结。

5. 阶段 4:数据清理写包(只编制,执行交 ops)

清单(研学实测快照 2026-10-03 18:05 CST):

# 项 规模 处置 回滚
B1 budget_month = 0 16 行 归档:status → 'archived' + remark 前缀 [年合计][归档] (初稿「补月」方案在编制期被否,理由见下) before-image 1:1 还原 status + remark
B2 source_type 全 NULL 301 行 按版本/来源补录(manual / kpi_generated / roll_forward),映射表逐版本固定 1:1 置回 NULL
B3 map_id 悬空 244 行 → 可回填 155 行(余 89 行地图维度无对应 KPI,保持 NULL) 按 kpi_id 归属战略地图回填(权威源 = 已发布地图 strategic_maps.dimensions 的 kpi_code;kpi_definitions.map_id 全表 0 行有值、map_objectives.kpis 仅 5 行非空,均不可用) 1:1 置回 NULL
B4 版本命名混乱 8 种 规范化映射表,只改 version 标签(如 v2026H2 → 2026H2),不改数值 1:1 反向映射还原

写包硬约束:四件 apply.sql / rollback.sql / verify.sql / before-image.rows.json(逐行 id + 改前值 + md5);reuse: forbidden;仅 UPDATE 所列字段;值来源逐条绑请求件指纹;执行交 ops 队列常态路由,本单不写库。

B1 处置变更(编制期实测推翻初稿「补月」方案):这 16 行 period='2026-00' 是年合计行(值 = 年度合计)。若把 budget_month 补成 1..12 任一月,POST /budget/sync-cash-plans 会以 datetime(budget_year, budget_month, 1) 生成收付款计划,upsert 键 (entity, related_kpi_id, plan_type, plan_date) 会与「该月的月度行」撞键互相覆盖(= 把年度总额写进某月度计划)。归档对现网可证中性:执行/对比/控制/考核按 period ∈ YYYY-01..12 取数,2026-00 本不在窗口内;版本列表不按 status 过滤;sync-cash-plans 只取 active ⇒ 归档行不再参与联动。残余 budget_month=0(模型注释 1-12)留待「年合计独立表达」另单。

B3 覆盖率(实测):权威源 = 已发布战略地图 strategic_maps.dimensions 的 kpi_code(kpi_definitions.map_id 全表 0 行有值、map_objectives.kpis 仅 5 行非空,均不可用)。244 行可回填 155 行(entity1→map 53 = 150、entity2→map 49 = 5),余 89 行在地图维度中无对应 KPI(如 F_GROSS_MARGIN)⇒ 保持 NULL 不编造,作数据缺口回报 finance。

写包落地:/srv/hermes/shared/budget-cleanup-20261003/(apply.sql / rollback.sql / verify.sql / before-image.rows.json / README.md / manifest.sha256),投 ops 队列由常态路由代执。

与状态机的耦合:§1.3 的「216 行 active → draft」DML 属待确认项,不进本单写包;确认后另编独立写包(同一纪律)。


6. 边界自证(本单不碰什么)

  • 不改 kpi_definitions 结构(direction / target_value)——属在途单 CMA-KPI-20261003.md。
  • 不碰 reports/dupont、predict/cvp 与缺件 KPI 注册——属在途单 finance-20261003-*。
  • 不动 cash_plans / CashPlanClassifyRule 数据与分类逻辑、不动取数采集逻辑、不碰 kpi_values。
  • 不删预算行:阶段 4 一律 UPDATE(status 改标或补值),rollback 1:1。
  • 零 push、零部署、零重启、零写库。