# 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 迁移方案与兼容 - 只改 `` 内 `` 的**出现顺序**与标签文案,`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` 记 `调整单: 基线:` → 写 `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、零部署、零重启、零写库。