From e82a6af59c68868a357a3087f4962c9b122ff06b Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Thu, 27 Aug 2026 16:54:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A2=84=E7=AE=97=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=96=AD=E7=82=B9=E4=BF=AE=E5=A4=8D=20=E2=80=94=20=E7=8E=B0?= =?UTF-8?q?=E9=87=91=E6=B5=81=E8=81=94=E5=8A=A8+=E8=A1=8C=E5=8A=A8?= =?UTF-8?q?=E6=96=B9=E6=A1=88=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 断点#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), 构建部署 --- backend/app/api/budget.py | 70 +++++++++++++++++++++++++ backend/app/models/__init__.py | 2 + frontend/src/views/BudgetManagement.vue | 16 ++++++ 3 files changed, 88 insertions(+) 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)