"""零基预算逐项论证 API — 管理会计OS (P2-① 2026-08-28) CRUD 逐项论证项(budget_zero_based_items) + generate 生成零基预算写 budget_plans。 method-comparison 的 zero_based 分支优先读论证项(有数据→逐项求和 is_demo=false)。 """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Optional from datetime import datetime from app.database import get_db from app.deps import get_entity_id from app.auth_middleware import require_auth, require_role from app.models import BudgetZeroBasedItem, KPIDefinition, BudgetPlan router = APIRouter( prefix="/api/cma/budget", tags=["零基预算"], dependencies=[Depends(require_role("ceo", "finance", "it"))], ) @router.get("/zero-based/items") def list_zero_based_items( kpi_id: Optional[int] = Query(None), period: Optional[str] = Query(None), db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id), ): """零基逐项论证列表(kpi_id+period 过滤)""" query = db.query(BudgetZeroBasedItem).filter(BudgetZeroBasedItem.entity_id == entity_id) if kpi_id: query = query.filter(BudgetZeroBasedItem.kpi_id == kpi_id) if period: query = query.filter(BudgetZeroBasedItem.period == period) rows = query.order_by(BudgetZeroBasedItem.id.desc()).all() kpi_ids = {r.kpi_id for r in rows} kpis = {k.id: k for k in db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()} if kpi_ids else {} result = [] for r in rows: kpi = kpis.get(r.kpi_id) result.append({ "id": r.id, "kpi_id": r.kpi_id, "kpi_code": kpi.kpi_code if kpi else "", "kpi_name": kpi.kpi_name if kpi else "", "period": r.period, "item_name": r.item_name, "item_category": r.item_category, "base_value": r.base_value, "justification": r.justification, "proposed_value": r.proposed_value, "status": r.status, "created_by": r.created_by, "created_at": r.created_at.isoformat() if r.created_at else None, }) total_proposed = round(sum(r.proposed_value for r in rows), 2) return {"data": result, "total": len(result), "total_proposed": total_proposed} @router.post("/zero-based/items") def create_zero_based_item( data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id), current_user=Depends(require_auth), ): """新建逐项论证项""" kpi_id = data.get("kpi_id") period = data.get("period") item_name = data.get("item_name") base_value = data.get("base_value") proposed_value = data.get("proposed_value") if not kpi_id or not period or not item_name: raise HTTPException(400, "缺少必要参数: kpi_id, period, item_name") if base_value is None: base_value = 0 if proposed_value is None: proposed_value = 0 kpi = db.query(KPIDefinition).filter( KPIDefinition.id == kpi_id, KPIDefinition.entity_id == entity_id, ).first() if not kpi: raise HTTPException(404, "KPI不存在") row = BudgetZeroBasedItem( entity_id=entity_id, kpi_id=kpi_id, period=period, item_name=item_name, item_category=data.get("item_category", "discretionary"), base_value=base_value, justification=data.get("justification"), proposed_value=proposed_value, status=data.get("status", "draft"), created_by=data.get("created_by") or (current_user.name if hasattr(current_user, "name") else None), ) db.add(row) db.commit() db.refresh(row) return {"message": "论证项已创建", "id": row.id} @router.put("/zero-based/items/{item_id}") def update_zero_based_item( item_id: int, data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id), current_user=Depends(require_auth), ): """更新逐项论证项""" row = db.query(BudgetZeroBasedItem).filter( BudgetZeroBasedItem.id == item_id, BudgetZeroBasedItem.entity_id == entity_id, ).first() if not row: raise HTTPException(404, "论证项不存在") for field in ("item_name", "item_category", "base_value", "justification", "proposed_value", "status"): if field in data: setattr(row, field, data[field]) db.commit() return {"message": "论证项已更新", "id": row.id} @router.delete("/zero-based/items/{item_id}") def delete_zero_based_item( item_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id), current_user=Depends(require_auth), ): """删除逐项论证项""" row = db.query(BudgetZeroBasedItem).filter( BudgetZeroBasedItem.id == item_id, BudgetZeroBasedItem.entity_id == entity_id, ).first() if not row: raise HTTPException(404, "论证项不存在") db.delete(row) db.commit() return {"message": "论证项已删除"} @router.post("/zero-based/generate") def generate_zero_based_budget( data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id), current_user=Depends(require_auth), ): """生成零基预算 = Σ(proposed_value),写 budget_plans(version='zbb-YYYYMMDD',calc_logic='zero_based_itemized')""" kpi_id = data.get("kpi_id") period = data.get("period") year = data.get("year") if not kpi_id or not period: raise HTTPException(400, "缺少必要参数: kpi_id, period") items = db.query(BudgetZeroBasedItem).filter( BudgetZeroBasedItem.entity_id == entity_id, BudgetZeroBasedItem.kpi_id == kpi_id, BudgetZeroBasedItem.period == period, ).all() if not items: raise HTTPException(400, f"期间 {period} 无逐项论证项,请先录入") total = round(sum(i.proposed_value for i in items), 2) # 解析年份 if not year: try: year = int(period.split("-")[0]) except Exception: year = datetime.now().year month = 0 try: month = int(period.split("-")[1]) if "-" in period else 0 except Exception: month = 0 version = f"zbb-{datetime.now().strftime('%Y%m%d')}" # 删除同KPI同期间同版本旧预算,防重复 db.query(BudgetPlan).filter( BudgetPlan.entity_id == entity_id, BudgetPlan.kpi_id == kpi_id, BudgetPlan.period == period, BudgetPlan.version == version, ).delete() bp = BudgetPlan( entity_id=entity_id, kpi_id=kpi_id, period=period, budget_value=total, budget_year=year, budget_month=month, version=version, status="active", source_type="zero_based", calc_logic="zero_based_itemized", remark=f"零基逐项论证生成: {len(items)}项 Σ(proposed_value)={total}", ) db.add(bp) db.commit() return { "message": f"零基预算已生成: {total}({len(items)}项论证)", "kpi_id": kpi_id, "period": period, "total": total, "item_count": len(items), "version": version, "plan_id": bp.id, }