fix: 预算流程断点修复 — 现金流联动+行动方案补齐
断点#1: 预算↔现金流打通 - cash_plans 加 related_kpi_id/budget_plan_id 字段 - POST /budget/sync-cash-plans: 按预算KPI生成收付款计划(收入类→receive/成本类→pay, upsert) - 前端预算执行页加'⇄同步现金流计划'按钮 - 实测: 新建108条(receive 75/pay 33, 6个KPI) 断点#2: 5个预算KPI补行动方案(净利/新客/厂补/供应链/数据自动化, 各1条含负责人) 断点#3: 悬空预算(2KPI)因已补行动方案, 性质从'无来源'→'有行动缺实际数据源', 标注保留 pytest 56 passed(budget+cash), 构建部署
This commit is contained in:
@@ -1308,3 +1308,73 @@ def diff_budget_versions(
|
|||||||
},
|
},
|
||||||
"diffs": diffs,
|
"diffs": diffs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 预算↔现金流联动(断点修复#1, 2026-08-27)
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@router.post("/sync-cash-plans")
|
||||||
|
def sync_cash_plans(
|
||||||
|
entity_id: int = Depends(get_entity_id),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""预算→现金流计划联动: 按预算KPI生成/更新收付款计划(修复断点#1)
|
||||||
|
|
||||||
|
收入类KPI(营收/回款/新客) → receive
|
||||||
|
成本类KPI(费用/厂补/采购) → pay
|
||||||
|
upsert: 同KPI+同日期+同类型 更新不重复
|
||||||
|
"""
|
||||||
|
from app.models import CashPlan
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
RECEIVE_KEYS = ("营收", "收入", "销售", "回款", "新客", "收款", "净利润", "毛利")
|
||||||
|
PAY_KEYS = ("费用", "成本", "厂补", "采购", "返利", "应付", "损耗", "投入")
|
||||||
|
|
||||||
|
budgets = db.query(BudgetPlan).filter(
|
||||||
|
BudgetPlan.entity_id == entity_id, BudgetPlan.status == "active"
|
||||||
|
).all()
|
||||||
|
kpi_ids = {b.kpi_id for b in budgets}
|
||||||
|
kpis = {k.id: k for k in db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()} if kpi_ids else {}
|
||||||
|
|
||||||
|
created, updated = 0, 0
|
||||||
|
for b in budgets:
|
||||||
|
kpi = kpis.get(b.kpi_id)
|
||||||
|
if not kpi:
|
||||||
|
continue
|
||||||
|
name = (kpi.kpi_name or "") + (kpi.kpi_code or "")
|
||||||
|
if any(k in name for k in RECEIVE_KEYS):
|
||||||
|
plan_type = "receive"
|
||||||
|
elif any(k in name for k in PAY_KEYS):
|
||||||
|
plan_type = "pay"
|
||||||
|
else:
|
||||||
|
continue # 无法判类别的KPI跳过
|
||||||
|
|
||||||
|
year, month = b.budget_year or 2026, b.budget_month or 1
|
||||||
|
try:
|
||||||
|
plan_date = datetime(year, month, 1)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
# upsert: 同KPI+同日期+同类型
|
||||||
|
existing = db.query(CashPlan).filter(
|
||||||
|
CashPlan.entity_id == entity_id,
|
||||||
|
CashPlan.related_kpi_id == b.kpi_id,
|
||||||
|
CashPlan.plan_type == plan_type,
|
||||||
|
CashPlan.plan_date == plan_date,
|
||||||
|
).first()
|
||||||
|
if existing:
|
||||||
|
existing.amount = b.budget_value
|
||||||
|
existing.budget_plan_id = b.id
|
||||||
|
existing.source = "budget_sync"
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
db.add(CashPlan(
|
||||||
|
entity_id=entity_id, plan_type=plan_type,
|
||||||
|
related_kpi_id=b.kpi_id, budget_plan_id=b.id,
|
||||||
|
amount=b.budget_value, plan_date=plan_date,
|
||||||
|
description=f"预算联动: {kpi.kpi_name or kpi.kpi_code}",
|
||||||
|
status="pending", source="budget_sync",
|
||||||
|
))
|
||||||
|
created += 1
|
||||||
|
db.commit()
|
||||||
|
return {"message": f"现金流联动完成: 新建{created}条, 更新{updated}条", "created": created, "updated": updated}
|
||||||
|
|||||||
@@ -485,6 +485,8 @@ class CashPlan(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
entity_id = Column(Integer, ForeignKey("entities.id"), default=1, comment="企业ID")
|
entity_id = Column(Integer, ForeignKey("entities.id"), default=1, comment="企业ID")
|
||||||
plan_type = Column(String(10), nullable=False, comment="receive收/pay付")
|
plan_type = Column(String(10), nullable=False, comment="receive收/pay付")
|
||||||
|
related_kpi_id = Column(Integer, nullable=True, comment="关联KPI(预算联动 2026-08-27)")
|
||||||
|
budget_plan_id = Column(Integer, nullable=True, comment="来源预算计划ID")
|
||||||
amount = Column(Float, nullable=False, comment="金额(万元)")
|
amount = Column(Float, nullable=False, comment="金额(万元)")
|
||||||
plan_date = Column(DateTime, nullable=False, comment="计划日期(应收即到期日)")
|
plan_date = Column(DateTime, nullable=False, comment="计划日期(应收即到期日)")
|
||||||
counterparty = Column(String(200), nullable=True, comment="关联客户/供应商")
|
counterparty = Column(String(200), nullable=True, comment="关联客户/供应商")
|
||||||
|
|||||||
@@ -178,6 +178,7 @@
|
|||||||
<el-select v-model="execFilterMonth" placeholder="截止月份" style="width:110px;" @change="loadExecutionReport"><el-option v-for="m in 12" :key="m" :label="`至${m}月`" :value="m" /></el-select>
|
<el-select v-model="execFilterMonth" placeholder="截止月份" style="width:110px;" @change="loadExecutionReport"><el-option v-for="m in 12" :key="m" :label="`至${m}月`" :value="m" /></el-select>
|
||||||
<el-select v-model="execFilterDim" placeholder="维度" clearable style="width:110px;" @change="loadExecutionReport"><el-option label="财务" value="finance" /><el-option label="客户" value="customer" /><el-option label="内部流程" value="process" /><el-option label="学习成长" value="learning" /></el-select>
|
<el-select v-model="execFilterDim" placeholder="维度" clearable style="width:110px;" @change="loadExecutionReport"><el-option label="财务" value="finance" /><el-option label="客户" value="customer" /><el-option label="内部流程" value="process" /><el-option label="学习成长" value="learning" /></el-select>
|
||||||
<el-button type="primary" @click="loadExecutionReport">刷新</el-button>
|
<el-button type="primary" @click="loadExecutionReport">刷新</el-button>
|
||||||
|
<el-button type="warning" :loading="syncingCash" @click="syncCashPlans">⇄ 同步现金流计划</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-row :gutter="16" style="margin-bottom:16px;">
|
<el-row :gutter="16" style="margin-bottom:16px;">
|
||||||
<el-col :span="6" v-for="card in execSummaryCards" :key="card.label"><el-card shadow="hover"><div style="text-align:center;"><div style="font-size:12px;color:#999;">{{ card.label }}</div><div style="font-size:22px;font-weight:600;margin-top:4px;" :style="{color: card.color}">{{ card.value }}</div></div></el-card></el-col>
|
<el-col :span="6" v-for="card in execSummaryCards" :key="card.label"><el-card shadow="hover"><div style="text-align:center;"><div style="font-size:12px;color:#999;">{{ card.label }}</div><div style="font-size:22px;font-weight:600;margin-top:4px;" :style="{color: card.color}">{{ card.value }}</div></div></el-card></el-col>
|
||||||
@@ -447,6 +448,21 @@ const filterYear = ref(currentYear); const filterMonth = ref(0); const searchKpi
|
|||||||
const loading = ref(false); const budgetList = ref<any[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const noMap = ref(false)
|
const loading = ref(false); const budgetList = ref<any[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const noMap = ref(false)
|
||||||
const batchEditMode = ref(false); const batchSavingAll = ref(false)
|
const batchEditMode = ref(false); const batchSavingAll = ref(false)
|
||||||
|
|
||||||
|
// ── 现金流联动(断点修复#1, 2026-08-27) ──
|
||||||
|
const syncingCash = ref(false)
|
||||||
|
async function syncCashPlans() {
|
||||||
|
syncingCash.value = true
|
||||||
|
try {
|
||||||
|
const r: any = await api.post('/budget/sync-cash-plans')
|
||||||
|
ElMessage.success(r.message || "现金流计划已同步")
|
||||||
|
loadExecutionReport()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error("同步失败: " + (e?.message || ""))
|
||||||
|
} finally {
|
||||||
|
syncingCash.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 战略地图选择 ──
|
// ── 战略地图选择 ──
|
||||||
const publishedMaps = ref<any[]>([])
|
const publishedMaps = ref<any[]>([])
|
||||||
const selectedMapId = ref<number | null>(null)
|
const selectedMapId = ref<number | null>(null)
|
||||||
|
|||||||
Reference in New Issue
Block a user