seaislee1209 90707005ed
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m27s
Build and Deploy Web / build-and-deploy (push) Successful in 54s
feat: V2功能增强 — 里程碑系统+圆环进度图+损耗修复+AI服务+报告系统
- 项目详情页三阶段里程碑管理(前期/制作/后期)
- 制作卡片改用180px ECharts圆环进度图+右侧数据列表
- 修复损耗率双重计算bug(测试秒数不再重复计入超产)
- 新增飞书推送服务、豆包AI风险分析、APScheduler定时报告
- 项目列表页增强(筛选/排序/批量操作/废弃功能)
- 成员详情页产出时间轴+效率对比
- 成本页固定开支管理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:36:44 +08:00

270 lines
9.1 KiB
Python

"""成本管理路由"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import date
from database import get_db
from models import (
User, AIToolCost, AIToolCostAllocation, OutsourceCost,
CostOverride, OverheadCost, SubscriptionPeriod, CostAllocationType,
OutsourceType, OverheadCostType
)
from schemas import (
AIToolCostCreate, AIToolCostOut, OutsourceCostCreate, OutsourceCostOut,
CostOverrideCreate, OverheadCostCreate, OverheadCostOut
)
from auth import get_current_user, require_permission
router = APIRouter(prefix="/api/costs", tags=["成本管理"])
# ──────────────────── AI 工具成本 ────────────────────
@router.get("/ai-tools", response_model=List[AIToolCostOut])
def list_ai_tool_costs(
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_ai:view"))
):
costs = db.query(AIToolCost).order_by(AIToolCost.record_date.desc()).all()
return [
AIToolCostOut(
id=c.id, tool_name=c.tool_name,
subscription_period=c.subscription_period.value if hasattr(c.subscription_period, 'value') else c.subscription_period,
amount=c.amount,
allocation_type=c.allocation_type.value if hasattr(c.allocation_type, 'value') else c.allocation_type,
project_id=c.project_id,
recorded_by=c.recorded_by,
record_date=c.record_date,
created_at=c.created_at,
)
for c in costs
]
@router.post("/ai-tools", response_model=AIToolCostOut)
def create_ai_tool_cost(
req: AIToolCostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_ai:create"))
):
cost = AIToolCost(
tool_name=req.tool_name,
subscription_period=SubscriptionPeriod(req.subscription_period),
amount=req.amount,
allocation_type=CostAllocationType(req.allocation_type),
project_id=req.project_id,
recorded_by=current_user.id,
record_date=req.record_date,
)
db.add(cost)
db.flush()
# 处理手动分摊
if req.allocation_type == "手动分摊" and req.allocations:
for alloc in req.allocations:
db.add(AIToolCostAllocation(
ai_tool_cost_id=cost.id,
project_id=alloc["project_id"],
percentage=alloc["percentage"],
))
db.commit()
db.refresh(cost)
return AIToolCostOut(
id=cost.id, tool_name=cost.tool_name,
subscription_period=cost.subscription_period.value,
amount=cost.amount,
allocation_type=cost.allocation_type.value,
project_id=cost.project_id,
recorded_by=cost.recorded_by,
record_date=cost.record_date,
created_at=cost.created_at,
)
@router.delete("/ai-tools/{cost_id}")
def delete_ai_tool_cost(
cost_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_ai:delete"))
):
cost = db.query(AIToolCost).filter(AIToolCost.id == cost_id).first()
if not cost:
raise HTTPException(status_code=404, detail="记录不存在")
db.query(AIToolCostAllocation).filter(AIToolCostAllocation.ai_tool_cost_id == cost_id).delete()
db.delete(cost)
db.commit()
return {"message": "已删除"}
# ──────────────────── 外包成本 ────────────────────
@router.get("/outsource", response_model=List[OutsourceCostOut])
def list_outsource_costs(
project_id: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_outsource:view"))
):
q = db.query(OutsourceCost)
if project_id:
q = q.filter(OutsourceCost.project_id == project_id)
costs = q.order_by(OutsourceCost.record_date.desc()).all()
return [
OutsourceCostOut(
id=c.id, project_id=c.project_id,
outsource_type=c.outsource_type.value if hasattr(c.outsource_type, 'value') else c.outsource_type,
episode_start=c.episode_start, episode_end=c.episode_end,
amount=c.amount, recorded_by=c.recorded_by,
record_date=c.record_date, created_at=c.created_at,
)
for c in costs
]
@router.post("/outsource", response_model=OutsourceCostOut)
def create_outsource_cost(
req: OutsourceCostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_outsource:create"))
):
cost = OutsourceCost(
project_id=req.project_id,
outsource_type=OutsourceType(req.outsource_type),
episode_start=req.episode_start,
episode_end=req.episode_end,
amount=req.amount,
recorded_by=current_user.id,
record_date=req.record_date,
)
db.add(cost)
db.commit()
db.refresh(cost)
return OutsourceCostOut(
id=cost.id, project_id=cost.project_id,
outsource_type=cost.outsource_type.value,
episode_start=cost.episode_start, episode_end=cost.episode_end,
amount=cost.amount, recorded_by=cost.recorded_by,
record_date=cost.record_date, created_at=cost.created_at,
)
@router.delete("/outsource/{cost_id}")
def delete_outsource_cost(
cost_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_outsource:delete"))
):
cost = db.query(OutsourceCost).filter(OutsourceCost.id == cost_id).first()
if not cost:
raise HTTPException(status_code=404, detail="记录不存在")
db.delete(cost)
db.commit()
return {"message": "已删除"}
# ──────────────────── 人力成本手动调整 ────────────────────
@router.post("/overrides")
def create_cost_override(
req: CostOverrideCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_labor:create"))
):
override = CostOverride(
user_id=req.user_id,
date=req.date,
project_id=req.project_id,
override_amount=req.override_amount,
adjusted_by=current_user.id,
reason=req.reason,
)
db.add(override)
db.commit()
return {"message": "已保存成本调整"}
@router.get("/overrides")
def list_cost_overrides(
user_id: Optional[int] = Query(None),
project_id: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_labor:view"))
):
q = db.query(CostOverride)
if user_id:
q = q.filter(CostOverride.user_id == user_id)
if project_id:
q = q.filter(CostOverride.project_id == project_id)
records = q.order_by(CostOverride.date.desc()).all()
return [
{
"id": r.id, "user_id": r.user_id, "date": r.date,
"project_id": r.project_id, "override_amount": r.override_amount,
"adjusted_by": r.adjusted_by, "reason": r.reason,
"created_at": r.created_at,
}
for r in records
]
# ──────────────────── 固定开支(办公室租金、水电费) ────────────────────
@router.get("/overhead", response_model=List[OverheadCostOut])
def list_overhead_costs(
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_overhead:view"))
):
costs = db.query(OverheadCost).order_by(OverheadCost.record_month.desc()).all()
return [
OverheadCostOut(
id=c.id,
cost_type=c.cost_type.value if hasattr(c.cost_type, 'value') else c.cost_type,
amount=c.amount,
record_month=c.record_month,
note=c.note,
recorded_by=c.recorded_by,
created_at=c.created_at,
)
for c in costs
]
@router.post("/overhead", response_model=OverheadCostOut)
def create_overhead_cost(
req: OverheadCostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_overhead:create"))
):
cost = OverheadCost(
cost_type=OverheadCostType(req.cost_type),
amount=req.amount,
record_month=req.record_month,
note=req.note,
recorded_by=current_user.id,
)
db.add(cost)
db.commit()
db.refresh(cost)
return OverheadCostOut(
id=cost.id,
cost_type=cost.cost_type.value,
amount=cost.amount,
record_month=cost.record_month,
note=cost.note,
recorded_by=cost.recorded_by,
created_at=cost.created_at,
)
@router.delete("/overhead/{cost_id}")
def delete_overhead_cost(
cost_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("cost_overhead:delete"))
):
cost = db.query(OverheadCost).filter(OverheadCost.id == cost_id).first()
if not cost:
raise HTTPException(status_code=404, detail="记录不存在")
db.delete(cost)
db.commit()
return {"message": "已删除"}