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:
@@ -0,0 +1,199 @@
|
||||
# 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`:
|
||||
|
||||
```python
|
||||
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、零部署、零重启、零写库。
|
||||
Reference in New Issue
Block a user