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)