diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py
index 243f9fa4..eb0ac7a9 100644
--- a/backend/app/api/budget.py
+++ b/backend/app/api/budget.py
@@ -1308,3 +1308,73 @@ def diff_budget_versions(
},
"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}
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index 7e257f1a..c8575263 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -485,6 +485,8 @@ class CashPlan(Base):
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), default=1, comment="企业ID")
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="金额(万元)")
plan_date = Column(DateTime, nullable=False, comment="计划日期(应收即到期日)")
counterparty = Column(String(200), nullable=True, comment="关联客户/供应商")
diff --git a/frontend/src/views/BudgetManagement.vue b/frontend/src/views/BudgetManagement.vue
index 52fa980d..0a9ba65a 100644
--- a/frontend/src/views/BudgetManagement.vue
+++ b/frontend/src/views/BudgetManagement.vue
@@ -178,6 +178,7 @@
刷新
+ ⇄ 同步现金流计划
{{ card.label }}
{{ card.value }}
@@ -447,6 +448,21 @@ const filterYear = ref(currentYear); const filterMonth = ref(0); const searchKpi
const loading = ref(false); const budgetList = ref([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const noMap = 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([])
const selectedMapId = ref(null)