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)
17 KiB
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/main8631ce7),不 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现网取值:active216 行 /approved85 行(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、零部署、零重启、零写库。